From cb4e2fcf43aae7c9009bcb807ab546ddf458bb09 Mon Sep 17 00:00:00 2001 From: Solomon Hykes Date: Wed, 13 Feb 2013 17:10:00 -0800 Subject: [PATCH 001/978] Moved server and client logic into sub-packages docker/server and docker/client, respectively. The UI is not affected. Upstream-commit: f5594142a8cb8f1bd3136eb6919779c8e47a5f1a Component: cli --- components/cli/client.go | 123 ++++++++++++++++++++++++++ components/cli/term.go | 145 +++++++++++++++++++++++++++++++ components/cli/termios_darwin.go | 8 ++ components/cli/termios_linux.go | 8 ++ 4 files changed, 284 insertions(+) create mode 100644 components/cli/client.go create mode 100644 components/cli/term.go create mode 100644 components/cli/termios_darwin.go create mode 100644 components/cli/termios_linux.go diff --git a/components/cli/client.go b/components/cli/client.go new file mode 100644 index 0000000000..6c5e6c4c99 --- /dev/null +++ b/components/cli/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/components/cli/term.go b/components/cli/term.go new file mode 100644 index 0000000000..8b58611cd9 --- /dev/null +++ b/components/cli/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/components/cli/termios_darwin.go b/components/cli/termios_darwin.go new file mode 100644 index 0000000000..185687920c --- /dev/null +++ b/components/cli/termios_darwin.go @@ -0,0 +1,8 @@ +package client + +import "syscall" + +const ( + getTermios = syscall.TIOCGETA + setTermios = syscall.TIOCSETA +) diff --git a/components/cli/termios_linux.go b/components/cli/termios_linux.go new file mode 100644 index 0000000000..36957c44a1 --- /dev/null +++ b/components/cli/termios_linux.go @@ -0,0 +1,8 @@ +package client + +import "syscall" + +const ( + getTermios = syscall.TCGETS + setTermios = syscall.TCSETS +) From 066591d5054c5014d9cb549cc55cc8eba3a0f70e Mon Sep 17 00:00:00 2001 From: Solomon Hykes Date: Thu, 14 Feb 2013 13:49:05 -0800 Subject: [PATCH 002/978] 'docker start' and 'docker restart': start or restart a container Upstream-commit: 29aab0e4bf614ff426977b5ecb081367c0e256f2 Component: cli --- components/cli/client.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/cli/client.go b/components/cli/client.go index 6c5e6c4c99..164e1be321 100644 --- a/components/cli/client.go +++ b/components/cli/client.go @@ -89,6 +89,8 @@ func InteractiveMode(scripts ...string) error { "kill", "wait", "stop", + "start", + "restart", "logs", "diff", "commit", From 477023814cc52d3ad373e5cdf1dcaac3c5840713 Mon Sep 17 00:00:00 2001 From: Jeff Lindsay Date: Wed, 20 Feb 2013 12:19:22 +0000 Subject: [PATCH 003/978] 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) Upstream-commit: 4e24b235c30a47b05920871596131beca3d4dcd0 Component: cli --- components/cli/term.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/components/cli/term.go b/components/cli/term.go index 8b58611cd9..7606cbd42b 100644 --- a/components/cli/term.go +++ b/components/cli/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 2d5e2c98cbce1c42f3780a94fea69586e47ee6c8 Mon Sep 17 00:00:00 2001 From: Jeff Lindsay Date: Wed, 20 Feb 2013 12:23:47 +0000 Subject: [PATCH 004/978] white space Upstream-commit: 52fa34605ca1fe604ec483aad864366ef21a1c9d Component: cli --- components/cli/term.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/cli/term.go b/components/cli/term.go index 7606cbd42b..ed52be96b4 100644 --- a/components/cli/term.go +++ b/components/cli/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 00a0161ee7bd6e31095d3d813d2c24ad5c44cdc3 Mon Sep 17 00:00:00 2001 From: Solomon Hykes Date: Wed, 20 Feb 2013 14:45:43 -0800 Subject: [PATCH 005/978] Interactive mode preserves existing PATH, to facilitate scripting Upstream-commit: 754cf30b0e88d67a0e04a8e4b1792669788a2c1b Component: cli --- components/cli/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/client.go b/components/cli/client.go index 164e1be321..5a8aac3807 100644 --- a/components/cli/client.go +++ b/components/cli/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 034971c8eeda777abd640587fabc156078123ff2 Mon Sep 17 00:00:00 2001 From: Solomon Hykes Date: Tue, 26 Feb 2013 17:26:46 -0800 Subject: [PATCH 006/978] go fmt Upstream-commit: df5134f46e7169fb7bec63eb5c33ed20115d3539 Component: cli --- components/cli/client.go | 4 +- components/cli/term.go | 172 +++++++++++++++++++-------------------- 2 files changed, 86 insertions(+), 90 deletions(-) diff --git a/components/cli/client.go b/components/cli/client.go index 5a8aac3807..4c4ea1c5e3 100644 --- a/components/cli/client.go +++ b/components/cli/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/components/cli/term.go b/components/cli/term.go index ed52be96b4..a988d0d796 100644 --- a/components/cli/term.go +++ b/components/cli/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 07e40068fc8f54bf28637a4d5560a5fffa0199d0 Mon Sep 17 00:00:00 2001 From: Solomon Hykes Date: Sat, 9 Mar 2013 19:44:09 -0800 Subject: [PATCH 007/978] gofmt Upstream-commit: 3de7ff271caa84f02f2c68e0d9122fdfa113bdb7 Component: cli --- components/cli/client.go | 4 +- components/cli/term.go | 172 +++++++++++++++++++-------------------- 2 files changed, 86 insertions(+), 90 deletions(-) diff --git a/components/cli/client.go b/components/cli/client.go index 5a8aac3807..4c4ea1c5e3 100644 --- a/components/cli/client.go +++ b/components/cli/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/components/cli/term.go b/components/cli/term.go index ed52be96b4..a988d0d796 100644 --- a/components/cli/term.go +++ b/components/cli/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 82b549063d2d9dee438deb1841c348a281ab2a8a Mon Sep 17 00:00:00 2001 From: shin- Date: Mon, 11 Mar 2013 07:39:06 -0700 Subject: [PATCH 008/978] post-merge repairs Upstream-commit: b4b078c5ae8f9f34d6f4af682a2455cd1ef6596b Component: cli --- components/cli/client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/client.go b/components/cli/client.go index 4c4ea1c5e3..a277a4b18f 100644 --- a/components/cli/client.go +++ b/components/cli/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 4b2bbc5754777431c0196301aa84886ef47b358e Mon Sep 17 00:00:00 2001 From: "Guillaume J. Charmes" Date: Mon, 11 Mar 2013 02:59:52 -0700 Subject: [PATCH 009/978] Change relative paths to absolute Upstream-commit: 39ad2cf8d371ec0fbcc810a399d35a8d4a017536 Component: cli --- components/cli/client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/client.go b/components/cli/client.go index a277a4b18f..4c4ea1c5e3 100644 --- a/components/cli/client.go +++ b/components/cli/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 45ad596952ee911470f2c35117c73ed757ff137d Mon Sep 17 00:00:00 2001 From: "Guillaume J. Charmes" Date: Tue, 12 Mar 2013 05:17:51 -0700 Subject: [PATCH 010/978] Put back the relative paths for dev purpose Upstream-commit: ab1211bcb8f346ec371a9225e6a534b926504dc4 Component: cli --- components/cli/client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/client.go b/components/cli/client.go index 4c4ea1c5e3..a277a4b18f 100644 --- a/components/cli/client.go +++ b/components/cli/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 cf4a1a8ac7c67a844a430dfdb89619126868c61b Mon Sep 17 00:00:00 2001 From: Louis Opter Date: Tue, 12 Mar 2013 12:12:40 -0700 Subject: [PATCH 011/978] Automatically remove the rcfile generated by docker -i from /tmp Upstream-commit: d895b3a7f8e824453b7ba63c102721ef66ab69b9 Component: cli --- components/cli/client.go | 1 + 1 file changed, 1 insertion(+) diff --git a/components/cli/client.go b/components/cli/client.go index 4c4ea1c5e3..814aed2f18 100644 --- a/components/cli/client.go +++ b/components/cli/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 f9945bccac95f54b893fd18e65f313a25cc12791 Mon Sep 17 00:00:00 2001 From: creack Date: Tue, 12 Mar 2013 11:59:27 -0700 Subject: [PATCH 012/978] Put back the github.com path for the import Upstream-commit: 20c2c684b20748eeb688fa9aa33df2d9efe3f136 Component: cli --- components/cli/client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/client.go b/components/cli/client.go index 073fe02b0b..814aed2f18 100644 --- a/components/cli/client.go +++ b/components/cli/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 ab2846c8812135f70c967c8135c2777402177292 Mon Sep 17 00:00:00 2001 From: Solomon Hykes Date: Tue, 12 Mar 2013 15:05:41 -0700 Subject: [PATCH 013/978] Removed interactive mode ('docker -i'). Cool UI experiment but seems more trouble than it's worth Upstream-commit: ae5f2d9a567ea9e45a9cc9e82b7759cc725d777a Component: cli --- components/cli/client.go | 73 ---------------------------------------- 1 file changed, 73 deletions(-) diff --git a/components/cli/client.go b/components/cli/client.go index 073fe02b0b..815e20a048 100644 --- a/components/cli/client.go +++ b/components/cli/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 b5dcd430d5cfd3bbe42c79f2265466760f6a2e61 Mon Sep 17 00:00:00 2001 From: Solomon Hykes Date: Wed, 13 Mar 2013 00:29:40 -0700 Subject: [PATCH 014/978] 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 Upstream-commit: 7f13a9cf3a1673c42868192a85a60f7614c9a85f Component: cli --- components/cli/client.go | 53 ------------ components/cli/term.go | 143 ------------------------------- components/cli/termios_darwin.go | 8 -- components/cli/termios_linux.go | 8 -- 4 files changed, 212 deletions(-) delete mode 100644 components/cli/client.go delete mode 100644 components/cli/term.go delete mode 100644 components/cli/termios_darwin.go delete mode 100644 components/cli/termios_linux.go diff --git a/components/cli/client.go b/components/cli/client.go deleted file mode 100644 index 30d741f8bd..0000000000 --- a/components/cli/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/components/cli/term.go b/components/cli/term.go deleted file mode 100644 index a988d0d796..0000000000 --- a/components/cli/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/components/cli/termios_darwin.go b/components/cli/termios_darwin.go deleted file mode 100644 index 185687920c..0000000000 --- a/components/cli/termios_darwin.go +++ /dev/null @@ -1,8 +0,0 @@ -package client - -import "syscall" - -const ( - getTermios = syscall.TIOCGETA - setTermios = syscall.TIOCSETA -) diff --git a/components/cli/termios_linux.go b/components/cli/termios_linux.go deleted file mode 100644 index 36957c44a1..0000000000 --- a/components/cli/termios_linux.go +++ /dev/null @@ -1,8 +0,0 @@ -package client - -import "syscall" - -const ( - getTermios = syscall.TCGETS - setTermios = syscall.TCSETS -) From 5f31535973fc9ba55cf95305868e1253757b4ad4 Mon Sep 17 00:00:00 2001 From: Tibor Vass Date: Tue, 5 May 2015 00:18:28 -0400 Subject: [PATCH 015/978] cli: new daemon command and new cli package This patch creates a new cli package that allows to combine both client and daemon commands (there is only one daemon command: docker daemon). The `-d` and `--daemon` top-level flags are deprecated and a special message is added to prompt the user to use `docker daemon`. Providing top-level daemon-specific flags for client commands result in an error message prompting the user to use `docker daemon`. This patch does not break any old but correct usages. This also makes `-d` and `--daemon` flags, as well as the `daemon` command illegal in client-only binaries. Signed-off-by: Tibor Vass Upstream-commit: c023f818aa9f1c4bee81079a4ae1cef45555429a Component: cli --- components/cli/cli.go | 200 +++++++++++++++++++++++++++++++++++++++ components/cli/client.go | 12 +++ components/cli/common.go | 20 ++++ 3 files changed, 232 insertions(+) create mode 100644 components/cli/cli.go create mode 100644 components/cli/client.go create mode 100644 components/cli/common.go diff --git a/components/cli/cli.go b/components/cli/cli.go new file mode 100644 index 0000000000..8e559fc3fd --- /dev/null +++ b/components/cli/cli.go @@ -0,0 +1,200 @@ +package cli + +import ( + "errors" + "fmt" + "io" + "os" + "reflect" + "strings" + + flag "github.com/docker/docker/pkg/mflag" +) + +// Cli represents a command line interface. +type Cli struct { + Stderr io.Writer + handlers []Handler + Usage func() +} + +// Handler holds the different commands Cli will call +// It should have methods with names starting with `Cmd` like: +// func (h myHandler) CmdFoo(args ...string) error +type Handler interface{} + +// Initializer can be optionally implemented by a Handler to +// initialize before each call to one of its commands. +type Initializer interface { + Initialize() error +} + +// New instantiates a ready-to-use Cli. +func New(handlers ...Handler) *Cli { + // make the generic Cli object the first cli handler + // in order to handle `docker help` appropriately + cli := new(Cli) + cli.handlers = append([]Handler{cli}, handlers...) + return cli +} + +// initErr is an error returned upon initialization of a handler implementing Initializer. +type initErr struct{ error } + +func (err initErr) Error() string { + return err.Error() +} + +func (cli *Cli) command(args ...string) (func(...string) error, error) { + for _, c := range cli.handlers { + if c == nil { + continue + } + camelArgs := make([]string, len(args)) + for i, s := range args { + if len(s) == 0 { + return nil, errors.New("empty command") + } + camelArgs[i] = strings.ToUpper(s[:1]) + strings.ToLower(s[1:]) + } + methodName := "Cmd" + strings.Join(camelArgs, "") + method := reflect.ValueOf(c).MethodByName(methodName) + if method.IsValid() { + if c, ok := c.(Initializer); ok { + if err := c.Initialize(); err != nil { + return nil, initErr{err} + } + } + return method.Interface().(func(...string) error), nil + } + } + return nil, errors.New("command not found") +} + +// Run executes the specified command. +func (cli *Cli) Run(args ...string) error { + if len(args) > 1 { + command, err := cli.command(args[:2]...) + switch err := err.(type) { + case nil: + return command(args[2:]...) + case initErr: + return err.error + } + } + if len(args) > 0 { + command, err := cli.command(args[0]) + switch err := err.(type) { + case nil: + return command(args[1:]...) + case initErr: + return err.error + } + cli.noSuchCommand(args[0]) + } + return cli.CmdHelp() +} + +func (cli *Cli) noSuchCommand(command string) { + if cli.Stderr == nil { + cli.Stderr = os.Stderr + } + fmt.Fprintf(cli.Stderr, "docker: '%s' is not a docker command.\nSee 'docker --help'.\n", command) + os.Exit(1) +} + +// CmdHelp displays information on a Docker command. +// +// If more than one command is specified, information is only shown for the first command. +// +// Usage: docker help COMMAND or docker COMMAND --help +func (cli *Cli) CmdHelp(args ...string) error { + if len(args) > 1 { + command, err := cli.command(args[:2]...) + switch err := err.(type) { + case nil: + command("--help") + return nil + case initErr: + return err.error + } + } + if len(args) > 0 { + command, err := cli.command(args[0]) + switch err := err.(type) { + case nil: + command("--help") + return nil + case initErr: + return err.error + } + cli.noSuchCommand(args[0]) + } + + if cli.Usage == nil { + flag.Usage() + } else { + cli.Usage() + } + + return nil +} + +// Subcmd is a subcommand of the main "docker" command. +// A subcommand represents an action that can be performed +// from the Docker command line client. +// +// To see all available subcommands, run "docker --help". +func Subcmd(name string, synopses []string, description string, exitOnError bool) *flag.FlagSet { + var errorHandling flag.ErrorHandling + if exitOnError { + errorHandling = flag.ExitOnError + } else { + errorHandling = flag.ContinueOnError + } + flags := flag.NewFlagSet(name, errorHandling) + flags.Usage = func() { + flags.ShortUsage() + flags.PrintDefaults() + } + + flags.ShortUsage = func() { + options := "" + if flags.FlagCountUndeprecated() > 0 { + options = " [OPTIONS]" + } + + if len(synopses) == 0 { + synopses = []string{""} + } + + // Allow for multiple command usage synopses. + for i, synopsis := range synopses { + lead := "\t" + if i == 0 { + // First line needs the word 'Usage'. + lead = "Usage:\t" + } + + if synopsis != "" { + synopsis = " " + synopsis + } + + fmt.Fprintf(flags.Out(), "\n%sdocker %s%s%s", lead, name, options, synopsis) + } + + fmt.Fprintf(flags.Out(), "\n\n%s\n", description) + } + + return flags +} + +// An StatusError reports an unsuccessful exit by a command. +type StatusError struct { + Status string + StatusCode int +} + +func (e StatusError) Error() string { + return fmt.Sprintf("Status: %s, Code: %d", e.Status, e.StatusCode) +} diff --git a/components/cli/client.go b/components/cli/client.go new file mode 100644 index 0000000000..6a82eb52a5 --- /dev/null +++ b/components/cli/client.go @@ -0,0 +1,12 @@ +package cli + +import flag "github.com/docker/docker/pkg/mflag" + +// ClientFlags represents flags for the docker client. +type ClientFlags struct { + FlagSet *flag.FlagSet + Common *CommonFlags + PostParse func() + + ConfigDir string +} diff --git a/components/cli/common.go b/components/cli/common.go new file mode 100644 index 0000000000..85a02ac43b --- /dev/null +++ b/components/cli/common.go @@ -0,0 +1,20 @@ +package cli + +import ( + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/tlsconfig" +) + +// CommonFlags represents flags that are common to both the client and the daemon. +type CommonFlags struct { + FlagSet *flag.FlagSet + PostParse func() + + Debug bool + Hosts []string + LogLevel string + TLS bool + TLSVerify bool + TLSOptions *tlsconfig.Options + TrustKey string +} From 6aa852474e7d3ec852ef2a1cae377a403c239e8c Mon Sep 17 00:00:00 2001 From: Lei Jitang Date: Thu, 8 Oct 2015 08:46:21 -0400 Subject: [PATCH 016/978] Use consistent command description Signed-off-by: Lei Jitang Upstream-commit: 2734a5821ce831305eaf140e6b35d217966e7631 Component: cli --- components/cli/common.go | 58 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/components/cli/common.go b/components/cli/common.go index 85a02ac43b..d3aa391be2 100644 --- a/components/cli/common.go +++ b/components/cli/common.go @@ -18,3 +18,61 @@ type CommonFlags struct { TLSOptions *tlsconfig.Options TrustKey string } + +// Command is the struct contains command name and description +type Command struct { + Name string + Description string +} + +var dockerCommands = []Command{ + {"attach", "Attach to a running container"}, + {"build", "Build an image from a Dockerfile"}, + {"commit", "Create a new image from a container's changes"}, + {"cp", "Copy files/folders between a container and the local filesystem"}, + {"create", "Create a new container"}, + {"diff", "Inspect changes on a container's filesystem"}, + {"events", "Get real time events from the server"}, + {"exec", "Run a command in a running container"}, + {"export", "Export a container's filesystem as a tar archive"}, + {"history", "Show the history of an image"}, + {"images", "List images"}, + {"import", "Import the contents from a tarball to create a filesystem image"}, + {"info", "Display system-wide information"}, + {"inspect", "Return low-level information on a container or image"}, + {"kill", "Kill a running container"}, + {"load", "Load an image from a tar archive or STDIN"}, + {"login", "Register or log in to a Docker registry"}, + {"logout", "Log out from a Docker registry"}, + {"logs", "Fetch the logs of a container"}, + {"pause", "Pause all processes within a container"}, + {"port", "List port mappings or a specific mapping for the CONTAINER"}, + {"ps", "List containers"}, + {"pull", "Pull an image or a repository from a registry"}, + {"push", "Push an image or a repository to a registry"}, + {"rename", "Rename a container"}, + {"restart", "Restart a container"}, + {"rm", "Remove one or more containers"}, + {"rmi", "Remove one or more images"}, + {"run", "Run a command in a new container"}, + {"save", "Save an image(s) to a tar archive"}, + {"search", "Search the Docker Hub for images"}, + {"start", "Start one or more stopped containers"}, + {"stats", "Display a live stream of container(s) resource usage statistics"}, + {"stop", "Stop a running container"}, + {"tag", "Tag an image into a repository"}, + {"top", "Display the running processes of a container"}, + {"unpause", "Unpause all processes within a container"}, + {"version", "Show the Docker version information"}, + {"volume", "Manage Docker volumes"}, + {"wait", "Block until a container stops, then print its exit code"}, +} + +// DockerCommands stores all the docker command +var DockerCommands = make(map[string]Command) + +func init() { + for _, cmd := range dockerCommands { + DockerCommands[cmd.Name] = cmd + } +} From 280ba2ad3c399f74bbde0c6ea38f0a725c16ef5e Mon Sep 17 00:00:00 2001 From: Madhu Venugopal Date: Thu, 15 Oct 2015 03:10:39 -0700 Subject: [PATCH 017/978] Added `network` to docker --help and help cleanup Fixes https://github.com/docker/docker/issues/16909 Signed-off-by: Madhu Venugopal Upstream-commit: 22e3fabb45d855f2258ea81c81f242c4f6847f4d Component: cli --- components/cli/common.go | 1 + 1 file changed, 1 insertion(+) diff --git a/components/cli/common.go b/components/cli/common.go index d3aa391be2..c03d9a90e5 100644 --- a/components/cli/common.go +++ b/components/cli/common.go @@ -45,6 +45,7 @@ var dockerCommands = []Command{ {"login", "Register or log in to a Docker registry"}, {"logout", "Log out from a Docker registry"}, {"logs", "Fetch the logs of a container"}, + {"network", "Manage Docker networks"}, {"pause", "Pause all processes within a container"}, {"port", "List port mappings or a specific mapping for the CONTAINER"}, {"ps", "List containers"}, From 5d3e431b12774a0f2a72524c99422afbb10a4837 Mon Sep 17 00:00:00 2001 From: Qiang Huang Date: Mon, 28 Dec 2015 19:19:26 +0800 Subject: [PATCH 018/978] Implemet docker update command It's used for updating properties of one or more containers, we only support resource configs for now. It can be extended in the future. Signed-off-by: Qiang Huang Upstream-commit: c9a59eb6440a098e587b1b77311c86f9735e7141 Component: cli --- components/cli/common.go | 1 + 1 file changed, 1 insertion(+) diff --git a/components/cli/common.go b/components/cli/common.go index c03d9a90e5..a1f3646d84 100644 --- a/components/cli/common.go +++ b/components/cli/common.go @@ -64,6 +64,7 @@ var dockerCommands = []Command{ {"tag", "Tag an image into a repository"}, {"top", "Display the running processes of a container"}, {"unpause", "Unpause all processes within a container"}, + {"update", "Update resources of one or more containers"}, {"version", "Show the Docker version information"}, {"volume", "Manage Docker volumes"}, {"wait", "Block until a container stops, then print its exit code"}, From f8da06917ee1b9de4067583e6e87766478c97a4d Mon Sep 17 00:00:00 2001 From: David Calavera Date: Tue, 29 Dec 2015 19:27:12 -0500 Subject: [PATCH 019/978] Remove usage of pkg sockets and tlsconfig. - Use the ones provided by docker/go-connections, they are a drop in replacement. - Remove pkg/sockets from docker. - Keep pkg/tlsconfig because libnetwork still needs it and there is a circular dependency issue. Signed-off-by: David Calavera Upstream-commit: 96832973488c4860de407bb3c0b6226cf7a1c4c4 Component: cli --- components/cli/common.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/common.go b/components/cli/common.go index a1f3646d84..1ece1fb616 100644 --- a/components/cli/common.go +++ b/components/cli/common.go @@ -2,7 +2,7 @@ package cli import ( flag "github.com/docker/docker/pkg/mflag" - "github.com/docker/docker/pkg/tlsconfig" + "github.com/docker/go-connections/tlsconfig" ) // CommonFlags represents flags that are common to both the client and the daemon. From 763bb430ef41d1c38ad127520cfd258f29d4ac78 Mon Sep 17 00:00:00 2001 From: huqun Date: Fri, 12 Feb 2016 16:11:31 +0800 Subject: [PATCH 020/978] fix grammar error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit it is not very important,but I think the modification makes the coders read more conviently! Signed-off-by: huqun Upstream-commit: c73cd919b4de6e70f81654ec92a2bee3a4f3ab0a Component: cli --- components/cli/common.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/common.go b/components/cli/common.go index 1ece1fb616..880ef6c80a 100644 --- a/components/cli/common.go +++ b/components/cli/common.go @@ -19,7 +19,7 @@ type CommonFlags struct { TrustKey string } -// Command is the struct contains command name and description +// Command is the struct containing the command name and description type Command struct { Name string Description string From c1018a0d0fa82593489f146bac095babd0330832 Mon Sep 17 00:00:00 2001 From: Zhang Wei Date: Mon, 4 Jan 2016 23:58:20 +0800 Subject: [PATCH 021/978] Update RestartPolicy of container Add `--restart` flag for `update` command, so we can change restart policy for a container no matter it's running or stopped. Signed-off-by: Zhang Wei Upstream-commit: 7a30e41b8437923d79d7f298aca76f3ac045f6d5 Component: cli --- components/cli/common.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/common.go b/components/cli/common.go index 880ef6c80a..d2fa93d882 100644 --- a/components/cli/common.go +++ b/components/cli/common.go @@ -64,7 +64,7 @@ var dockerCommands = []Command{ {"tag", "Tag an image into a repository"}, {"top", "Display the running processes of a container"}, {"unpause", "Unpause all processes within a container"}, - {"update", "Update resources of one or more containers"}, + {"update", "Update configuration of one or more containers"}, {"version", "Show the Docker version information"}, {"volume", "Manage Docker volumes"}, {"wait", "Block until a container stops, then print its exit code"}, From c903000eea25092ab889544bf13c414d55fe36a6 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 1 Mar 2016 17:28:42 +0100 Subject: [PATCH 022/978] Remove some references to "register" through login These were left-overs from the now deprecated and removed functionality to registrer a new account through "docker login" Signed-off-by: Sebastiaan van Stijn Upstream-commit: 38f2513340ae07bb4834d0b0d3b2934f0a7ef812 Component: cli --- components/cli/common.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/common.go b/components/cli/common.go index d2fa93d882..df6a6ec115 100644 --- a/components/cli/common.go +++ b/components/cli/common.go @@ -42,7 +42,7 @@ var dockerCommands = []Command{ {"inspect", "Return low-level information on a container or image"}, {"kill", "Kill a running container"}, {"load", "Load an image from a tar archive or STDIN"}, - {"login", "Register or log in to a Docker registry"}, + {"login", "Log in to a Docker registry"}, {"logout", "Log out from a Docker registry"}, {"logs", "Fetch the logs of a container"}, {"network", "Manage Docker networks"}, From aebcbe8e0abd6d1e24501d4a5cade197c4986d5b Mon Sep 17 00:00:00 2001 From: Martin Mosegaard Amdisen Date: Mon, 21 Mar 2016 15:15:40 +0100 Subject: [PATCH 023/978] Fix plural typo in 'save' command help The form "Save an images" is not correct. Either "Save an image" or "Save images" work, but since the save commands accepts multiple images, I chose the latter. Fixed in all places where I could grep "Save an image(s)". Signed-off-by: Martin Mosegaard Amdisen Upstream-commit: 54e7de9b12677a607e3df5c8dee2cd69229697be Component: cli --- components/cli/common.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/common.go b/components/cli/common.go index df6a6ec115..0b2f0e6e2c 100644 --- a/components/cli/common.go +++ b/components/cli/common.go @@ -56,7 +56,7 @@ var dockerCommands = []Command{ {"rm", "Remove one or more containers"}, {"rmi", "Remove one or more images"}, {"run", "Run a command in a new container"}, - {"save", "Save an image(s) to a tar archive"}, + {"save", "Save images to a tar archive"}, {"search", "Search the Docker Hub for images"}, {"start", "Start one or more stopped containers"}, {"stats", "Display a live stream of container(s) resource usage statistics"}, From 8892876943e16fe132877e83f1375f1dbeaf04fc Mon Sep 17 00:00:00 2001 From: Martin Mosegaard Amdisen Date: Tue, 22 Mar 2016 08:16:52 +0100 Subject: [PATCH 024/978] Update 'save' command help Based on review feedback. Signed-off-by: Martin Mosegaard Amdisen Upstream-commit: bcd0ac71aea666eca668cab98c883ac0703c9ae9 Component: cli --- components/cli/common.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/common.go b/components/cli/common.go index 0b2f0e6e2c..7f6a24ba1f 100644 --- a/components/cli/common.go +++ b/components/cli/common.go @@ -56,7 +56,7 @@ var dockerCommands = []Command{ {"rm", "Remove one or more containers"}, {"rmi", "Remove one or more images"}, {"run", "Run a command in a new container"}, - {"save", "Save images to a tar archive"}, + {"save", "Save one or more images to a tar archive"}, {"search", "Search the Docker Hub for images"}, {"start", "Start one or more stopped containers"}, {"stats", "Display a live stream of container(s) resource usage statistics"}, From c5f866738c652d32ef2a7c6dde5db4042aa00da6 Mon Sep 17 00:00:00 2001 From: allencloud Date: Sat, 26 Mar 2016 22:06:45 +0800 Subject: [PATCH 025/978] fix typos Signed-off-by: allencloud Upstream-commit: 57171ee83caaef435bc20a95b9efe39b14e1dbe2 Component: cli --- components/cli/cli.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/cli.go b/components/cli/cli.go index 8e559fc3fd..88c6e68c24 100644 --- a/components/cli/cli.go +++ b/components/cli/cli.go @@ -189,7 +189,7 @@ func Subcmd(name string, synopses []string, description string, exitOnError bool return flags } -// An StatusError reports an unsuccessful exit by a command. +// StatusError reports an unsuccessful exit by a command. type StatusError struct { Status string StatusCode int From fb521be467262e808e37cc492f9f12f43ed621f1 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 19 Feb 2016 17:42:51 -0500 Subject: [PATCH 026/978] 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 Upstream-commit: 91dd0c0c6984240eced455a20d0842da72f8b9d0 Component: cli --- components/cli/client.go | 38 +++++++++++++++ components/cli/client_test.go | 23 +++++++++ components/cli/daemon.go | 43 +++++++++++++++++ components/cli/docker.go | 82 ++++++++++++++++++++++++++++++++ components/cli/docker_windows.go | 5 ++ components/cli/flags.go | 30 ++++++++++++ components/cli/flags_test.go | 13 +++++ 7 files changed, 234 insertions(+) create mode 100644 components/cli/client.go create mode 100644 components/cli/client_test.go create mode 100644 components/cli/daemon.go create mode 100644 components/cli/docker.go create mode 100644 components/cli/docker_windows.go create mode 100644 components/cli/flags.go create mode 100644 components/cli/flags_test.go diff --git a/components/cli/client.go b/components/cli/client.go new file mode 100644 index 0000000000..e8c7f889f8 --- /dev/null +++ b/components/cli/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/components/cli/client_test.go b/components/cli/client_test.go new file mode 100644 index 0000000000..5708c96cb5 --- /dev/null +++ b/components/cli/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/components/cli/daemon.go b/components/cli/daemon.go new file mode 100644 index 0000000000..48064b4cf6 --- /dev/null +++ b/components/cli/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/components/cli/docker.go b/components/cli/docker.go new file mode 100644 index 0000000000..5641f12b12 --- /dev/null +++ b/components/cli/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/components/cli/docker_windows.go b/components/cli/docker_windows.go new file mode 100644 index 0000000000..a31dffc95c --- /dev/null +++ b/components/cli/docker_windows.go @@ -0,0 +1,5 @@ +package main + +import ( + _ "github.com/docker/docker/autogen/winresources" +) diff --git a/components/cli/flags.go b/components/cli/flags.go new file mode 100644 index 0000000000..35a8108880 --- /dev/null +++ b/components/cli/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/components/cli/flags_test.go b/components/cli/flags_test.go new file mode 100644 index 0000000000..28021ba4c9 --- /dev/null +++ b/components/cli/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 c8402be4311ed6acb705855bbc8e203cf5797aa2 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 19 Feb 2016 17:42:51 -0500 Subject: [PATCH 027/978] 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 Upstream-commit: 0c4f21fee36b73c54c9f514978e8927b8564d6c8 Component: cli --- components/cli/flags/common.go | 110 +++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 components/cli/flags/common.go diff --git a/components/cli/flags/common.go b/components/cli/flags/common.go new file mode 100644 index 0000000000..d23696979b --- /dev/null +++ b/components/cli/flags/common.go @@ -0,0 +1,110 @@ +package flags + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/cli" + "github.com/docker/docker/cliconfig" + "github.com/docker/docker/opts" + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/go-connections/tlsconfig" +) + +const ( + // DefaultTrustKeyFile is the default filename for the trust key + DefaultTrustKeyFile = "key.json" + // DefaultCaFile is the default filename for the CA pem file + DefaultCaFile = "ca.pem" + // DefaultKeyFile is the default filename for the key pem file + DefaultKeyFile = "key.pem" + // DefaultCertFile is the default filename for the cert pem file + DefaultCertFile = "cert.pem" + // TLSVerifyKey is the default flag name for the tls verification option + TLSVerifyKey = "tlsverify" +) + +var ( + dockerCertPath = os.Getenv("DOCKER_CERT_PATH") + dockerTLSVerify = os.Getenv("DOCKER_TLS_VERIFY") != "" +) + +// InitCommonFlags initializes flags common to both client and daemon +func InitCommonFlags() *cli.CommonFlags { + var commonFlags = &cli.CommonFlags{FlagSet: new(flag.FlagSet)} + + if dockerCertPath == "" { + dockerCertPath = cliconfig.ConfigDir() + } + + commonFlags.PostParse = func() { postParseCommon(commonFlags) } + + cmd := commonFlags.FlagSet + + cmd.BoolVar(&commonFlags.Debug, []string{"D", "-debug"}, false, "Enable debug mode") + cmd.StringVar(&commonFlags.LogLevel, []string{"l", "-log-level"}, "info", "Set the logging level") + cmd.BoolVar(&commonFlags.TLS, []string{"-tls"}, false, "Use TLS; implied by --tlsverify") + cmd.BoolVar(&commonFlags.TLSVerify, []string{"-tlsverify"}, dockerTLSVerify, "Use TLS and verify the remote") + + // TODO use flag flag.String([]string{"i", "-identity"}, "", "Path to libtrust key file") + + var tlsOptions tlsconfig.Options + commonFlags.TLSOptions = &tlsOptions + cmd.StringVar(&tlsOptions.CAFile, []string{"-tlscacert"}, filepath.Join(dockerCertPath, DefaultCaFile), "Trust certs signed only by this CA") + cmd.StringVar(&tlsOptions.CertFile, []string{"-tlscert"}, filepath.Join(dockerCertPath, DefaultCertFile), "Path to TLS certificate file") + cmd.StringVar(&tlsOptions.KeyFile, []string{"-tlskey"}, filepath.Join(dockerCertPath, DefaultKeyFile), "Path to TLS key file") + + cmd.Var(opts.NewNamedListOptsRef("hosts", &commonFlags.Hosts, opts.ValidateHost), []string{"H", "-host"}, "Daemon socket(s) to connect to") + return commonFlags +} + +func postParseCommon(commonFlags *cli.CommonFlags) { + cmd := commonFlags.FlagSet + + SetDaemonLogLevel(commonFlags.LogLevel) + + // Regardless of whether the user sets it to true or false, if they + // specify --tlsverify at all then we need to turn on tls + // TLSVerify can be true even if not set due to DOCKER_TLS_VERIFY env var, so we need + // to check that here as well + if cmd.IsSet("-"+TLSVerifyKey) || commonFlags.TLSVerify { + commonFlags.TLS = true + } + + if !commonFlags.TLS { + commonFlags.TLSOptions = nil + } else { + tlsOptions := commonFlags.TLSOptions + tlsOptions.InsecureSkipVerify = !commonFlags.TLSVerify + + // Reset CertFile and KeyFile to empty string if the user did not specify + // the respective flags and the respective default files were not found. + if !cmd.IsSet("-tlscert") { + if _, err := os.Stat(tlsOptions.CertFile); os.IsNotExist(err) { + tlsOptions.CertFile = "" + } + } + if !cmd.IsSet("-tlskey") { + if _, err := os.Stat(tlsOptions.KeyFile); os.IsNotExist(err) { + tlsOptions.KeyFile = "" + } + } + } +} + +// SetDaemonLogLevel sets the logrus logging level +// TODO: this is a bad name, it applies to the client as well. +func SetDaemonLogLevel(logLevel string) { + if logLevel != "" { + lvl, err := logrus.ParseLevel(logLevel) + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to parse logging level: %s\n", logLevel) + os.Exit(1) + } + logrus.SetLevel(lvl) + } else { + logrus.SetLevel(logrus.InfoLevel) + } +} From 8becd69255d673ea7ba733964513269f9ece9970 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 22 Apr 2016 12:37:48 -0400 Subject: [PATCH 028/978] Cleanup from CR. Signed-off-by: Daniel Nephin Upstream-commit: ef9ad854299d36241115e58f84f13d509ed98b7d Component: cli --- components/cli/daemon.go | 32 ----------------------- components/cli/daemon_unix.go | 37 +++++++++++++++++++++++++++ components/cli/daemon_windows.go | 11 ++++++++ components/cli/daemon_windows_test.go | 18 +++++++++++++ components/cli/docker.go | 5 ---- 5 files changed, 66 insertions(+), 37 deletions(-) create mode 100644 components/cli/daemon_unix.go create mode 100644 components/cli/daemon_windows.go create mode 100644 components/cli/daemon_windows_test.go diff --git a/components/cli/daemon.go b/components/cli/daemon.go index 48064b4cf6..15dffbaefb 100644 --- a/components/cli/daemon.go +++ b/components/cli/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/components/cli/daemon_unix.go b/components/cli/daemon_unix.go new file mode 100644 index 0000000000..abe9ebfc51 --- /dev/null +++ b/components/cli/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/components/cli/daemon_windows.go b/components/cli/daemon_windows.go new file mode 100644 index 0000000000..41c0133b67 --- /dev/null +++ b/components/cli/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/components/cli/daemon_windows_test.go b/components/cli/daemon_windows_test.go new file mode 100644 index 0000000000..3da4e5d7cc --- /dev/null +++ b/components/cli/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/components/cli/docker.go b/components/cli/docker.go index 5641f12b12..838602164d 100644 --- a/components/cli/docker.go +++ b/components/cli/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 1eb8e79404ecaa26e064a996add579d60d27b1ac Mon Sep 17 00:00:00 2001 From: John Starks Date: Sat, 23 Apr 2016 15:11:08 -0700 Subject: [PATCH 029/978] 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 Upstream-commit: 5c252a7914378dbd535d2d098a3af1474bbc55fb Component: cli --- components/cli/docker_windows.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/docker_windows.go b/components/cli/docker_windows.go index a31dffc95c..de32257586 100644 --- a/components/cli/docker_windows.go +++ b/components/cli/docker_windows.go @@ -1,5 +1,5 @@ package main import ( - _ "github.com/docker/docker/autogen/winresources" + _ "github.com/docker/docker/autogen/winresources/docker" ) From 977a19e21d2be2aeff170a7a52a2e88238c5843b Mon Sep 17 00:00:00 2001 From: John Howard Date: Sat, 23 Apr 2016 18:31:57 -0700 Subject: [PATCH 030/978] Make dockerd debuggable Signed-off-by: John Howard Upstream-commit: c6919a6e797dbafdf3da088a65f9519de3740de3 Component: cli --- components/cli/client.go | 38 +++++++++++++ components/cli/client_test.go | 23 ++++++++ components/cli/daemon.go | 11 ++++ components/cli/daemon_unix.go | 37 +++++++++++++ components/cli/daemon_windows.go | 11 ++++ components/cli/daemon_windows_test.go | 18 +++++++ components/cli/docker.go | 77 +++++++++++++++++++++++++++ components/cli/docker_windows.go | 5 ++ components/cli/flags.go | 30 +++++++++++ components/cli/flags_test.go | 13 +++++ 10 files changed, 263 insertions(+) create mode 100644 components/cli/client.go create mode 100644 components/cli/client_test.go create mode 100644 components/cli/daemon.go create mode 100644 components/cli/daemon_unix.go create mode 100644 components/cli/daemon_windows.go create mode 100644 components/cli/daemon_windows_test.go create mode 100644 components/cli/docker.go create mode 100644 components/cli/docker_windows.go create mode 100644 components/cli/flags.go create mode 100644 components/cli/flags_test.go diff --git a/components/cli/client.go b/components/cli/client.go new file mode 100644 index 0000000000..e8c7f889f8 --- /dev/null +++ b/components/cli/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/components/cli/client_test.go b/components/cli/client_test.go new file mode 100644 index 0000000000..5708c96cb5 --- /dev/null +++ b/components/cli/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/components/cli/daemon.go b/components/cli/daemon.go new file mode 100644 index 0000000000..15dffbaefb --- /dev/null +++ b/components/cli/daemon.go @@ -0,0 +1,11 @@ +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/components/cli/daemon_unix.go b/components/cli/daemon_unix.go new file mode 100644 index 0000000000..abe9ebfc51 --- /dev/null +++ b/components/cli/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/components/cli/daemon_windows.go b/components/cli/daemon_windows.go new file mode 100644 index 0000000000..41c0133b67 --- /dev/null +++ b/components/cli/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/components/cli/daemon_windows_test.go b/components/cli/daemon_windows_test.go new file mode 100644 index 0000000000..3da4e5d7cc --- /dev/null +++ b/components/cli/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/components/cli/docker.go b/components/cli/docker.go new file mode 100644 index 0000000000..838602164d --- /dev/null +++ b/components/cli/docker.go @@ -0,0 +1,77 @@ +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/components/cli/docker_windows.go b/components/cli/docker_windows.go new file mode 100644 index 0000000000..889e35272d --- /dev/null +++ b/components/cli/docker_windows.go @@ -0,0 +1,5 @@ +package main + +import ( + _ "github.com/docker/docker/autogen/winresources/dockerd" +) diff --git a/components/cli/flags.go b/components/cli/flags.go new file mode 100644 index 0000000000..35a8108880 --- /dev/null +++ b/components/cli/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/components/cli/flags_test.go b/components/cli/flags_test.go new file mode 100644 index 0000000000..28021ba4c9 --- /dev/null +++ b/components/cli/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 858af44a04abf23e4f97a40eee1e3d2101c1a4f5 Mon Sep 17 00:00:00 2001 From: John Howard Date: Sat, 23 Apr 2016 18:31:57 -0700 Subject: [PATCH 031/978] Make dockerd debuggable Signed-off-by: John Howard Upstream-commit: 969302c16934ae253627ed77dda4ed05f56182ab Component: cli --- components/cli/client.go | 38 ------------- components/cli/client_test.go | 23 -------- components/cli/daemon.go | 11 ---- components/cli/daemon_unix.go | 37 ------------- components/cli/daemon_windows.go | 11 ---- components/cli/daemon_windows_test.go | 18 ------- components/cli/docker.go | 77 --------------------------- components/cli/docker_windows.go | 5 -- components/cli/flags.go | 30 ----------- components/cli/flags_test.go | 13 ----- 10 files changed, 263 deletions(-) delete mode 100644 components/cli/client.go delete mode 100644 components/cli/client_test.go delete mode 100644 components/cli/daemon.go delete mode 100644 components/cli/daemon_unix.go delete mode 100644 components/cli/daemon_windows.go delete mode 100644 components/cli/daemon_windows_test.go delete mode 100644 components/cli/docker.go delete mode 100644 components/cli/docker_windows.go delete mode 100644 components/cli/flags.go delete mode 100644 components/cli/flags_test.go diff --git a/components/cli/client.go b/components/cli/client.go deleted file mode 100644 index e8c7f889f8..0000000000 --- a/components/cli/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/components/cli/client_test.go b/components/cli/client_test.go deleted file mode 100644 index 5708c96cb5..0000000000 --- a/components/cli/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/components/cli/daemon.go b/components/cli/daemon.go deleted file mode 100644 index 15dffbaefb..0000000000 --- a/components/cli/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/components/cli/daemon_unix.go b/components/cli/daemon_unix.go deleted file mode 100644 index abe9ebfc51..0000000000 --- a/components/cli/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/components/cli/daemon_windows.go b/components/cli/daemon_windows.go deleted file mode 100644 index 41c0133b67..0000000000 --- a/components/cli/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/components/cli/daemon_windows_test.go b/components/cli/daemon_windows_test.go deleted file mode 100644 index 3da4e5d7cc..0000000000 --- a/components/cli/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/components/cli/docker.go b/components/cli/docker.go deleted file mode 100644 index 838602164d..0000000000 --- a/components/cli/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/components/cli/docker_windows.go b/components/cli/docker_windows.go deleted file mode 100644 index de32257586..0000000000 --- a/components/cli/docker_windows.go +++ /dev/null @@ -1,5 +0,0 @@ -package main - -import ( - _ "github.com/docker/docker/autogen/winresources/docker" -) diff --git a/components/cli/flags.go b/components/cli/flags.go deleted file mode 100644 index 35a8108880..0000000000 --- a/components/cli/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/components/cli/flags_test.go b/components/cli/flags_test.go deleted file mode 100644 index 28021ba4c9..0000000000 --- a/components/cli/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 73fd4d25b9a673a0812b84ece533f80c5f710e0b Mon Sep 17 00:00:00 2001 From: John Starks Date: Fri, 22 Apr 2016 17:16:14 -0700 Subject: [PATCH 032/978] Windows: Support running dockerd as a service This adds support for Windows dockerd to run as a Windows service, managed by the service control manager. The log is written to the Windows event log (and can be viewed in the event viewer or in PowerShell). If there is a Go panic, the stack is written to a file panic.log in the Docker root. Signed-off-by: John Starks Upstream-commit: 421e366d8d574bec16eee862d984d299d3fb9ac7 Component: cli --- components/cli/docker_windows.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/docker_windows.go b/components/cli/docker_windows.go index 889e35272d..de32257586 100644 --- a/components/cli/docker_windows.go +++ b/components/cli/docker_windows.go @@ -1,5 +1,5 @@ package main import ( - _ "github.com/docker/docker/autogen/winresources/dockerd" + _ "github.com/docker/docker/autogen/winresources/docker" ) From 9e49ad8267a91b4e95564cd5680e74b3c8148b25 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 21 Apr 2016 17:51:28 -0400 Subject: [PATCH 033/978] Cleanup the structure of the cli package. Move all flags into cli/flags Move usage help into cli/usage.go Signed-off-by: Daniel Nephin Upstream-commit: 315e242b9c34b8e6bd258d5b5a76952dc16a6d84 Component: cli --- components/cli/client.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/components/cli/client.go b/components/cli/client.go index e8c7f889f8..4d98a33cc4 100644 --- a/components/cli/client.go +++ b/components/cli/client.go @@ -3,7 +3,6 @@ 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" @@ -12,7 +11,7 @@ import ( var ( commonFlags = cliflags.InitCommonFlags() - clientFlags = &cli.ClientFlags{FlagSet: new(flag.FlagSet), Common: commonFlags} + clientFlags = &cliflags.ClientFlags{FlagSet: new(flag.FlagSet), Common: commonFlags} ) func init() { From c15182b1a023aa47fc17f0dacd45403955cc1c16 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 21 Apr 2016 17:51:28 -0400 Subject: [PATCH 034/978] Cleanup the structure of the cli package. Move all flags into cli/flags Move usage help into cli/usage.go Signed-off-by: Daniel Nephin Upstream-commit: a5c08fdbf0a32a39f047812025d3219ee01c8015 Component: cli --- components/cli/{ => flags}/client.go | 2 +- components/cli/flags/common.go | 21 +++++++++++++++++---- components/cli/{common.go => usage.go} | 19 ------------------- 3 files changed, 18 insertions(+), 24 deletions(-) rename components/cli/{ => flags}/client.go (94%) rename components/cli/{common.go => usage.go} (86%) diff --git a/components/cli/client.go b/components/cli/flags/client.go similarity index 94% rename from components/cli/client.go rename to components/cli/flags/client.go index 6a82eb52a5..cc7309db4b 100644 --- a/components/cli/client.go +++ b/components/cli/flags/client.go @@ -1,4 +1,4 @@ -package cli +package flags import flag "github.com/docker/docker/pkg/mflag" diff --git a/components/cli/flags/common.go b/components/cli/flags/common.go index d23696979b..4726b04f2a 100644 --- a/components/cli/flags/common.go +++ b/components/cli/flags/common.go @@ -6,7 +6,6 @@ import ( "path/filepath" "github.com/Sirupsen/logrus" - "github.com/docker/docker/cli" "github.com/docker/docker/cliconfig" "github.com/docker/docker/opts" flag "github.com/docker/docker/pkg/mflag" @@ -31,9 +30,23 @@ var ( dockerTLSVerify = os.Getenv("DOCKER_TLS_VERIFY") != "" ) +// CommonFlags are flags common to both the client and the daemon. +type CommonFlags struct { + FlagSet *flag.FlagSet + PostParse func() + + Debug bool + Hosts []string + LogLevel string + TLS bool + TLSVerify bool + TLSOptions *tlsconfig.Options + TrustKey string +} + // InitCommonFlags initializes flags common to both client and daemon -func InitCommonFlags() *cli.CommonFlags { - var commonFlags = &cli.CommonFlags{FlagSet: new(flag.FlagSet)} +func InitCommonFlags() *CommonFlags { + var commonFlags = &CommonFlags{FlagSet: new(flag.FlagSet)} if dockerCertPath == "" { dockerCertPath = cliconfig.ConfigDir() @@ -60,7 +73,7 @@ func InitCommonFlags() *cli.CommonFlags { return commonFlags } -func postParseCommon(commonFlags *cli.CommonFlags) { +func postParseCommon(commonFlags *CommonFlags) { cmd := commonFlags.FlagSet SetDaemonLogLevel(commonFlags.LogLevel) diff --git a/components/cli/common.go b/components/cli/usage.go similarity index 86% rename from components/cli/common.go rename to components/cli/usage.go index 7f6a24ba1f..4b0eb0e0c3 100644 --- a/components/cli/common.go +++ b/components/cli/usage.go @@ -1,24 +1,5 @@ package cli -import ( - flag "github.com/docker/docker/pkg/mflag" - "github.com/docker/go-connections/tlsconfig" -) - -// CommonFlags represents flags that are common to both the client and the daemon. -type CommonFlags struct { - FlagSet *flag.FlagSet - PostParse func() - - Debug bool - Hosts []string - LogLevel string - TLS bool - TLSVerify bool - TLSOptions *tlsconfig.Options - TrustKey string -} - // Command is the struct containing the command name and description type Command struct { Name string From 9d807c44244f9f63808e7dcf28f50fdcefd1f262 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 25 Apr 2016 12:05:42 -0400 Subject: [PATCH 035/978] Consolidate the files in client/ Signed-off-by: Daniel Nephin Upstream-commit: ec6cc96fa177a91b0516c11ab3c1dfc5c42ac8db Component: cli --- components/cli/client.go | 37 ------------------- components/cli/docker.go | 34 +++++++++++++++++ .../cli/{client_test.go => docker_test.go} | 0 components/cli/{flags.go => usage.go} | 16 ++------ .../cli/{flags_test.go => usage_test.go} | 4 +- 5 files changed, 41 insertions(+), 50 deletions(-) delete mode 100644 components/cli/client.go rename components/cli/{client_test.go => docker_test.go} (100%) rename components/cli/{flags.go => usage.go} (51%) rename components/cli/{flags_test.go => usage_test.go} (70%) diff --git a/components/cli/client.go b/components/cli/client.go deleted file mode 100644 index 4d98a33cc4..0000000000 --- a/components/cli/client.go +++ /dev/null @@ -1,37 +0,0 @@ -package main - -import ( - "path/filepath" - - 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 = &cliflags.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/components/cli/docker.go b/components/cli/docker.go index 838602164d..8397124932 100644 --- a/components/cli/docker.go +++ b/components/cli/docker.go @@ -3,16 +3,26 @@ package main import ( "fmt" "os" + "path/filepath" "github.com/Sirupsen/logrus" "github.com/docker/docker/api/client" "github.com/docker/docker/cli" + cliflags "github.com/docker/docker/cli/flags" + "github.com/docker/docker/cliconfig" "github.com/docker/docker/dockerversion" flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/pkg/term" "github.com/docker/docker/utils" ) +var ( + commonFlags = cliflags.InitCommonFlags() + clientFlags = initClientFlags(commonFlags) + flHelp = flag.Bool([]string{"h", "-help"}, false, "Print usage") + flVersion = flag.Bool([]string{"v", "-version"}, false, "Print version information and quit") +) + func main() { // Set terminal emulation based on platform as required. stdin, stdout, stderr := term.StdStreams() @@ -30,6 +40,7 @@ func main() { help := "\nCommands:\n" + dockerCommands := sortCommands(cli.DockerCommandUsage) for _, cmd := range dockerCommands { help += fmt.Sprintf(" %-10.10s%s\n", cmd.Name, cmd.Description) } @@ -75,3 +86,26 @@ func showVersion() { fmt.Printf("Docker version %s, build %s\n", dockerversion.Version, dockerversion.GitCommit) } } + +func initClientFlags(commonFlags *cliflags.CommonFlags) *cliflags.ClientFlags { + clientFlags := &cliflags.ClientFlags{FlagSet: new(flag.FlagSet), Common: commonFlags} + 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() + } + } + return clientFlags +} diff --git a/components/cli/client_test.go b/components/cli/docker_test.go similarity index 100% rename from components/cli/client_test.go rename to components/cli/docker_test.go diff --git a/components/cli/flags.go b/components/cli/usage.go similarity index 51% rename from components/cli/flags.go rename to components/cli/usage.go index 35a8108880..792d178073 100644 --- a/components/cli/flags.go +++ b/components/cli/usage.go @@ -4,12 +4,6 @@ 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 @@ -18,13 +12,11 @@ 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) - } +func sortCommands(commands []cli.Command) []cli.Command { + dockerCommands := make([]cli.Command, len(commands)) + copy(dockerCommands, commands) sort.Sort(byName(dockerCommands)) + return dockerCommands } diff --git a/components/cli/flags_test.go b/components/cli/usage_test.go similarity index 70% rename from components/cli/flags_test.go rename to components/cli/usage_test.go index 28021ba4c9..0453265db8 100644 --- a/components/cli/flags_test.go +++ b/components/cli/usage_test.go @@ -3,11 +3,13 @@ package main import ( "sort" "testing" + + "github.com/docker/docker/cli" ) // Tests if the subcommands of docker are sorted func TestDockerSubcommandsAreSorted(t *testing.T) { - if !sort.IsSorted(byName(dockerCommands)) { + if !sort.IsSorted(byName(cli.DockerCommandUsage)) { t.Fatal("Docker subcommands are not in sorted order") } } From 63c0c343f92acffed8c71ca58925fd170ad9b25d Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 25 Apr 2016 12:05:42 -0400 Subject: [PATCH 036/978] Consolidate the files in client/ Signed-off-by: Daniel Nephin Upstream-commit: 2bc929b019f5f1c2149c886e487c78a31123bf77 Component: cli --- components/cli/usage.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/components/cli/usage.go b/components/cli/usage.go index 4b0eb0e0c3..1ef6a35dec 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -6,7 +6,8 @@ type Command struct { Description string } -var dockerCommands = []Command{ +// DockerCommandUsage lists the top level docker commands and their short usage +var DockerCommandUsage = []Command{ {"attach", "Attach to a running container"}, {"build", "Build an image from a Dockerfile"}, {"commit", "Create a new image from a container's changes"}, @@ -55,7 +56,7 @@ var dockerCommands = []Command{ var DockerCommands = make(map[string]Command) func init() { - for _, cmd := range dockerCommands { + for _, cmd := range DockerCommandUsage { DockerCommands[cmd.Name] = cmd } } From a190e4c730445cc69addb49c61c97f4f48f84f36 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Fri, 29 Apr 2016 11:16:34 -0400 Subject: [PATCH 037/978] Remove reflection on CLI init before: ``` $ time docker --help real 0m0.177s user 0m0.000s sys 0m0.040s ``` after: ``` $ time docker --help real 0m0.010s user 0m0.000s sys 0m0.000s ``` Signed-off-by: Brian Goff Upstream-commit: eb35552fb363ccc70c0473a29e40161ead848e37 Component: cli --- components/cli/daemon.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/components/cli/daemon.go b/components/cli/daemon.go index 15dffbaefb..8fe3484761 100644 --- a/components/cli/daemon.go +++ b/components/cli/daemon.go @@ -9,3 +9,10 @@ type DaemonProxy struct{} func NewDaemonProxy() DaemonProxy { return DaemonProxy{} } + +// Command returns a cli command handler if one exists +func (p DaemonProxy) Command(name string) func(...string) error { + return map[string]func(...string) error{ + "daemon": p.CmdDaemon, + }[name] +} From 2007fb140dff0e6ca8e8436f700febaa5966277e Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Fri, 29 Apr 2016 11:16:34 -0400 Subject: [PATCH 038/978] Remove reflection on CLI init before: ``` $ time docker --help real 0m0.177s user 0m0.000s sys 0m0.040s ``` after: ``` $ time docker --help real 0m0.010s user 0m0.000s sys 0m0.000s ``` Signed-off-by: Brian Goff Upstream-commit: 89d78abcdc393e8008636c4fad7c4fad45e1a759 Component: cli --- components/cli/cli.go | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/components/cli/cli.go b/components/cli/cli.go index 88c6e68c24..12649df6da 100644 --- a/components/cli/cli.go +++ b/components/cli/cli.go @@ -5,7 +5,6 @@ import ( "fmt" "io" "os" - "reflect" "strings" flag "github.com/docker/docker/pkg/mflag" @@ -21,7 +20,9 @@ type Cli struct { // Handler holds the different commands Cli will call // It should have methods with names starting with `Cmd` like: // func (h myHandler) CmdFoo(args ...string) error -type Handler interface{} +type Handler interface { + Command(name string) func(...string) error +} // Initializer can be optionally implemented by a Handler to // initialize before each call to one of its commands. @@ -50,22 +51,13 @@ func (cli *Cli) command(args ...string) (func(...string) error, error) { if c == nil { continue } - camelArgs := make([]string, len(args)) - for i, s := range args { - if len(s) == 0 { - return nil, errors.New("empty command") - } - camelArgs[i] = strings.ToUpper(s[:1]) + strings.ToLower(s[1:]) - } - methodName := "Cmd" + strings.Join(camelArgs, "") - method := reflect.ValueOf(c).MethodByName(methodName) - if method.IsValid() { - if c, ok := c.(Initializer); ok { - if err := c.Initialize(); err != nil { + if cmd := c.Command(strings.Join(args, " ")); cmd != nil { + if ci, ok := c.(Initializer); ok { + if err := ci.Initialize(); err != nil { return nil, initErr{err} } } - return method.Interface().(func(...string) error), nil + return cmd, nil } } return nil, errors.New("command not found") @@ -103,6 +95,13 @@ func (cli *Cli) noSuchCommand(command string) { os.Exit(1) } +// Command returns a command handler, or nil if the command does not exist +func (cli *Cli) Command(name string) func(...string) error { + return map[string]func(...string) error{ + "help": cli.CmdHelp, + }[name] +} + // CmdHelp displays information on a Docker command. // // If more than one command is specified, information is only shown for the first command. From 55cad0247705a425bd4c3e8a950a6b09f5f583b0 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 27 Apr 2016 12:11:32 -0400 Subject: [PATCH 039/978] When exec'ing dockerd, look for it in the same directory as the docker binary first, before checking path. Signed-off-by: Daniel Nephin Upstream-commit: 625263e2c7ad70ff9ce55e1161e866db83c66b37 Component: cli --- components/cli/daemon_unix.go | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/components/cli/daemon_unix.go b/components/cli/daemon_unix.go index abe9ebfc51..896782b36c 100644 --- a/components/cli/daemon_unix.go +++ b/components/cli/daemon_unix.go @@ -5,27 +5,40 @@ package main import ( "os" "os/exec" + "path/filepath" "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) + binaryPath, err := findDaemonBinary() if err != nil { return err } return syscall.Exec( - binaryAbsPath, + binaryPath, append([]string{daemonBinary}, args...), os.Environ()) } +// findDaemonBinary looks for the path to the dockerd binary starting with +// the directory of the current executable (if one exists) and followed by $PATH +func findDaemonBinary() (string, error) { + execDirname := filepath.Dir(os.Args[0]) + if execDirname != "" { + binaryPath := filepath.Join(execDirname, daemonBinary) + if _, err := os.Stat(binaryPath); err == nil { + return binaryPath, nil + } + } + + return exec.LookPath(daemonBinary) +} + // stripDaemonArg removes the `daemon` argument from the list func stripDaemonArg(args []string) []string { for i, arg := range args { From 3773be609d999ce7c820ef04a33b1ff03c397ba3 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 27 Apr 2016 13:08:20 -0400 Subject: [PATCH 040/978] Raise a more relevant error when dockerd is not available on the platform. Signed-off-by: Daniel Nephin Upstream-commit: 765ab2b6921b1a6f21504d0c4ef4eed046a76d89 Component: cli --- components/cli/{daemon_windows.go => daemon_none.go} | 7 ++++++- .../cli/{daemon_windows_test.go => daemon_none_test.go} | 4 +++- components/cli/daemon_unix.go | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) rename components/cli/{daemon_windows.go => daemon_none.go} (55%) rename components/cli/{daemon_windows_test.go => daemon_none_test.go} (80%) diff --git a/components/cli/daemon_windows.go b/components/cli/daemon_none.go similarity index 55% rename from components/cli/daemon_windows.go rename to components/cli/daemon_none.go index 41c0133b67..d66bf1a546 100644 --- a/components/cli/daemon_windows.go +++ b/components/cli/daemon_none.go @@ -1,11 +1,16 @@ +// +build !daemon + package main import ( "fmt" + "runtime" + "strings" ) // 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") + "`docker daemon` is not supported on %s. Please run `dockerd` directly", + strings.Title(runtime.GOOS)) } diff --git a/components/cli/daemon_windows_test.go b/components/cli/daemon_none_test.go similarity index 80% rename from components/cli/daemon_windows_test.go rename to components/cli/daemon_none_test.go index 3da4e5d7cc..d75453bcc5 100644 --- a/components/cli/daemon_windows_test.go +++ b/components/cli/daemon_none_test.go @@ -1,3 +1,5 @@ +// +build !daemon + package main import ( @@ -9,7 +11,7 @@ func TestCmdDaemon(t *testing.T) { proxy := NewDaemonProxy() err := proxy.CmdDaemon("--help") if err == nil { - t.Fatal("Expected CmdDaemon to fail in Windows.") + t.Fatal("Expected CmdDaemon to fail on Windows.") } if !strings.Contains(err.Error(), "Please run `dockerd`") { diff --git a/components/cli/daemon_unix.go b/components/cli/daemon_unix.go index 896782b36c..7a27518636 100644 --- a/components/cli/daemon_unix.go +++ b/components/cli/daemon_unix.go @@ -1,4 +1,4 @@ -// +build !windows +// +build daemon package main From c70421e0ad5b07982ef9abe9aa3d4ee941b56126 Mon Sep 17 00:00:00 2001 From: muge Date: Mon, 16 May 2016 09:38:04 +0800 Subject: [PATCH 041/978] cli: remove unnecessary initErr type Signed-off-by: ZhangHang Signed-off-by: Alexander Morozov Upstream-commit: 79b8543b546aa40f6f1017eaf4b24d603a777c5c Component: cli --- components/cli/cli.go | 55 ++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/components/cli/cli.go b/components/cli/cli.go index 12649df6da..f6d48d6fac 100644 --- a/components/cli/cli.go +++ b/components/cli/cli.go @@ -39,12 +39,7 @@ func New(handlers ...Handler) *Cli { return cli } -// initErr is an error returned upon initialization of a handler implementing Initializer. -type initErr struct{ error } - -func (err initErr) Error() string { - return err.Error() -} +var errCommandNotFound = errors.New("command not found") func (cli *Cli) command(args ...string) (func(...string) error, error) { for _, c := range cli.handlers { @@ -54,35 +49,36 @@ func (cli *Cli) command(args ...string) (func(...string) error, error) { if cmd := c.Command(strings.Join(args, " ")); cmd != nil { if ci, ok := c.(Initializer); ok { if err := ci.Initialize(); err != nil { - return nil, initErr{err} + return nil, err } } return cmd, nil } } - return nil, errors.New("command not found") + return nil, errCommandNotFound } // Run executes the specified command. func (cli *Cli) Run(args ...string) error { if len(args) > 1 { command, err := cli.command(args[:2]...) - switch err := err.(type) { - case nil: + if err == nil { return command(args[2:]...) - case initErr: - return err.error + } + if err != errCommandNotFound { + return err } } if len(args) > 0 { command, err := cli.command(args[0]) - switch err := err.(type) { - case nil: - return command(args[1:]...) - case initErr: - return err.error + if err != nil { + if err == errCommandNotFound { + cli.noSuchCommand(args[0]) + return nil + } + return err } - cli.noSuchCommand(args[0]) + return command(args[1:]...) } return cli.CmdHelp() } @@ -110,24 +106,25 @@ func (cli *Cli) Command(name string) func(...string) error { func (cli *Cli) CmdHelp(args ...string) error { if len(args) > 1 { command, err := cli.command(args[:2]...) - switch err := err.(type) { - case nil: + if err == nil { command("--help") return nil - case initErr: - return err.error + } + if err != errCommandNotFound { + return err } } if len(args) > 0 { command, err := cli.command(args[0]) - switch err := err.(type) { - case nil: - command("--help") - return nil - case initErr: - return err.error + if err != nil { + if err == errCommandNotFound { + cli.noSuchCommand(args[0]) + return nil + } + return err } - cli.noSuchCommand(args[0]) + command("--help") + return nil } if cli.Usage == nil { From 4b0173813a015922a583435c407a6143b749c7d6 Mon Sep 17 00:00:00 2001 From: John Starks Date: Fri, 20 May 2016 10:38:31 -0700 Subject: [PATCH 042/978] Windows: work around Go 1.6.2/Nano Server TP5 issue This works around golang/go#15286 by explicitly loading shell32.dll at load time, ensuring that syscall can load it dynamically during process startup. Signed-off-by: John Starks Signed-off-by: Antonio Murdaca Upstream-commit: 9b1a322d9ef85e0c0a4613d11d1ba93dac35569b Component: cli --- components/cli/docker_windows.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/components/cli/docker_windows.go b/components/cli/docker_windows.go index de32257586..9bc507e20c 100644 --- a/components/cli/docker_windows.go +++ b/components/cli/docker_windows.go @@ -1,5 +1,18 @@ package main import ( + "sync/atomic" + _ "github.com/docker/docker/autogen/winresources/docker" ) + +//go:cgo_import_dynamic main.dummy CommandLineToArgvW%2 "shell32.dll" + +var dummy uintptr + +func init() { + // Ensure that this import is not removed by the linker. This is used to + // ensure that shell32.dll is loaded by the system loader, preventing + // go#15286 from triggering on Nano Server TP5. + atomic.LoadUintptr(&dummy) +} From 5848cf4f34a078c2fc8468c4be0f5cbace7d876c Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 19 Apr 2016 12:59:48 -0400 Subject: [PATCH 043/978] Migrate volume commands to cobra. Signed-off-by: Daniel Nephin Upstream-commit: 4786ccd05c2785b2cc0ef4d7e3c33cbaeff8ef73 Component: cli --- components/cli/cobraadaptor/adaptor.go | 83 ++++++++++++++++++++++++++ components/cli/required.go | 23 +++++++ 2 files changed, 106 insertions(+) create mode 100644 components/cli/cobraadaptor/adaptor.go create mode 100644 components/cli/required.go diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go new file mode 100644 index 0000000000..07ff8124b0 --- /dev/null +++ b/components/cli/cobraadaptor/adaptor.go @@ -0,0 +1,83 @@ +package cobraadaptor + +import ( + "github.com/docker/docker/api/client" + "github.com/docker/docker/api/client/volume" + "github.com/docker/docker/cli" + cliflags "github.com/docker/docker/cli/flags" + "github.com/docker/docker/pkg/term" + "github.com/spf13/cobra" +) + +// CobraAdaptor is an adaptor for supporting spf13/cobra commands in the +// docker/cli framework +type CobraAdaptor struct { + rootCmd *cobra.Command + dockerCli *client.DockerCli +} + +// NewCobraAdaptor returns a new handler +func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { + var rootCmd = &cobra.Command{ + Use: "docker", + } + rootCmd.SetUsageTemplate(usageTemplate) + + stdin, stdout, stderr := term.StdStreams() + dockerCli := client.NewDockerCli(stdin, stdout, stderr, clientFlags) + + rootCmd.AddCommand( + volume.NewVolumeCommand(dockerCli), + ) + return CobraAdaptor{ + rootCmd: rootCmd, + dockerCli: dockerCli, + } +} + +// Usage returns the list of commands and their short usage string for +// all top level cobra commands. +func (c CobraAdaptor) Usage() []cli.Command { + cmds := []cli.Command{} + for _, cmd := range c.rootCmd.Commands() { + cmds = append(cmds, cli.Command{Name: cmd.Use, Description: cmd.Short}) + } + return cmds +} + +func (c CobraAdaptor) run(cmd string, args []string) error { + c.dockerCli.Initialize() + // Prepend the command name to support normal cobra command delegation + c.rootCmd.SetArgs(append([]string{cmd}, args...)) + return c.rootCmd.Execute() +} + +// Command returns a cli command handler if one exists +func (c CobraAdaptor) Command(name string) func(...string) error { + for _, cmd := range c.rootCmd.Commands() { + if cmd.Name() == name { + return func(args ...string) error { + return c.run(name, args) + } + } + } + return nil +} + +var usageTemplate = `Usage: {{if .Runnable}}{{if .HasFlags}}{{appendIfNotPresent .UseLine "[OPTIONS]"}}{{else}}{{.UseLine}}{{end}}{{end}}{{if .HasSubCommands}}{{ .CommandPath}} COMMAND {{end}}{{if gt .Aliases 0}} + +Aliases: + {{.NameAndAliases}} +{{end}}{{if .HasExample}} + +Examples: +{{ .Example }}{{end}}{{ if .HasLocalFlags}} + +Options: +{{.LocalFlags.FlagUsages | trimRightSpace}}{{end}}{{ if .HasAvailableSubCommands}} + +Commands:{{range .Commands}}{{if .IsAvailableCommand}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{ if .HasSubCommands }} + +Run '{{.CommandPath}} COMMAND --help' for more information on a command.{{end}} +` diff --git a/components/cli/required.go b/components/cli/required.go new file mode 100644 index 0000000000..6b83fadde1 --- /dev/null +++ b/components/cli/required.go @@ -0,0 +1,23 @@ +package cli + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// MinRequiredArgs checks if the minimum number of args exists, and returns an +// error if they do not. +func MinRequiredArgs(args []string, min int, cmd *cobra.Command) error { + if len(args) >= min { + return nil + } + + return fmt.Errorf( + "\"%s\" requires at least %d argument(s).\n\nUsage: %s\n\n%s", + cmd.CommandPath(), + min, + cmd.UseLine(), + cmd.Short, + ) +} From a0e9eab4646c5f7392b23ae25dace091d027d8fb Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 16 May 2016 17:20:29 -0400 Subject: [PATCH 044/978] Update usage and help to (almost) match the existing docker behaviour Signed-off-by: Daniel Nephin Upstream-commit: 13cea4e58d0a0ab729849e5cd11ed48a30130341 Component: cli --- components/cli/cobraadaptor/adaptor.go | 28 +++++++++++++++++--------- components/cli/required.go | 14 +++++++++++++ components/cli/usage.go | 1 - 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index 07ff8124b0..35b77b47e1 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -18,17 +18,21 @@ type CobraAdaptor struct { // NewCobraAdaptor returns a new handler func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { - var rootCmd = &cobra.Command{ - Use: "docker", - } - rootCmd.SetUsageTemplate(usageTemplate) - stdin, stdout, stderr := term.StdStreams() dockerCli := client.NewDockerCli(stdin, stdout, stderr, clientFlags) + var rootCmd = &cobra.Command{ + Use: "docker", + SilenceUsage: true, + SilenceErrors: true, + } + rootCmd.SetUsageTemplate(usageTemplate) + rootCmd.SetHelpTemplate(helpTemplate) + rootCmd.SetOutput(stdout) rootCmd.AddCommand( volume.NewVolumeCommand(dockerCli), ) + return CobraAdaptor{ rootCmd: rootCmd, dockerCli: dockerCli, @@ -64,20 +68,24 @@ func (c CobraAdaptor) Command(name string) func(...string) error { return nil } -var usageTemplate = `Usage: {{if .Runnable}}{{if .HasFlags}}{{appendIfNotPresent .UseLine "[OPTIONS]"}}{{else}}{{.UseLine}}{{end}}{{end}}{{if .HasSubCommands}}{{ .CommandPath}} COMMAND {{end}}{{if gt .Aliases 0}} +var usageTemplate = `Usage: {{if not .HasSubCommands}}{{if .HasLocalFlags}}{{appendIfNotPresent .UseLine "[OPTIONS]"}}{{else}}{{.UseLine}}{{end}}{{end}}{{if .HasSubCommands}}{{ .CommandPath}} COMMAND{{end}} + +{{with or .Long .Short }}{{. | trim}}{{end}}{{if gt .Aliases 0}} Aliases: - {{.NameAndAliases}} -{{end}}{{if .HasExample}} + {{.NameAndAliases}}{{end}}{{if .HasExample}} Examples: -{{ .Example }}{{end}}{{ if .HasLocalFlags}} +{{ .Example }}{{end}}{{if .HasFlags}} Options: -{{.LocalFlags.FlagUsages | trimRightSpace}}{{end}}{{ if .HasAvailableSubCommands}} +{{.Flags.FlagUsages | trimRightSpace}}{{end}}{{ if .HasAvailableSubCommands}} Commands:{{range .Commands}}{{if .IsAvailableCommand}} {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{ if .HasSubCommands }} Run '{{.CommandPath}} COMMAND --help' for more information on a command.{{end}} ` + +var helpTemplate = ` +{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}` diff --git a/components/cli/required.go b/components/cli/required.go index 6b83fadde1..db1a98bd52 100644 --- a/components/cli/required.go +++ b/components/cli/required.go @@ -21,3 +21,17 @@ func MinRequiredArgs(args []string, min int, cmd *cobra.Command) error { cmd.Short, ) } + +// AcceptsNoArgs returns an error message if there are args +func AcceptsNoArgs(args []string, cmd *cobra.Command) error { + if len(args) == 0 { + return nil + } + + return fmt.Errorf( + "\"%s\" accepts no argument(s).\n\nUsage: %s\n\n%s", + cmd.CommandPath(), + cmd.UseLine(), + cmd.Short, + ) +} diff --git a/components/cli/usage.go b/components/cli/usage.go index 1ef6a35dec..9ddc17326a 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -48,7 +48,6 @@ var DockerCommandUsage = []Command{ {"unpause", "Unpause all processes within a container"}, {"update", "Update configuration of one or more containers"}, {"version", "Show the Docker version information"}, - {"volume", "Manage Docker volumes"}, {"wait", "Block until a container stops, then print its exit code"}, } From 64201068c60a08141f6404b2aea70f5408ee1587 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 19 Apr 2016 12:59:48 -0400 Subject: [PATCH 045/978] Migrate volume commands to cobra. Signed-off-by: Daniel Nephin Upstream-commit: ad83c422f25498afda0c9a45741dff76e5ae8a3d Component: cli --- components/cli/docker.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/components/cli/docker.go b/components/cli/docker.go index 8397124932..45de2e3fca 100644 --- a/components/cli/docker.go +++ b/components/cli/docker.go @@ -8,6 +8,7 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/docker/api/client" "github.com/docker/docker/cli" + "github.com/docker/docker/cli/cobraadaptor" cliflags "github.com/docker/docker/cli/flags" "github.com/docker/docker/cliconfig" "github.com/docker/docker/dockerversion" @@ -31,6 +32,8 @@ func main() { flag.Merge(flag.CommandLine, clientFlags.FlagSet, commonFlags.FlagSet) + cobraAdaptor := cobraadaptor.NewCobraAdaptor(clientFlags) + 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") @@ -40,8 +43,8 @@ func main() { help := "\nCommands:\n" - dockerCommands := sortCommands(cli.DockerCommandUsage) - for _, cmd := range dockerCommands { + dockerCommands := append(cli.DockerCommandUsage, cobraAdaptor.Usage()...) + for _, cmd := range sortCommands(dockerCommands) { help += fmt.Sprintf(" %-10.10s%s\n", cmd.Name, cmd.Description) } @@ -65,7 +68,7 @@ func main() { clientCli := client.NewDockerCli(stdin, stdout, stderr, clientFlags) - c := cli.New(clientCli, NewDaemonProxy()) + c := cli.New(clientCli, NewDaemonProxy(), cobraAdaptor) if err := c.Run(flag.Args()...); err != nil { if sterr, ok := err.(cli.StatusError); ok { if sterr.Status != "" { From 83e2fbd2b2d692e6ee0c4b1b62dee1854c241e8f Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 25 May 2016 14:57:18 -0700 Subject: [PATCH 046/978] Support usage messages on bad flags. Signed-off-by: Daniel Nephin Upstream-commit: 11ede593799f2c5ff6be8fca986c6122d274d7d8 Component: cli --- components/cli/cobraadaptor/adaptor.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index 35b77b47e1..cfa1b7f04c 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -1,6 +1,8 @@ package cobraadaptor import ( + "fmt" + "github.com/docker/docker/api/client" "github.com/docker/docker/api/client/volume" "github.com/docker/docker/cli" @@ -28,6 +30,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { } rootCmd.SetUsageTemplate(usageTemplate) rootCmd.SetHelpTemplate(helpTemplate) + rootCmd.SetFlagErrorFunc(flagErrorFunc) rootCmd.SetOutput(stdout) rootCmd.AddCommand( volume.NewVolumeCommand(dockerCli), @@ -68,6 +71,20 @@ func (c CobraAdaptor) Command(name string) func(...string) error { return nil } +// flagErrorFunc prints an error messages which matches the format of the +// docker/docker/cli error messages +func flagErrorFunc(cmd *cobra.Command, err error) error { + if err == nil { + return err + } + + usage := "" + if cmd.HasSubCommands() { + usage = "\n\n" + cmd.UsageString() + } + return fmt.Errorf("%s\nSee '%s --help'.%s", err, cmd.CommandPath(), usage) +} + var usageTemplate = `Usage: {{if not .HasSubCommands}}{{if .HasLocalFlags}}{{appendIfNotPresent .UseLine "[OPTIONS]"}}{{else}}{{.UseLine}}{{end}}{{end}}{{if .HasSubCommands}}{{ .CommandPath}} COMMAND{{end}} {{with or .Long .Short }}{{. | trim}}{{end}}{{if gt .Aliases 0}} From 625794a55543d68724fa8383e3dc9ab9b82c5e2d Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 26 May 2016 14:57:31 -0700 Subject: [PATCH 047/978] Use Args in cobra.Command to validate args. Also re-use context. Signed-off-by: Daniel Nephin Upstream-commit: 25892d27be5dd10dac694fd4d5ae317882c32fc1 Component: cli --- components/cli/required.go | 39 +++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/components/cli/required.go b/components/cli/required.go index db1a98bd52..94710374e6 100644 --- a/components/cli/required.go +++ b/components/cli/required.go @@ -2,30 +2,19 @@ package cli import ( "fmt" + "strings" "github.com/spf13/cobra" ) -// MinRequiredArgs checks if the minimum number of args exists, and returns an -// error if they do not. -func MinRequiredArgs(args []string, min int, cmd *cobra.Command) error { - if len(args) >= min { +// NoArgs validate args and returns an error if there are any args +func NoArgs(cmd *cobra.Command, args []string) error { + if len(args) == 0 { return nil } - return fmt.Errorf( - "\"%s\" requires at least %d argument(s).\n\nUsage: %s\n\n%s", - cmd.CommandPath(), - min, - cmd.UseLine(), - cmd.Short, - ) -} - -// AcceptsNoArgs returns an error message if there are args -func AcceptsNoArgs(args []string, cmd *cobra.Command) error { - if len(args) == 0 { - return nil + if cmd.HasSubCommands() { + return fmt.Errorf("\n" + strings.TrimRight(cmd.UsageString(), "\n")) } return fmt.Errorf( @@ -35,3 +24,19 @@ func AcceptsNoArgs(args []string, cmd *cobra.Command) error { cmd.Short, ) } + +// RequiresMinArgs returns an error if there is not at least min args +func RequiresMinArgs(min int) cobra.PositionalArgs { + return func(cmd *cobra.Command, args []string) error { + if len(args) >= min { + return nil + } + return fmt.Errorf( + "\"%s\" requires at least %d argument(s).\n\nUsage: %s\n\n%s", + cmd.CommandPath(), + min, + cmd.UseLine(), + cmd.Short, + ) + } +} From c79c17177485cd4a6c97eb8b23a24239f42dec2e Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 31 May 2016 14:47:51 -0700 Subject: [PATCH 048/978] Make the -h flag deprecated. Signed-off-by: Daniel Nephin Upstream-commit: 82c85e1e83f2cf040e69c1a3b3b1ab66538a06bd Component: cli --- components/cli/cobraadaptor/adaptor.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index cfa1b7f04c..db16166f09 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -36,6 +36,9 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { volume.NewVolumeCommand(dockerCli), ) + rootCmd.PersistentFlags().BoolP("help", "h", false, "Print usage") + rootCmd.PersistentFlags().MarkShorthandDeprecated("help", "please use --help") + return CobraAdaptor{ rootCmd: rootCmd, dockerCli: dockerCli, From c67e8b1f27f15266df2eac8f91cc453c73ada8ae Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 3 Jun 2016 19:50:01 +0200 Subject: [PATCH 049/978] Use spf13/cobra for docker search - Move image command search to `api/client/image/search.go` - Use cobra :) Signed-off-by: Vincent Demeester Upstream-commit: bbefa88a8c4014f368cd6e8cd16800fe8ca6f542 Component: cli --- components/cli/cobraadaptor/adaptor.go | 2 ++ components/cli/required.go | 16 ++++++++++++++++ components/cli/usage.go | 1 - 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index db16166f09..633844a12a 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/docker/docker/api/client" + "github.com/docker/docker/api/client/image" "github.com/docker/docker/api/client/volume" "github.com/docker/docker/cli" cliflags "github.com/docker/docker/cli/flags" @@ -34,6 +35,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { rootCmd.SetOutput(stdout) rootCmd.AddCommand( volume.NewVolumeCommand(dockerCli), + image.NewSearchCommand(dockerCli), ) rootCmd.PersistentFlags().BoolP("help", "h", false, "Print usage") diff --git a/components/cli/required.go b/components/cli/required.go index 94710374e6..b3ebb9ba9f 100644 --- a/components/cli/required.go +++ b/components/cli/required.go @@ -40,3 +40,19 @@ func RequiresMinArgs(min int) cobra.PositionalArgs { ) } } + +// ExactArgs returns an error if there is not the exact number of args +func ExactArgs(number int) cobra.PositionalArgs { + return func(cmd *cobra.Command, args []string) error { + if len(args) == number { + return nil + } + return fmt.Errorf( + "\"%s\" requires exactly %d argument(s).\n\nUsage: %s\n\n%s", + cmd.CommandPath(), + number, + cmd.UseLine(), + cmd.Short, + ) + } +} diff --git a/components/cli/usage.go b/components/cli/usage.go index 9ddc17326a..98d7fdf441 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -39,7 +39,6 @@ var DockerCommandUsage = []Command{ {"rmi", "Remove one or more images"}, {"run", "Run a command in a new container"}, {"save", "Save one or more images to a tar archive"}, - {"search", "Search the Docker Hub for images"}, {"start", "Start one or more stopped containers"}, {"stats", "Display a live stream of container(s) resource usage statistics"}, {"stop", "Stop a running container"}, From b31d040a09c561fb0a5b337de0ffdcd15aa5598e Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 31 May 2016 16:49:32 -0700 Subject: [PATCH 050/978] Convert 'docker run' to a cobra command and to use pflags Move container options into a struct so that tests should pass. Remove unused FlagSet arg from Parse Disable interspersed args on docker run Signed-off-by: Daniel Nephin Upstream-commit: 396c0660abcbd4d49db195c6b0cc14838e96a881 Component: cli --- components/cli/cobraadaptor/adaptor.go | 6 ++++-- components/cli/usage.go | 1 - 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index 633844a12a..7e6327ac2d 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/docker/docker/api/client" + "github.com/docker/docker/api/client/container" "github.com/docker/docker/api/client/image" "github.com/docker/docker/api/client/volume" "github.com/docker/docker/cli" @@ -34,8 +35,9 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { rootCmd.SetFlagErrorFunc(flagErrorFunc) rootCmd.SetOutput(stdout) rootCmd.AddCommand( - volume.NewVolumeCommand(dockerCli), + container.NewRunCommand(dockerCli), image.NewSearchCommand(dockerCli), + volume.NewVolumeCommand(dockerCli), ) rootCmd.PersistentFlags().BoolP("help", "h", false, "Print usage") @@ -52,7 +54,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { func (c CobraAdaptor) Usage() []cli.Command { cmds := []cli.Command{} for _, cmd := range c.rootCmd.Commands() { - cmds = append(cmds, cli.Command{Name: cmd.Use, Description: cmd.Short}) + cmds = append(cmds, cli.Command{Name: cmd.Name(), Description: cmd.Short}) } return cmds } diff --git a/components/cli/usage.go b/components/cli/usage.go index 98d7fdf441..324d1d92bf 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -37,7 +37,6 @@ var DockerCommandUsage = []Command{ {"restart", "Restart a container"}, {"rm", "Remove one or more containers"}, {"rmi", "Remove one or more images"}, - {"run", "Run a command in a new container"}, {"save", "Save one or more images to a tar archive"}, {"start", "Start one or more stopped containers"}, {"stats", "Display a live stream of container(s) resource usage statistics"}, From ed3db9e52f81e0bf3c8ee053fc0c4b248f47d9b6 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 31 May 2016 22:19:13 -0700 Subject: [PATCH 051/978] Convert 'docker create' to use cobra and pflag Return the correct status code on flag parsins errors. Signed-off-by: Daniel Nephin Upstream-commit: 69d30376353f1b86eed94135f47fb625be7182a5 Component: cli --- components/cli/cobraadaptor/adaptor.go | 19 ++----------------- components/cli/flagerrors.go | 21 +++++++++++++++++++++ components/cli/usage.go | 1 - 3 files changed, 23 insertions(+), 18 deletions(-) create mode 100644 components/cli/flagerrors.go diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index 7e6327ac2d..719eaf4e1d 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -1,8 +1,6 @@ package cobraadaptor import ( - "fmt" - "github.com/docker/docker/api/client" "github.com/docker/docker/api/client/container" "github.com/docker/docker/api/client/image" @@ -32,9 +30,10 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { } rootCmd.SetUsageTemplate(usageTemplate) rootCmd.SetHelpTemplate(helpTemplate) - rootCmd.SetFlagErrorFunc(flagErrorFunc) + rootCmd.SetFlagErrorFunc(cli.FlagErrorFunc) rootCmd.SetOutput(stdout) rootCmd.AddCommand( + container.NewCreateCommand(dockerCli), container.NewRunCommand(dockerCli), image.NewSearchCommand(dockerCli), volume.NewVolumeCommand(dockerCli), @@ -78,20 +77,6 @@ func (c CobraAdaptor) Command(name string) func(...string) error { return nil } -// flagErrorFunc prints an error messages which matches the format of the -// docker/docker/cli error messages -func flagErrorFunc(cmd *cobra.Command, err error) error { - if err == nil { - return err - } - - usage := "" - if cmd.HasSubCommands() { - usage = "\n\n" + cmd.UsageString() - } - return fmt.Errorf("%s\nSee '%s --help'.%s", err, cmd.CommandPath(), usage) -} - var usageTemplate = `Usage: {{if not .HasSubCommands}}{{if .HasLocalFlags}}{{appendIfNotPresent .UseLine "[OPTIONS]"}}{{else}}{{.UseLine}}{{end}}{{end}}{{if .HasSubCommands}}{{ .CommandPath}} COMMAND{{end}} {{with or .Long .Short }}{{. | trim}}{{end}}{{if gt .Aliases 0}} diff --git a/components/cli/flagerrors.go b/components/cli/flagerrors.go new file mode 100644 index 0000000000..aab8a98845 --- /dev/null +++ b/components/cli/flagerrors.go @@ -0,0 +1,21 @@ +package cli + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// FlagErrorFunc prints an error messages which matches the format of the +// docker/docker/cli error messages +func FlagErrorFunc(cmd *cobra.Command, err error) error { + if err == nil { + return err + } + + usage := "" + if cmd.HasSubCommands() { + usage = "\n\n" + cmd.UsageString() + } + return fmt.Errorf("%s\nSee '%s --help'.%s", err, cmd.CommandPath(), usage) +} diff --git a/components/cli/usage.go b/components/cli/usage.go index 324d1d92bf..d6c97c32f5 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -12,7 +12,6 @@ var DockerCommandUsage = []Command{ {"build", "Build an image from a Dockerfile"}, {"commit", "Create a new image from a container's changes"}, {"cp", "Copy files/folders between a container and the local filesystem"}, - {"create", "Create a new container"}, {"diff", "Inspect changes on a container's filesystem"}, {"events", "Get real time events from the server"}, {"exec", "Run a command in a running container"}, From 50bfe19924ce89101cd941c9adf0033bd5fbedc7 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 31 May 2016 22:19:13 -0700 Subject: [PATCH 052/978] Convert 'docker create' to use cobra and pflag Return the correct status code on flag parsins errors. Signed-off-by: Daniel Nephin Upstream-commit: aee35785205a0239ba071d18ea6e971961bb494c Component: cli --- components/cli/docker.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/components/cli/docker.go b/components/cli/docker.go index 45de2e3fca..0c727e32c9 100644 --- a/components/cli/docker.go +++ b/components/cli/docker.go @@ -73,6 +73,10 @@ func main() { if sterr, ok := err.(cli.StatusError); ok { if sterr.Status != "" { fmt.Fprintln(stderr, sterr.Status) + } + // StatusError should only be used for errors, and all errors should + // have a non-zero exit status, so never exit with 0 + if sterr.StatusCode == 0 { os.Exit(1) } os.Exit(sterr.StatusCode) From 53aae17014f040bb383f47d3f46d4ee88d22e831 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Sat, 4 Jun 2016 16:19:54 +0200 Subject: [PATCH 053/978] Display "See 'docker cmd --help'." in error cases This brings back this message in case missing arguments. Signed-off-by: Vincent Demeester Upstream-commit: 4a7a5f3a57a967c0dd0658658c96c21b8c172d29 Component: cli --- components/cli/required.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/components/cli/required.go b/components/cli/required.go index b3ebb9ba9f..1bbd55f524 100644 --- a/components/cli/required.go +++ b/components/cli/required.go @@ -18,7 +18,8 @@ func NoArgs(cmd *cobra.Command, args []string) error { } return fmt.Errorf( - "\"%s\" accepts no argument(s).\n\nUsage: %s\n\n%s", + "\"%s\" accepts no argument(s).\nSee '%s --help'.\n\nUsage: %s\n\n%s", + cmd.CommandPath(), cmd.CommandPath(), cmd.UseLine(), cmd.Short, @@ -32,9 +33,10 @@ func RequiresMinArgs(min int) cobra.PositionalArgs { return nil } return fmt.Errorf( - "\"%s\" requires at least %d argument(s).\n\nUsage: %s\n\n%s", + "\"%s\" requires at least %d argument(s).\nSee '%s --help'.\n\nUsage: %s\n\n%s", cmd.CommandPath(), min, + cmd.CommandPath(), cmd.UseLine(), cmd.Short, ) @@ -48,9 +50,10 @@ func ExactArgs(number int) cobra.PositionalArgs { return nil } return fmt.Errorf( - "\"%s\" requires exactly %d argument(s).\n\nUsage: %s\n\n%s", + "\"%s\" requires exactly %d argument(s).\nSee '%s --help'.\n\nUsage: %s\n\n%s", cmd.CommandPath(), number, + cmd.CommandPath(), cmd.UseLine(), cmd.Short, ) From 036d47c325bb296645b395a8086334c6ea843dbd Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Sun, 5 Jun 2016 16:42:19 +0200 Subject: [PATCH 054/978] Migrate export command to cobra Signed-off-by: Vincent Demeester Upstream-commit: 6ee903eea0678533d7c456704c6e87219737378d Component: cli --- components/cli/cobraadaptor/adaptor.go | 1 + components/cli/usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index 719eaf4e1d..a838f28170 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -34,6 +34,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { rootCmd.SetOutput(stdout) rootCmd.AddCommand( container.NewCreateCommand(dockerCli), + container.NewExportCommand(dockerCli), container.NewRunCommand(dockerCli), image.NewSearchCommand(dockerCli), volume.NewVolumeCommand(dockerCli), diff --git a/components/cli/usage.go b/components/cli/usage.go index d6c97c32f5..4a22afaad1 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -15,7 +15,6 @@ var DockerCommandUsage = []Command{ {"diff", "Inspect changes on a container's filesystem"}, {"events", "Get real time events from the server"}, {"exec", "Run a command in a running container"}, - {"export", "Export a container's filesystem as a tar archive"}, {"history", "Show the history of an image"}, {"images", "List images"}, {"import", "Import the contents from a tarball to create a filesystem image"}, From 2c18ee5cf6c0b17fa2be0699be9b9d65c18ab331 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 5 Jun 2016 10:25:21 -0700 Subject: [PATCH 055/978] Use spf13/cobra for docker stop This fix is part of the effort to convert commands to spf13/cobra #23211. Thif fix coverted command `docker stop` to use spf13/cobra Signed-off-by: Yong Tang Upstream-commit: 4770a4ba82d7e5ad26a845279f7f96cc585288c1 Component: cli --- components/cli/cobraadaptor/adaptor.go | 1 + components/cli/usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index 719eaf4e1d..40672bd8b9 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -35,6 +35,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { rootCmd.AddCommand( container.NewCreateCommand(dockerCli), container.NewRunCommand(dockerCli), + container.NewStopCommand(dockerCli), image.NewSearchCommand(dockerCli), volume.NewVolumeCommand(dockerCli), ) diff --git a/components/cli/usage.go b/components/cli/usage.go index d6c97c32f5..fb0de5cbe6 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -39,7 +39,6 @@ var DockerCommandUsage = []Command{ {"save", "Save one or more images to a tar archive"}, {"start", "Start one or more stopped containers"}, {"stats", "Display a live stream of container(s) resource usage statistics"}, - {"stop", "Stop a running container"}, {"tag", "Tag an image into a repository"}, {"top", "Display the running processes of a container"}, {"unpause", "Unpause all processes within a container"}, From f615c6e69d8cf575b7cf516a6690819ec9578488 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Sun, 5 Jun 2016 22:40:35 +0200 Subject: [PATCH 056/978] Use spf13/cobra for docker rmi Moves image command rmi to `api/client/image/remove.go` and use cobra :) Signed-off-by: Vincent Demeester Upstream-commit: 894cc1f20183e266f3e687bba1c657157c5932ee Component: cli --- components/cli/cobraadaptor/adaptor.go | 1 + components/cli/usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index cc74cce34c..139b9b1ec4 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -37,6 +37,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { container.NewExportCommand(dockerCli), container.NewRunCommand(dockerCli), container.NewStopCommand(dockerCli), + image.NewRemoveCommand(dockerCli), image.NewSearchCommand(dockerCli), volume.NewVolumeCommand(dockerCli), ) diff --git a/components/cli/usage.go b/components/cli/usage.go index 86bd1fcec4..1bcce75a3d 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -34,7 +34,6 @@ var DockerCommandUsage = []Command{ {"rename", "Rename a container"}, {"restart", "Restart a container"}, {"rm", "Remove one or more containers"}, - {"rmi", "Remove one or more images"}, {"save", "Save one or more images to a tar archive"}, {"start", "Start one or more stopped containers"}, {"stats", "Display a live stream of container(s) resource usage statistics"}, From c16e4d5a768293f8c6c32405a6e88061653c673c Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 5 Jun 2016 15:13:55 -0700 Subject: [PATCH 057/978] Use spf13/cobra for docker diff This fix is part of the effort to convert commands to spf13/cobra #23211. Thif fix coverted command `docker diff` to use spf13/cobra Signed-off-by: Yong Tang Upstream-commit: 65fed1bca2a23d22ee7299bd45e47d4f50cd3269 Component: cli --- components/cli/cobraadaptor/adaptor.go | 1 + components/cli/usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index cc74cce34c..ab1b0c6cc2 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -34,6 +34,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { rootCmd.SetOutput(stdout) rootCmd.AddCommand( container.NewCreateCommand(dockerCli), + container.NewDiffCommand(dockerCli), container.NewExportCommand(dockerCli), container.NewRunCommand(dockerCli), container.NewStopCommand(dockerCli), diff --git a/components/cli/usage.go b/components/cli/usage.go index 86bd1fcec4..99d7b6d707 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -12,7 +12,6 @@ var DockerCommandUsage = []Command{ {"build", "Build an image from a Dockerfile"}, {"commit", "Create a new image from a container's changes"}, {"cp", "Copy files/folders between a container and the local filesystem"}, - {"diff", "Inspect changes on a container's filesystem"}, {"events", "Get real time events from the server"}, {"exec", "Run a command in a running container"}, {"history", "Show the history of an image"}, From fd4182f4a441d4f7f4012a193792b7565063f008 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 5 Jun 2016 15:50:48 -0700 Subject: [PATCH 058/978] Use spf13/cobra for docker logs This fix is part of the effort to convert commands to spf13/cobra #23211. Thif fix coverted command `docker logs` to use spf13/cobra Signed-off-by: Yong Tang Upstream-commit: a5c6af94b1e48dca2b94661f6258c5e70701faa9 Component: cli --- components/cli/cobraadaptor/adaptor.go | 1 + components/cli/usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index 139b9b1ec4..a5dc7de8d6 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -35,6 +35,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { rootCmd.AddCommand( container.NewCreateCommand(dockerCli), container.NewExportCommand(dockerCli), + container.NewLogsCommand(dockerCli), container.NewRunCommand(dockerCli), container.NewStopCommand(dockerCli), image.NewRemoveCommand(dockerCli), diff --git a/components/cli/usage.go b/components/cli/usage.go index 1bcce75a3d..1235d45aa4 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -24,7 +24,6 @@ var DockerCommandUsage = []Command{ {"load", "Load an image from a tar archive or STDIN"}, {"login", "Log in to a Docker registry"}, {"logout", "Log out from a Docker registry"}, - {"logs", "Fetch the logs of a container"}, {"network", "Manage Docker networks"}, {"pause", "Pause all processes within a container"}, {"port", "List port mappings or a specific mapping for the CONTAINER"}, From 3b0881ed77c7e348ae6e1d9c2ea2ecddcc8e0c7e Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 5 Jun 2016 11:43:07 -0700 Subject: [PATCH 059/978] Use spf13/cobra for docker unpause This fix is part of the effort to convert commands to spf13/cobra #23211. Thif fix coverted command `docker unpause` to use spf13/cobra Signed-off-by: Yong Tang Upstream-commit: 316ab12eedcf450e84e7a64666ca3e094cea130a Component: cli --- components/cli/cobraadaptor/adaptor.go | 1 + components/cli/usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index 139b9b1ec4..941b5e0941 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -37,6 +37,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { container.NewExportCommand(dockerCli), container.NewRunCommand(dockerCli), container.NewStopCommand(dockerCli), + container.NewUnpauseCommand(dockerCli), image.NewRemoveCommand(dockerCli), image.NewSearchCommand(dockerCli), volume.NewVolumeCommand(dockerCli), diff --git a/components/cli/usage.go b/components/cli/usage.go index 1bcce75a3d..97cfb9d483 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -39,7 +39,6 @@ var DockerCommandUsage = []Command{ {"stats", "Display a live stream of container(s) resource usage statistics"}, {"tag", "Tag an image into a repository"}, {"top", "Display the running processes of a container"}, - {"unpause", "Unpause all processes within a container"}, {"update", "Update configuration of one or more containers"}, {"version", "Show the Docker version information"}, {"wait", "Block until a container stops, then print its exit code"}, From 91b39330c0e1e14fcb3201e8c14ee7f9baaeedf9 Mon Sep 17 00:00:00 2001 From: Zhang Wei Date: Sun, 5 Jun 2016 23:03:58 +0800 Subject: [PATCH 060/978] Migrate start command to cobra Signed-off-by: Zhang Wei Upstream-commit: 217e98c710075a691fad7ef8278fc9d2e2482be5 Component: cli --- components/cli/cobraadaptor/adaptor.go | 1 + components/cli/usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index 139b9b1ec4..bc497dfe9d 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -36,6 +36,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { container.NewCreateCommand(dockerCli), container.NewExportCommand(dockerCli), container.NewRunCommand(dockerCli), + container.NewStartCommand(dockerCli), container.NewStopCommand(dockerCli), image.NewRemoveCommand(dockerCli), image.NewSearchCommand(dockerCli), diff --git a/components/cli/usage.go b/components/cli/usage.go index 1bcce75a3d..f62e462b12 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -35,7 +35,6 @@ var DockerCommandUsage = []Command{ {"restart", "Restart a container"}, {"rm", "Remove one or more containers"}, {"save", "Save one or more images to a tar archive"}, - {"start", "Start one or more stopped containers"}, {"stats", "Display a live stream of container(s) resource usage statistics"}, {"tag", "Tag an image into a repository"}, {"top", "Display the running processes of a container"}, From 231ae1a7df0a0542327c4bc9a00abbd582f8edbd Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 6 Jun 2016 10:28:52 +0200 Subject: [PATCH 061/978] Migrate network command to cobra - Migrates network command and subcommands (connect, create, disconnect, inspect, list and remove) to spf13/cobra - Create a RequiredExactArgs helper function for command that require an exact number of arguments. Signed-off-by: Vincent Demeester Upstream-commit: fac425608a53091aa9033c37cac1eea127356e26 Component: cli --- components/cli/cobraadaptor/adaptor.go | 2 ++ components/cli/usage.go | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index a3df870cb7..4ceb57757d 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -4,6 +4,7 @@ import ( "github.com/docker/docker/api/client" "github.com/docker/docker/api/client/container" "github.com/docker/docker/api/client/image" + "github.com/docker/docker/api/client/network" "github.com/docker/docker/api/client/volume" "github.com/docker/docker/cli" cliflags "github.com/docker/docker/cli/flags" @@ -43,6 +44,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { container.NewUnpauseCommand(dockerCli), image.NewRemoveCommand(dockerCli), image.NewSearchCommand(dockerCli), + network.NewNetworkCommand(dockerCli), volume.NewVolumeCommand(dockerCli), ) diff --git a/components/cli/usage.go b/components/cli/usage.go index b5d9e904ad..f6152e5c93 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -23,7 +23,6 @@ var DockerCommandUsage = []Command{ {"load", "Load an image from a tar archive or STDIN"}, {"login", "Log in to a Docker registry"}, {"logout", "Log out from a Docker registry"}, - {"network", "Manage Docker networks"}, {"pause", "Pause all processes within a container"}, {"port", "List port mappings or a specific mapping for the CONTAINER"}, {"ps", "List containers"}, From afe109a57d6e2aa42c5f7b6b3c8422ad543bd761 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 6 Jun 2016 13:58:23 +0200 Subject: [PATCH 062/978] Migrate import command to cobra Signed-off-by: Vincent Demeester Upstream-commit: bbf4cd7b56f044ef43de212870e6be3bbd98ae22 Component: cli --- components/cli/cobraadaptor/adaptor.go | 1 + components/cli/usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index 4ceb57757d..05d726dacd 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -44,6 +44,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { container.NewUnpauseCommand(dockerCli), image.NewRemoveCommand(dockerCli), image.NewSearchCommand(dockerCli), + image.NewImportCommand(dockerCli), network.NewNetworkCommand(dockerCli), volume.NewVolumeCommand(dockerCli), ) diff --git a/components/cli/usage.go b/components/cli/usage.go index f6152e5c93..b60395073c 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -16,7 +16,6 @@ var DockerCommandUsage = []Command{ {"exec", "Run a command in a running container"}, {"history", "Show the history of an image"}, {"images", "List images"}, - {"import", "Import the contents from a tarball to create a filesystem image"}, {"info", "Display system-wide information"}, {"inspect", "Return low-level information on a container or image"}, {"kill", "Kill a running container"}, From 30758566427df9462b80ffbd907f317648df2bec Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 5 Jun 2016 17:25:22 -0700 Subject: [PATCH 063/978] Use spf13/cobra for docker wait This fix is part of the effort to convert commands to spf13/cobra #23211. Thif fix coverted command `docker wait` to use spf13/cobra Signed-off-by: Yong Tang Upstream-commit: b4421407a0417a20d72d0fc1cb3386c1360ca65f Component: cli --- components/cli/cobraadaptor/adaptor.go | 1 + components/cli/usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index 4ceb57757d..ee51435b87 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -42,6 +42,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { container.NewStartCommand(dockerCli), container.NewStopCommand(dockerCli), container.NewUnpauseCommand(dockerCli), + container.NewWaitCommand(dockerCli), image.NewRemoveCommand(dockerCli), image.NewSearchCommand(dockerCli), network.NewNetworkCommand(dockerCli), diff --git a/components/cli/usage.go b/components/cli/usage.go index f6152e5c93..23c357e1cf 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -37,7 +37,6 @@ var DockerCommandUsage = []Command{ {"top", "Display the running processes of a container"}, {"update", "Update configuration of one or more containers"}, {"version", "Show the Docker version information"}, - {"wait", "Block until a container stops, then print its exit code"}, } // DockerCommands stores all the docker command From b6dfddf0e2806d79fe34942636c10e393d196d1d Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 5 Jun 2016 16:48:26 -0700 Subject: [PATCH 064/978] Use spf13/cobra for docker port This fix is part of the effort to convert commands to spf13/cobra #23211. Thif fix coverted command `docker port` to use spf13/cobra Note: As part of this fix, a new function `RequiresMinMaxArgs(min int, max int)` has been added in cli/required.go. This function restrict the args to be at least min and at most max. Signed-off-by: Yong Tang Upstream-commit: c289179c99c9b66f798f19e8bc39152a6d4d0ed3 Component: cli --- components/cli/cobraadaptor/adaptor.go | 1 + components/cli/required.go | 18 ++++++++++++++++++ components/cli/usage.go | 1 - 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index 4ceb57757d..b873e9cae9 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -38,6 +38,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { container.NewDiffCommand(dockerCli), container.NewExportCommand(dockerCli), container.NewLogsCommand(dockerCli), + container.NewPortCommand(dockerCli), container.NewRunCommand(dockerCli), container.NewStartCommand(dockerCli), container.NewStopCommand(dockerCli), diff --git a/components/cli/required.go b/components/cli/required.go index 1bbd55f524..0cf35b3d68 100644 --- a/components/cli/required.go +++ b/components/cli/required.go @@ -43,6 +43,24 @@ func RequiresMinArgs(min int) cobra.PositionalArgs { } } +// RequiresMinMaxArgs returns an error if there is not at least min args and at most max args +func RequiresMinMaxArgs(min int, max int) cobra.PositionalArgs { + return func(cmd *cobra.Command, args []string) error { + if len(args) >= min && len(args) <= max { + return nil + } + return fmt.Errorf( + "\"%s\" requires at least %d and at most %d argument(s).\nSee '%s --help'.\n\nUsage: %s\n\n%s", + cmd.CommandPath(), + min, + max, + cmd.CommandPath(), + cmd.UseLine(), + cmd.Short, + ) + } +} + // ExactArgs returns an error if there is not the exact number of args func ExactArgs(number int) cobra.PositionalArgs { return func(cmd *cobra.Command, args []string) error { diff --git a/components/cli/usage.go b/components/cli/usage.go index f6152e5c93..fc232b5ecc 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -24,7 +24,6 @@ var DockerCommandUsage = []Command{ {"login", "Log in to a Docker registry"}, {"logout", "Log out from a Docker registry"}, {"pause", "Pause all processes within a container"}, - {"port", "List port mappings or a specific mapping for the CONTAINER"}, {"ps", "List containers"}, {"pull", "Pull an image or a repository from a registry"}, {"push", "Push an image or a repository to a registry"}, From fb1223adc05c1c99a6291c5f6a7c4ac70d0c73e7 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Mon, 6 Jun 2016 06:38:43 -0700 Subject: [PATCH 065/978] Use spf13/cobra for docker top This fix is part of the effort to convert commands to spf13/cobra #23211. Thif fix coverted command `docker top` to use spf13/cobra Signed-off-by: Yong Tang Upstream-commit: 6829d53a08940730ad681c473ec131e52ebafa2b Component: cli --- components/cli/cobraadaptor/adaptor.go | 1 + components/cli/usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index 4ceb57757d..7c839c628e 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -41,6 +41,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { container.NewRunCommand(dockerCli), container.NewStartCommand(dockerCli), container.NewStopCommand(dockerCli), + container.NewTopCommand(dockerCli), container.NewUnpauseCommand(dockerCli), image.NewRemoveCommand(dockerCli), image.NewSearchCommand(dockerCli), diff --git a/components/cli/usage.go b/components/cli/usage.go index f6152e5c93..31eda11b9b 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -34,7 +34,6 @@ var DockerCommandUsage = []Command{ {"save", "Save one or more images to a tar archive"}, {"stats", "Display a live stream of container(s) resource usage statistics"}, {"tag", "Tag an image into a repository"}, - {"top", "Display the running processes of a container"}, {"update", "Update configuration of one or more containers"}, {"version", "Show the Docker version information"}, {"wait", "Block until a container stops, then print its exit code"}, From a3b1f76002a11f8ed9eb5b1d8243240c251bdbdf Mon Sep 17 00:00:00 2001 From: Zhang Wei Date: Mon, 6 Jun 2016 00:17:39 +0800 Subject: [PATCH 066/978] Move attach command to cobra. Signed-off-by: Zhang Wei Upstream-commit: 096f7f72bf289b780381e5b858020d07f2cf9241 Component: cli --- components/cli/cobraadaptor/adaptor.go | 1 + components/cli/usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index 4ceb57757d..cf3966f141 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -34,6 +34,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { rootCmd.SetFlagErrorFunc(cli.FlagErrorFunc) rootCmd.SetOutput(stdout) rootCmd.AddCommand( + container.NewAttachCommand(dockerCli), container.NewCreateCommand(dockerCli), container.NewDiffCommand(dockerCli), container.NewExportCommand(dockerCli), diff --git a/components/cli/usage.go b/components/cli/usage.go index f6152e5c93..6a42f9281f 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -8,7 +8,6 @@ type Command struct { // DockerCommandUsage lists the top level docker commands and their short usage var DockerCommandUsage = []Command{ - {"attach", "Attach to a running container"}, {"build", "Build an image from a Dockerfile"}, {"commit", "Create a new image from a container's changes"}, {"cp", "Copy files/folders between a container and the local filesystem"}, From f8e3edcba2d5159fa4006d4064603026eebd1778 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 5 Jun 2016 19:54:33 -0700 Subject: [PATCH 067/978] Use spf13/cobra for docker history This fix is part of the effort to convert commands to spf13/cobra #23211. Thif fix coverted command `docker history` to use spf13/cobra Signed-off-by: Yong Tang Upstream-commit: 0090463cabd9ebcb94539763ecd26ccbe471de7f Component: cli --- components/cli/cobraadaptor/adaptor.go | 1 + components/cli/usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index ee51435b87..f1d97002c6 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -43,6 +43,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { container.NewStopCommand(dockerCli), container.NewUnpauseCommand(dockerCli), container.NewWaitCommand(dockerCli), + image.NewHistoryCommand(dockerCli), image.NewRemoveCommand(dockerCli), image.NewSearchCommand(dockerCli), network.NewNetworkCommand(dockerCli), diff --git a/components/cli/usage.go b/components/cli/usage.go index 23c357e1cf..b9563cb154 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -14,7 +14,6 @@ var DockerCommandUsage = []Command{ {"cp", "Copy files/folders between a container and the local filesystem"}, {"events", "Get real time events from the server"}, {"exec", "Run a command in a running container"}, - {"history", "Show the history of an image"}, {"images", "List images"}, {"import", "Import the contents from a tarball to create a filesystem image"}, {"info", "Display system-wide information"}, From bd7d99cb269f4eb20ecaa6a0afa153a2ae0e8493 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 6 Jun 2016 14:17:04 -0400 Subject: [PATCH 068/978] Fix a panic when the DOCKER_HOST was invalid using cobra commands. Signed-off-by: Daniel Nephin Upstream-commit: 55d46e8352713ef50a2f888fc642c8855bcc3362 Component: cli --- components/cli/cobraadaptor/adaptor.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index ca7d4e54c0..342c1f8781 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -71,7 +71,9 @@ func (c CobraAdaptor) Usage() []cli.Command { } func (c CobraAdaptor) run(cmd string, args []string) error { - c.dockerCli.Initialize() + if err := c.dockerCli.Initialize(); err != nil { + return err + } // Prepend the command name to support normal cobra command delegation c.rootCmd.SetArgs(append([]string{cmd}, args...)) return c.rootCmd.Execute() From 8fc0484b38a01916efd7ce61184512a92b3604e0 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 5 Jun 2016 16:12:59 -0700 Subject: [PATCH 069/978] Use spf13/cobra for docker pause This fix is part of the effort to convert commands to spf13/cobra #23211. Thif fix coverted command `docker pause` to use spf13/cobra Signed-off-by: Yong Tang Upstream-commit: 084a028e84e19580f8788e14224767239e8f399f Component: cli --- components/cli/cobraadaptor/adaptor.go | 1 + components/cli/usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index ca7d4e54c0..2f245d8d3f 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -38,6 +38,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { container.NewDiffCommand(dockerCli), container.NewExportCommand(dockerCli), container.NewLogsCommand(dockerCli), + container.NewPauseCommand(dockerCli), container.NewPortCommand(dockerCli), container.NewRunCommand(dockerCli), container.NewStartCommand(dockerCli), diff --git a/components/cli/usage.go b/components/cli/usage.go index f336d611f2..4fbc5d08d6 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -23,7 +23,6 @@ var DockerCommandUsage = []Command{ {"load", "Load an image from a tar archive or STDIN"}, {"login", "Log in to a Docker registry"}, {"logout", "Log out from a Docker registry"}, - {"pause", "Pause all processes within a container"}, {"ps", "List containers"}, {"pull", "Pull an image or a repository from a registry"}, {"push", "Push an image or a repository to a registry"}, From 7251e7360714b6c03a710cd22e46029f002dec25 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 5 Jun 2016 19:16:26 -0700 Subject: [PATCH 070/978] Use spf13/cobra for docker rename This fix is part of the effort to convert commands to spf13/cobra #23211. Thif fix coverted command `docker rename` to use spf13/cobra Signed-off-by: Yong Tang Upstream-commit: 3ff6d507fe5ad73a58b2aa90cf1489533454dc94 Component: cli --- components/cli/cobraadaptor/adaptor.go | 1 + components/cli/usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index ca7d4e54c0..83b01a3f7f 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -39,6 +39,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { container.NewExportCommand(dockerCli), container.NewLogsCommand(dockerCli), container.NewPortCommand(dockerCli), + container.NewRenameCommand(dockerCli), container.NewRunCommand(dockerCli), container.NewStartCommand(dockerCli), container.NewStopCommand(dockerCli), diff --git a/components/cli/usage.go b/components/cli/usage.go index f336d611f2..6807951271 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -27,7 +27,6 @@ var DockerCommandUsage = []Command{ {"ps", "List containers"}, {"pull", "Pull an image or a repository from a registry"}, {"push", "Push an image or a repository to a registry"}, - {"rename", "Rename a container"}, {"restart", "Restart a container"}, {"rm", "Remove one or more containers"}, {"save", "Save one or more images to a tar archive"}, From 1422866b8882d5b6ccc45378091a37d92f10124f Mon Sep 17 00:00:00 2001 From: Zhang Wei Date: Mon, 6 Jun 2016 22:15:46 +0800 Subject: [PATCH 071/978] Migrate restart command to cobra Signed-off-by: Zhang Wei Upstream-commit: 254cce44cd2dd7329ce60706d4bbffbba97971ac Component: cli --- components/cli/cobraadaptor/adaptor.go | 1 + components/cli/usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index f371ce4ed7..28b57e2bbb 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -42,6 +42,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { container.NewPauseCommand(dockerCli), container.NewPortCommand(dockerCli), container.NewRenameCommand(dockerCli), + container.NewRestartCommand(dockerCli), container.NewRunCommand(dockerCli), container.NewStartCommand(dockerCli), container.NewStopCommand(dockerCli), diff --git a/components/cli/usage.go b/components/cli/usage.go index 787a87aaee..34397ccd23 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -24,7 +24,6 @@ var DockerCommandUsage = []Command{ {"ps", "List containers"}, {"pull", "Pull an image or a repository from a registry"}, {"push", "Push an image or a repository to a registry"}, - {"restart", "Restart a container"}, {"rm", "Remove one or more containers"}, {"save", "Save one or more images to a tar archive"}, {"stats", "Display a live stream of container(s) resource usage statistics"}, From e5717aed2fd1f81b9614abe3c7571cde2e0c4d90 Mon Sep 17 00:00:00 2001 From: Zhang Wei Date: Mon, 6 Jun 2016 15:38:20 +0800 Subject: [PATCH 072/978] Migrate kill command to cobra Signed-off-by: Zhang Wei Upstream-commit: 05c7e2e124cacc7add50733242ed34bad64c4f69 Component: cli --- components/cli/cobraadaptor/adaptor.go | 1 + components/cli/usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index f371ce4ed7..2cb64e0541 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -38,6 +38,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { container.NewCreateCommand(dockerCli), container.NewDiffCommand(dockerCli), container.NewExportCommand(dockerCli), + container.NewKillCommand(dockerCli), container.NewLogsCommand(dockerCli), container.NewPauseCommand(dockerCli), container.NewPortCommand(dockerCli), diff --git a/components/cli/usage.go b/components/cli/usage.go index 787a87aaee..727a7fb77a 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -17,7 +17,6 @@ var DockerCommandUsage = []Command{ {"import", "Import the contents from a tarball to create a filesystem image"}, {"info", "Display system-wide information"}, {"inspect", "Return low-level information on a container or image"}, - {"kill", "Kill a running container"}, {"load", "Load an image from a tar archive or STDIN"}, {"login", "Log in to a Docker registry"}, {"logout", "Log out from a Docker registry"}, From a90edeef1db66b6857898f85a5bc0cfff7044518 Mon Sep 17 00:00:00 2001 From: Zhang Wei Date: Mon, 6 Jun 2016 23:32:38 +0800 Subject: [PATCH 073/978] Migrate rm command to cobra Signed-off-by: Zhang Wei Upstream-commit: 50b375d189a31dd1582dedd378245ced8435ca32 Component: cli --- components/cli/cobraadaptor/adaptor.go | 1 + components/cli/usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index 0d536f0608..fbddc77f1f 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -44,6 +44,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { container.NewPortCommand(dockerCli), container.NewRenameCommand(dockerCli), container.NewRestartCommand(dockerCli), + container.NewRmCommand(dockerCli), container.NewRunCommand(dockerCli), container.NewStartCommand(dockerCli), container.NewStopCommand(dockerCli), diff --git a/components/cli/usage.go b/components/cli/usage.go index baf4948931..85de1fa36f 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -22,7 +22,6 @@ var DockerCommandUsage = []Command{ {"ps", "List containers"}, {"pull", "Pull an image or a repository from a registry"}, {"push", "Push an image or a repository to a registry"}, - {"rm", "Remove one or more containers"}, {"save", "Save one or more images to a tar archive"}, {"stats", "Display a live stream of container(s) resource usage statistics"}, {"tag", "Tag an image into a repository"}, From fb468efb0a8dfa5d13e2bc237b3c2741bb4388e2 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 5 Jun 2016 20:55:47 -0700 Subject: [PATCH 074/978] Use spf13/cobra for docker images This fix is part of the effort to convert commands to spf13/cobra #23211. Thif fix coverted command `docker images` to use spf13/cobra NOTE: As part of this fix, a new function `RequiresMaxArgs()` has been defined in `cli/required.go`. This func returns an error if there is not at most max args Signed-off-by: Yong Tang Upstream-commit: fcd9f9f7bd623d0d8a6ee823e463786ed408faa1 Component: cli --- components/cli/cobraadaptor/adaptor.go | 1 + components/cli/required.go | 17 +++++++++++++++++ components/cli/usage.go | 1 - 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index 0d536f0608..4f1f77ca00 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -51,6 +51,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { container.NewUnpauseCommand(dockerCli), container.NewWaitCommand(dockerCli), image.NewHistoryCommand(dockerCli), + image.NewImagesCommand(dockerCli), image.NewRemoveCommand(dockerCli), image.NewSearchCommand(dockerCli), image.NewImportCommand(dockerCli), diff --git a/components/cli/required.go b/components/cli/required.go index 0cf35b3d68..0c08e64f1a 100644 --- a/components/cli/required.go +++ b/components/cli/required.go @@ -43,6 +43,23 @@ func RequiresMinArgs(min int) cobra.PositionalArgs { } } +// RequiresMaxArgs returns an error if there is not at most max args +func RequiresMaxArgs(max int) cobra.PositionalArgs { + return func(cmd *cobra.Command, args []string) error { + if len(args) <= max { + return nil + } + return fmt.Errorf( + "\"%s\" requires at most %d argument(s).\nSee '%s --help'.\n\nUsage: %s\n\n%s", + cmd.CommandPath(), + max, + cmd.CommandPath(), + cmd.UseLine(), + cmd.Short, + ) + } +} + // RequiresMinMaxArgs returns an error if there is not at least min args and at most max args func RequiresMinMaxArgs(min int, max int) cobra.PositionalArgs { return func(cmd *cobra.Command, args []string) error { diff --git a/components/cli/usage.go b/components/cli/usage.go index baf4948931..a144df1af8 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -13,7 +13,6 @@ var DockerCommandUsage = []Command{ {"cp", "Copy files/folders between a container and the local filesystem"}, {"events", "Get real time events from the server"}, {"exec", "Run a command in a running container"}, - {"images", "List images"}, {"info", "Display system-wide information"}, {"inspect", "Return low-level information on a container or image"}, {"load", "Load an image from a tar archive or STDIN"}, From b125b6b0a73787c3fd6d9082da4df1c8a70b0d89 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Mon, 6 Jun 2016 19:15:37 -0700 Subject: [PATCH 075/978] Use spf13/cobra for docker version This fix is part of the effort to convert commands to spf13/cobra #23211. Thif fix coverted command `docker version` to use spf13/cobra NOTE: Most of the commands like `run`, `images` etc. goes to packages of `container`, `image`, `network`, etc. Didn't find a good place for `docker version` so just use the package `client` for now. Signed-off-by: Yong Tang Upstream-commit: 7e043735b6e71e759b9a1b34f67c919f87c7d2cd Component: cli --- components/cli/cobraadaptor/adaptor.go | 2 ++ components/cli/usage.go | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index 0d536f0608..555f265ac0 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -5,6 +5,7 @@ import ( "github.com/docker/docker/api/client/container" "github.com/docker/docker/api/client/image" "github.com/docker/docker/api/client/network" + "github.com/docker/docker/api/client/system" "github.com/docker/docker/api/client/volume" "github.com/docker/docker/cli" cliflags "github.com/docker/docker/cli/flags" @@ -55,6 +56,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { image.NewSearchCommand(dockerCli), image.NewImportCommand(dockerCli), network.NewNetworkCommand(dockerCli), + system.NewVersionCommand(dockerCli), volume.NewVolumeCommand(dockerCli), ) diff --git a/components/cli/usage.go b/components/cli/usage.go index baf4948931..1ad0a891e8 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -27,7 +27,6 @@ var DockerCommandUsage = []Command{ {"stats", "Display a live stream of container(s) resource usage statistics"}, {"tag", "Tag an image into a repository"}, {"update", "Update configuration of one or more containers"}, - {"version", "Show the Docker version information"}, } // DockerCommands stores all the docker command From 528e8c062e56301c685d1088b328f67e936f5c09 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 5 Jun 2016 11:05:35 -0700 Subject: [PATCH 076/978] Use spf13/cobra for docker tag This fix is part of the effort to convert commands to spf13/cobra #23211. Thif fix coverted command `docker tag` to use spf13/cobra Signed-off-by: Yong Tang Upstream-commit: 4951a30626466f78e1ab20cf763f878038a69a1b Component: cli --- components/cli/cobraadaptor/adaptor.go | 1 + components/cli/usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index 0d536f0608..8c73831f4a 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -54,6 +54,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { image.NewRemoveCommand(dockerCli), image.NewSearchCommand(dockerCli), image.NewImportCommand(dockerCli), + image.NewTagCommand(dockerCli), network.NewNetworkCommand(dockerCli), volume.NewVolumeCommand(dockerCli), ) diff --git a/components/cli/usage.go b/components/cli/usage.go index baf4948931..b8314eb0ab 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -25,7 +25,6 @@ var DockerCommandUsage = []Command{ {"rm", "Remove one or more containers"}, {"save", "Save one or more images to a tar archive"}, {"stats", "Display a live stream of container(s) resource usage statistics"}, - {"tag", "Tag an image into a repository"}, {"update", "Update configuration of one or more containers"}, {"version", "Show the Docker version information"}, } From 640b5bcdfed311a63cfb4320f63aafbeb6a9ec22 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Tue, 7 Jun 2016 18:15:44 +0200 Subject: [PATCH 077/978] Migrate docker build to cobra Signed-off-by: Vincent Demeester Upstream-commit: 15083c2e98b5067d93beb32a5fe6201388ba84ea Component: cli --- components/cli/cobraadaptor/adaptor.go | 1 + components/cli/usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index 0d536f0608..51dc09bd84 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -50,6 +50,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { container.NewTopCommand(dockerCli), container.NewUnpauseCommand(dockerCli), container.NewWaitCommand(dockerCli), + image.NewBuildCommand(dockerCli), image.NewHistoryCommand(dockerCli), image.NewRemoveCommand(dockerCli), image.NewSearchCommand(dockerCli), diff --git a/components/cli/usage.go b/components/cli/usage.go index baf4948931..065debaae0 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -8,7 +8,6 @@ type Command struct { // DockerCommandUsage lists the top level docker commands and their short usage var DockerCommandUsage = []Command{ - {"build", "Build an image from a Dockerfile"}, {"commit", "Create a new image from a container's changes"}, {"cp", "Copy files/folders between a container and the local filesystem"}, {"events", "Get real time events from the server"}, From 7fd502822b204ea9f190f291993cf93df52fffb5 Mon Sep 17 00:00:00 2001 From: Zhang Wei Date: Tue, 7 Jun 2016 00:47:18 +0800 Subject: [PATCH 078/978] Migrate stats and events command to cobra. Signed-off-by: Zhang Wei Upstream-commit: 3f5ac2f50f20a4b6212f0de95436c3b53dddd15e Component: cli --- components/cli/cobraadaptor/adaptor.go | 2 ++ components/cli/usage.go | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index 5b31a0bd67..2b7cd169e5 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -48,6 +48,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { container.NewRmCommand(dockerCli), container.NewRunCommand(dockerCli), container.NewStartCommand(dockerCli), + container.NewStatsCommand(dockerCli), container.NewStopCommand(dockerCli), container.NewTopCommand(dockerCli), container.NewUnpauseCommand(dockerCli), @@ -59,6 +60,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { image.NewImportCommand(dockerCli), image.NewTagCommand(dockerCli), network.NewNetworkCommand(dockerCli), + system.NewEventsCommand(dockerCli), system.NewVersionCommand(dockerCli), volume.NewVolumeCommand(dockerCli), ) diff --git a/components/cli/usage.go b/components/cli/usage.go index 7b1573fc6a..4749f20aa3 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -11,7 +11,6 @@ var DockerCommandUsage = []Command{ {"build", "Build an image from a Dockerfile"}, {"commit", "Create a new image from a container's changes"}, {"cp", "Copy files/folders between a container and the local filesystem"}, - {"events", "Get real time events from the server"}, {"exec", "Run a command in a running container"}, {"info", "Display system-wide information"}, {"inspect", "Return low-level information on a container or image"}, @@ -22,7 +21,6 @@ var DockerCommandUsage = []Command{ {"pull", "Pull an image or a repository from a registry"}, {"push", "Push an image or a repository to a registry"}, {"save", "Save one or more images to a tar archive"}, - {"stats", "Display a live stream of container(s) resource usage statistics"}, {"update", "Update configuration of one or more containers"}, } From 65ee11dbcd7f6c597db7637eb605d36cd4ff1ff3 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Mon, 6 Jun 2016 07:57:44 -0700 Subject: [PATCH 079/978] Use spf13/cobra for docker commit This fix is part of the effort to convert commands to spf13/cobra #23211. Thif fix coverted command `docker commit` to use spf13/cobra NOTE: `RequiresMinMaxArgs()` has been renamed to `RequiresRangeArgs()`. Signed-off-by: Yong Tang Upstream-commit: 43fdd6f2fa3dc514bf53338c375af2710c5b971f Component: cli --- components/cli/cobraadaptor/adaptor.go | 1 + components/cli/required.go | 4 ++-- components/cli/usage.go | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index 11b821cc44..4d4ca433ba 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -36,6 +36,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { rootCmd.SetOutput(stdout) rootCmd.AddCommand( container.NewAttachCommand(dockerCli), + container.NewCommitCommand(dockerCli), container.NewCreateCommand(dockerCli), container.NewDiffCommand(dockerCli), container.NewExportCommand(dockerCli), diff --git a/components/cli/required.go b/components/cli/required.go index 0c08e64f1a..9276a5740a 100644 --- a/components/cli/required.go +++ b/components/cli/required.go @@ -60,8 +60,8 @@ func RequiresMaxArgs(max int) cobra.PositionalArgs { } } -// RequiresMinMaxArgs returns an error if there is not at least min args and at most max args -func RequiresMinMaxArgs(min int, max int) cobra.PositionalArgs { +// RequiresRangeArgs returns an error if there is not at least min args and at most max args +func RequiresRangeArgs(min int, max int) cobra.PositionalArgs { return func(cmd *cobra.Command, args []string) error { if len(args) >= min && len(args) <= max { return nil diff --git a/components/cli/usage.go b/components/cli/usage.go index e9debf3be7..32b3442653 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -8,7 +8,6 @@ type Command struct { // DockerCommandUsage lists the top level docker commands and their short usage var DockerCommandUsage = []Command{ - {"commit", "Create a new image from a container's changes"}, {"cp", "Copy files/folders between a container and the local filesystem"}, {"exec", "Run a command in a running container"}, {"info", "Display system-wide information"}, From 2710da2962ceaf14fae367bbb3f3573a44070173 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Thu, 9 Jun 2016 17:28:33 +0200 Subject: [PATCH 080/978] Migrate load command to cobra Signed-off-by: Vincent Demeester Upstream-commit: ca96b906bc6ca90c1bdef1832a316935950d870d Component: cli --- components/cli/cobraadaptor/adaptor.go | 1 + components/cli/usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index 11b821cc44..c5fcac0698 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -56,6 +56,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { image.NewBuildCommand(dockerCli), image.NewHistoryCommand(dockerCli), image.NewImagesCommand(dockerCli), + image.NewLoadCommand(dockerCli), image.NewRemoveCommand(dockerCli), image.NewSearchCommand(dockerCli), image.NewImportCommand(dockerCli), diff --git a/components/cli/usage.go b/components/cli/usage.go index e9debf3be7..b6d0439052 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -13,7 +13,6 @@ var DockerCommandUsage = []Command{ {"exec", "Run a command in a running container"}, {"info", "Display system-wide information"}, {"inspect", "Return low-level information on a container or image"}, - {"load", "Load an image from a tar archive or STDIN"}, {"login", "Log in to a Docker registry"}, {"logout", "Log out from a Docker registry"}, {"ps", "List containers"}, From 2c2e707ce1f7b8c68a05c475d16cb56b77bad6fa Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Thu, 9 Jun 2016 17:38:20 +0200 Subject: [PATCH 081/978] Migrate save command to cobra Signed-off-by: Vincent Demeester Upstream-commit: 171adb0b0fb19b7a5833703e86a92a464b2be6cf Component: cli --- components/cli/cobraadaptor/adaptor.go | 1 + components/cli/usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index c5fcac0698..a9bdd1a170 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -58,6 +58,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { image.NewImagesCommand(dockerCli), image.NewLoadCommand(dockerCli), image.NewRemoveCommand(dockerCli), + image.NewSaveCommand(dockerCli), image.NewSearchCommand(dockerCli), image.NewImportCommand(dockerCli), image.NewTagCommand(dockerCli), diff --git a/components/cli/usage.go b/components/cli/usage.go index b6d0439052..20eae80f74 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -18,7 +18,6 @@ var DockerCommandUsage = []Command{ {"ps", "List containers"}, {"pull", "Pull an image or a repository from a registry"}, {"push", "Push an image or a repository to a registry"}, - {"save", "Save one or more images to a tar archive"}, {"update", "Update configuration of one or more containers"}, } From ab55be6e386beac9e8c5754be69e1f0edb2a31c1 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 9 Jun 2016 13:20:55 -0400 Subject: [PATCH 082/978] Support running 'docker help daemon' Signed-off-by: Daniel Nephin Upstream-commit: 90e5326097e1b508f440404d8b605eae7e57a337 Component: cli --- components/cli/daemon_unix.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/components/cli/daemon_unix.go b/components/cli/daemon_unix.go index 7a27518636..d515b82914 100644 --- a/components/cli/daemon_unix.go +++ b/components/cli/daemon_unix.go @@ -11,8 +11,14 @@ import ( // CmdDaemon execs dockerd with the same flags func (p DaemonProxy) CmdDaemon(args ...string) error { - // Use os.Args[1:] so that "global" args are passed to dockerd - args = stripDaemonArg(os.Args[1:]) + // Special case for handling `docker help daemon`. When pkg/mflag is removed + // we can support this on the daemon side, but that is not possible with + // pkg/mflag because it uses os.Exit(1) instead of returning an error on + // unexpected args. + if len(args) == 0 || args[0] != "--help" { + // Use os.Args[1:] so that "global" args are passed to dockerd + args = stripDaemonArg(os.Args[1:]) + } binaryPath, err := findDaemonBinary() if err != nil { From 102ec2c2f84863e2371d8b95bf62beeaf850042d Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 10 Jun 2016 12:04:29 +0200 Subject: [PATCH 083/978] Migrate login & logout command to cobra MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also moves some common stuff around : - `api/client/registry.go` for registry related method (`ElectAuthServer`, …) - `api/client/credentials.go` to interact with credentials Migrate logout command to cobra Signed-off-by: Vincent Demeester Upstream-commit: d4fef62ce0171c219642d60a2b705bcff3cbbc10 Component: cli --- components/cli/cobraadaptor/adaptor.go | 3 +++ components/cli/usage.go | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index a9bdd1a170..11edef6f28 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -5,6 +5,7 @@ import ( "github.com/docker/docker/api/client/container" "github.com/docker/docker/api/client/image" "github.com/docker/docker/api/client/network" + "github.com/docker/docker/api/client/registry" "github.com/docker/docker/api/client/system" "github.com/docker/docker/api/client/volume" "github.com/docker/docker/cli" @@ -64,6 +65,8 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { image.NewTagCommand(dockerCli), network.NewNetworkCommand(dockerCli), system.NewEventsCommand(dockerCli), + registry.NewLoginCommand(dockerCli), + registry.NewLogoutCommand(dockerCli), system.NewVersionCommand(dockerCli), volume.NewVolumeCommand(dockerCli), ) diff --git a/components/cli/usage.go b/components/cli/usage.go index 20eae80f74..1e6c3dc202 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -13,8 +13,6 @@ var DockerCommandUsage = []Command{ {"exec", "Run a command in a running container"}, {"info", "Display system-wide information"}, {"inspect", "Return low-level information on a container or image"}, - {"login", "Log in to a Docker registry"}, - {"logout", "Log out from a Docker registry"}, {"ps", "List containers"}, {"pull", "Pull an image or a repository from a registry"}, {"push", "Push an image or a repository to a registry"}, From 7f453bbb7047c9f1471e5db1d1905e4f6e72e911 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 10 Jun 2016 12:07:23 +0200 Subject: [PATCH 084/978] Migrate pull command to cobra Signed-off-by: Vincent Demeester Upstream-commit: 27fd1bffb02da5c5d951581bcbe3e7587352035d Component: cli --- components/cli/cobraadaptor/adaptor.go | 1 + components/cli/usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index a9bdd1a170..2f3d14821e 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -59,6 +59,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { image.NewLoadCommand(dockerCli), image.NewRemoveCommand(dockerCli), image.NewSaveCommand(dockerCli), + image.NewPullCommand(dockerCli), image.NewSearchCommand(dockerCli), image.NewImportCommand(dockerCli), image.NewTagCommand(dockerCli), diff --git a/components/cli/usage.go b/components/cli/usage.go index 20eae80f74..c063dd0da6 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -16,7 +16,6 @@ var DockerCommandUsage = []Command{ {"login", "Log in to a Docker registry"}, {"logout", "Log out from a Docker registry"}, {"ps", "List containers"}, - {"pull", "Pull an image or a repository from a registry"}, {"push", "Push an image or a repository to a registry"}, {"update", "Update configuration of one or more containers"}, } From 08cd8b5ab5973c6e2fcb8a75ff746ff8b16fbdd7 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 10 Jun 2016 12:07:28 +0200 Subject: [PATCH 085/978] Migrate push command to cobra Signed-off-by: Vincent Demeester Upstream-commit: 16dbf630a2fb98985d7b39b7c54090d211021e2e Component: cli --- components/cli/cobraadaptor/adaptor.go | 1 + 1 file changed, 1 insertion(+) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index 2f3d14821e..ec0c6d39c0 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -60,6 +60,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { image.NewRemoveCommand(dockerCli), image.NewSaveCommand(dockerCli), image.NewPullCommand(dockerCli), + image.NewPushCommand(dockerCli), image.NewSearchCommand(dockerCli), image.NewImportCommand(dockerCli), image.NewTagCommand(dockerCli), From c516b89c830beaa009289eda53edc461e27938a6 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 10 Jun 2016 12:07:32 +0200 Subject: [PATCH 086/978] Moving Image{Push,Pull}Privileged to trust.go Signed-off-by: Vincent Demeester Upstream-commit: 0679ae5bf4c34bb8d099770e85bb9023bfe23839 Component: cli --- components/cli/usage.go | 1 - 1 file changed, 1 deletion(-) diff --git a/components/cli/usage.go b/components/cli/usage.go index c063dd0da6..7ebbf2d12f 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -16,7 +16,6 @@ var DockerCommandUsage = []Command{ {"login", "Log in to a Docker registry"}, {"logout", "Log out from a Docker registry"}, {"ps", "List containers"}, - {"push", "Push an image or a repository to a registry"}, {"update", "Update configuration of one or more containers"}, } From 331a2b56c2787d77344fc8efaa71b65536c1bdbb Mon Sep 17 00:00:00 2001 From: Tianyi Wang Date: Wed, 8 Jun 2016 21:56:44 +0900 Subject: [PATCH 087/978] Migrate ps command to cobra Signed-off-by: Tianyi Wang Upstream-commit: 92e6b85fa0c518a4ac44c71e1737d891d1b50892 Component: cli --- components/cli/cobraadaptor/adaptor.go | 1 + components/cli/usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index b1980491a8..4d2958b531 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -45,6 +45,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { container.NewLogsCommand(dockerCli), container.NewPauseCommand(dockerCli), container.NewPortCommand(dockerCli), + container.NewPsCommand(dockerCli), container.NewRenameCommand(dockerCli), container.NewRestartCommand(dockerCli), container.NewRmCommand(dockerCli), diff --git a/components/cli/usage.go b/components/cli/usage.go index 9cd7acd244..73fa4f2245 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -12,7 +12,6 @@ var DockerCommandUsage = []Command{ {"exec", "Run a command in a running container"}, {"info", "Display system-wide information"}, {"inspect", "Return low-level information on a container or image"}, - {"ps", "List containers"}, {"update", "Update configuration of one or more containers"}, } From dab4dc32bc2b7d7bd39b619b100ac2e533f6709d Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 13 Jun 2016 19:56:23 -0700 Subject: [PATCH 088/978] Add Swarm management CLI commands As described in our ROADMAP.md, introduce new Swarm management commands to call to the corresponding API endpoints. This PR is fully backward compatible (joining a Swarm is an optional feature of the Engine, and existing commands are not impacted). Signed-off-by: Daniel Nephin Signed-off-by: Victor Vieux Signed-off-by: Tonis Tiigi Upstream-commit: 408531dafa6ae9d50e7ebd13253e07244e04ffe5 Component: cli --- components/cli/cobraadaptor/adaptor.go | 6 ++++++ components/cli/usage.go | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index 4d2958b531..6f1a8876b3 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -5,7 +5,10 @@ import ( "github.com/docker/docker/api/client/container" "github.com/docker/docker/api/client/image" "github.com/docker/docker/api/client/network" + "github.com/docker/docker/api/client/node" "github.com/docker/docker/api/client/registry" + "github.com/docker/docker/api/client/service" + "github.com/docker/docker/api/client/swarm" "github.com/docker/docker/api/client/system" "github.com/docker/docker/api/client/volume" "github.com/docker/docker/cli" @@ -36,6 +39,9 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { rootCmd.SetFlagErrorFunc(cli.FlagErrorFunc) rootCmd.SetOutput(stdout) rootCmd.AddCommand( + node.NewNodeCommand(dockerCli), + service.NewServiceCommand(dockerCli), + swarm.NewSwarmCommand(dockerCli), container.NewAttachCommand(dockerCli), container.NewCommitCommand(dockerCli), container.NewCreateCommand(dockerCli), diff --git a/components/cli/usage.go b/components/cli/usage.go index 73fa4f2245..3c3b321be6 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -11,7 +11,7 @@ var DockerCommandUsage = []Command{ {"cp", "Copy files/folders between a container and the local filesystem"}, {"exec", "Run a command in a running container"}, {"info", "Display system-wide information"}, - {"inspect", "Return low-level information on a container or image"}, + {"inspect", "Return low-level information on a container, image or task"}, {"update", "Update configuration of one or more containers"}, } From a9bb747fe8de6a6c7c0d7cb57e062674b25715a0 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Tue, 14 Jun 2016 17:16:59 +0200 Subject: [PATCH 089/978] Migrate cp command to cobra Signed-off-by: Vincent Demeester Upstream-commit: 50626d2b2bf15bf925dcd235a9079fab8fda7eb7 Component: cli --- components/cli/cobraadaptor/adaptor.go | 1 + components/cli/usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index 6f1a8876b3..55538dd98f 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -44,6 +44,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { swarm.NewSwarmCommand(dockerCli), container.NewAttachCommand(dockerCli), container.NewCommitCommand(dockerCli), + container.NewCopyCommand(dockerCli), container.NewCreateCommand(dockerCli), container.NewDiffCommand(dockerCli), container.NewExportCommand(dockerCli), diff --git a/components/cli/usage.go b/components/cli/usage.go index 3c3b321be6..0e2923740f 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -8,7 +8,6 @@ type Command struct { // DockerCommandUsage lists the top level docker commands and their short usage var DockerCommandUsage = []Command{ - {"cp", "Copy files/folders between a container and the local filesystem"}, {"exec", "Run a command in a running container"}, {"info", "Display system-wide information"}, {"inspect", "Return low-level information on a container, image or task"}, From d3a41b659b65f8fe99a74b25753176614fc46b59 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 8 Jun 2016 13:47:46 -0400 Subject: [PATCH 090/978] Add experimental docker stack commands Signed-off-by: Daniel Nephin Upstream-commit: 3fe470656a2bd5c1002f328cd98aca91f52af2c4 Component: cli --- components/cli/cobraadaptor/adaptor.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index 55538dd98f..44d0a5e00c 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -8,6 +8,7 @@ import ( "github.com/docker/docker/api/client/node" "github.com/docker/docker/api/client/registry" "github.com/docker/docker/api/client/service" + "github.com/docker/docker/api/client/stack" "github.com/docker/docker/api/client/swarm" "github.com/docker/docker/api/client/system" "github.com/docker/docker/api/client/volume" @@ -41,6 +42,8 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { rootCmd.AddCommand( node.NewNodeCommand(dockerCli), service.NewServiceCommand(dockerCli), + stack.NewStackCommand(dockerCli), + stack.NewTopLevelDeployCommand(dockerCli), swarm.NewSwarmCommand(dockerCli), container.NewAttachCommand(dockerCli), container.NewCommitCommand(dockerCli), @@ -96,7 +99,9 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { func (c CobraAdaptor) Usage() []cli.Command { cmds := []cli.Command{} for _, cmd := range c.rootCmd.Commands() { - cmds = append(cmds, cli.Command{Name: cmd.Name(), Description: cmd.Short}) + if cmd.Name() != "" { + cmds = append(cmds, cli.Command{Name: cmd.Name(), Description: cmd.Short}) + } } return cmds } From 9dfb7344799169d3140f65e4010b409863770b97 Mon Sep 17 00:00:00 2001 From: Tibor Vass Date: Mon, 16 May 2016 11:50:55 -0400 Subject: [PATCH 091/978] plugins: experimental support for new plugin management This patch introduces a new experimental engine-level plugin management with a new API and command line. Plugins can be distributed via a Docker registry, and their lifecycle is managed by the engine. This makes plugins a first-class construct. For more background, have a look at issue #20363. Documentation is in a separate commit. If you want to understand how the new plugin system works, you can start by reading the documentation. Note: backwards compatibility with existing plugins is maintained, albeit they won't benefit from the advantages of the new system. Signed-off-by: Tibor Vass Signed-off-by: Anusha Ragunathan Upstream-commit: 11c8c6c8fcfa5ebf8d918962020d1b171328186f Component: cli --- components/cli/cobraadaptor/adaptor.go | 2 ++ components/cli/error.go | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 components/cli/error.go diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index 55538dd98f..a87c75206a 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -6,6 +6,7 @@ import ( "github.com/docker/docker/api/client/image" "github.com/docker/docker/api/client/network" "github.com/docker/docker/api/client/node" + "github.com/docker/docker/api/client/plugin" "github.com/docker/docker/api/client/registry" "github.com/docker/docker/api/client/service" "github.com/docker/docker/api/client/swarm" @@ -81,6 +82,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { system.NewVersionCommand(dockerCli), volume.NewVolumeCommand(dockerCli), ) + plugin.NewPluginCommand(rootCmd, dockerCli) rootCmd.PersistentFlags().BoolP("help", "h", false, "Print usage") rootCmd.PersistentFlags().MarkShorthandDeprecated("help", "please use --help") diff --git a/components/cli/error.go b/components/cli/error.go new file mode 100644 index 0000000000..902d1b6e49 --- /dev/null +++ b/components/cli/error.go @@ -0,0 +1,21 @@ +package cli + +import "bytes" + +// Errors is a list of errors. +// Useful in a loop if you don't want to return the error right away and you want to display after the loop, +// all the errors that happened during the loop. +type Errors []error + +func (errs Errors) Error() string { + if len(errs) < 1 { + return "" + } + var buf bytes.Buffer + buf.WriteString(errs[0].Error()) + for _, err := range errs[1:] { + buf.WriteString(", ") + buf.WriteString(err.Error()) + } + return buf.String() +} From b25f2d76d281f2f975897fd8bcef8601f03cb624 Mon Sep 17 00:00:00 2001 From: Anusha Ragunathan Date: Wed, 15 Jun 2016 09:17:05 -0700 Subject: [PATCH 092/978] Avoid back and forth conversion between strings and bytes. Signed-off-by: Anusha Ragunathan Upstream-commit: 63bccf7f313697ef76fb924feac741bf808340e1 Component: cli --- components/cli/error.go | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/components/cli/error.go b/components/cli/error.go index 902d1b6e49..e421c7f7c7 100644 --- a/components/cli/error.go +++ b/components/cli/error.go @@ -1,21 +1,20 @@ package cli -import "bytes" +import "strings" // Errors is a list of errors. // Useful in a loop if you don't want to return the error right away and you want to display after the loop, // all the errors that happened during the loop. type Errors []error -func (errs Errors) Error() string { - if len(errs) < 1 { +func (errList Errors) Error() string { + if len(errList) < 1 { return "" } - var buf bytes.Buffer - buf.WriteString(errs[0].Error()) - for _, err := range errs[1:] { - buf.WriteString(", ") - buf.WriteString(err.Error()) + + out := make([]string, len(errList)) + for i := range errList { + out[i] = errList[i].Error() } - return buf.String() + return strings.Join(out, ", ") } From 3c8b37971d616a509df27aaf534310cdc5bd1a62 Mon Sep 17 00:00:00 2001 From: Tomasz Kopczynski Date: Wed, 8 Jun 2016 23:42:25 +0200 Subject: [PATCH 093/978] Migrate info command to cobra Signed-off-by: Tomasz Kopczynski Upstream-commit: 91b49f8538b423648d4c93855e0dc0ce326bdfc3 Component: cli --- components/cli/cobraadaptor/adaptor.go | 1 + components/cli/usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index e8a29c6817..5466840310 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -84,6 +84,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { registry.NewLogoutCommand(dockerCli), system.NewVersionCommand(dockerCli), volume.NewVolumeCommand(dockerCli), + system.NewInfoCommand(dockerCli), ) plugin.NewPluginCommand(rootCmd, dockerCli) diff --git a/components/cli/usage.go b/components/cli/usage.go index 0e2923740f..451c5e7756 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -9,7 +9,6 @@ type Command struct { // DockerCommandUsage lists the top level docker commands and their short usage var DockerCommandUsage = []Command{ {"exec", "Run a command in a running container"}, - {"info", "Display system-wide information"}, {"inspect", "Return low-level information on a container, image or task"}, {"update", "Update configuration of one or more containers"}, } From 501e0f5cc5731f41babaea1cd9b0629933c1fa67 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 5 Jun 2016 13:55:00 -0700 Subject: [PATCH 094/978] Use spf13/cobra for docker update This fix is part of the effort to convert commands to spf13/cobra #23211. Thif fix coverted command `docker update` to use spf13/cobra Signed-off-by: Yong Tang Upstream-commit: 396d11999f37dd49c60cdcc6e4394a407ee5d340 Component: cli --- components/cli/cobraadaptor/adaptor.go | 1 + components/cli/usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index 5466840310..589e46c160 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -66,6 +66,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { container.NewStopCommand(dockerCli), container.NewTopCommand(dockerCli), container.NewUnpauseCommand(dockerCli), + container.NewUpdateCommand(dockerCli), container.NewWaitCommand(dockerCli), image.NewBuildCommand(dockerCli), image.NewHistoryCommand(dockerCli), diff --git a/components/cli/usage.go b/components/cli/usage.go index 451c5e7756..56e406e3a3 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -10,7 +10,6 @@ type Command struct { var DockerCommandUsage = []Command{ {"exec", "Run a command in a running container"}, {"inspect", "Return low-level information on a container, image or task"}, - {"update", "Update configuration of one or more containers"}, } // DockerCommands stores all the docker command From 3caf06c1220eb0b69d95d6ca63dc0916b69bde41 Mon Sep 17 00:00:00 2001 From: allencloud Date: Sun, 3 Jul 2016 20:47:39 +0800 Subject: [PATCH 095/978] fix typos Signed-off-by: allencloud Upstream-commit: 5545165e0298e9198821a07aaeb49f1ac8973816 Component: cli --- components/cli/required.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/required.go b/components/cli/required.go index 9276a5740a..8ee02c8429 100644 --- a/components/cli/required.go +++ b/components/cli/required.go @@ -7,7 +7,7 @@ import ( "github.com/spf13/cobra" ) -// NoArgs validate args and returns an error if there are any args +// NoArgs validates args and returns an error if there are any args func NoArgs(cmd *cobra.Command, args []string) error { if len(args) == 0 { return nil From 80a0e584b990f2fc8ba29cecd78c9cd961fa71fe Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sat, 16 Jul 2016 16:44:10 +0200 Subject: [PATCH 096/978] Don't automagically add "[OPTIONS]" to usage This removes the logic to automatically add [OPTIONS] to the usage output. The current logic was broken if a command only has deprecated or hidden flags, and in many cases put the [OPTIONS] in the wrong location. Requiring the usage string to be set manually gives more predictable results, and shouldn't require much to maintain. Signed-off-by: Sebastiaan van Stijn Upstream-commit: 6393b5fcc7506d6e3606c508a2d99ec28a4c3bf0 Component: cli --- components/cli/cli.go | 7 +------ components/cli/cobraadaptor/adaptor.go | 4 ++-- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/components/cli/cli.go b/components/cli/cli.go index f6d48d6fac..8d21cda69d 100644 --- a/components/cli/cli.go +++ b/components/cli/cli.go @@ -155,11 +155,6 @@ func Subcmd(name string, synopses []string, description string, exitOnError bool } flags.ShortUsage = func() { - options := "" - if flags.FlagCountUndeprecated() > 0 { - options = " [OPTIONS]" - } - if len(synopses) == 0 { synopses = []string{""} } @@ -176,7 +171,7 @@ func Subcmd(name string, synopses []string, description string, exitOnError bool synopsis = " " + synopsis } - fmt.Fprintf(flags.Out(), "\n%sdocker %s%s%s", lead, name, options, synopsis) + fmt.Fprintf(flags.Out(), "\n%sdocker %s%s", lead, name, synopsis) } fmt.Fprintf(flags.Out(), "\n\n%s\n", description) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index 589e46c160..f63307cb8b 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -32,7 +32,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { dockerCli := client.NewDockerCli(stdin, stdout, stderr, clientFlags) var rootCmd = &cobra.Command{ - Use: "docker", + Use: "docker [OPTIONS]", SilenceUsage: true, SilenceErrors: true, } @@ -131,7 +131,7 @@ func (c CobraAdaptor) Command(name string) func(...string) error { return nil } -var usageTemplate = `Usage: {{if not .HasSubCommands}}{{if .HasLocalFlags}}{{appendIfNotPresent .UseLine "[OPTIONS]"}}{{else}}{{.UseLine}}{{end}}{{end}}{{if .HasSubCommands}}{{ .CommandPath}} COMMAND{{end}} +var usageTemplate = `Usage: {{if not .HasSubCommands}}{{.UseLine}}{{end}}{{if .HasSubCommands}}{{ .CommandPath}} COMMAND{{end}} {{with or .Long .Short }}{{. | trim}}{{end}}{{if gt .Aliases 0}} From a303f0b6764027eea2b21fa3a52d98569e3df64f Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 9 Jun 2016 11:33:28 -0400 Subject: [PATCH 097/978] Add a script to generate man pages from cobra commands. Use the generate.sh script instead of md2man directly. Update Dockerfile for generating man pages. Signed-off-by: Daniel Nephin Upstream-commit: 6f66e15f990c58c46efaf73754ae94675e49b6dc Component: cli --- components/cli/cobraadaptor/adaptor.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index f63307cb8b..f2c64c9f47 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -33,6 +33,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { var rootCmd = &cobra.Command{ Use: "docker [OPTIONS]", + Short: "A self-sufficient runtime for containers", SilenceUsage: true, SilenceErrors: true, } @@ -131,9 +132,15 @@ func (c CobraAdaptor) Command(name string) func(...string) error { return nil } +// GetRootCommand returns the root command. Required to generate the man pages +// and reference docs from a script outside this package. +func (c CobraAdaptor) GetRootCommand() *cobra.Command { + return c.rootCmd +} + var usageTemplate = `Usage: {{if not .HasSubCommands}}{{.UseLine}}{{end}}{{if .HasSubCommands}}{{ .CommandPath}} COMMAND{{end}} -{{with or .Long .Short }}{{. | trim}}{{end}}{{if gt .Aliases 0}} +{{ .Short | trim }}{{if gt .Aliases 0}} Aliases: {{.NameAndAliases}}{{end}}{{if .HasExample}} From b878e00458282986fe7e0740d14ad609829f34aa Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Mon, 20 Jun 2016 13:27:56 +0000 Subject: [PATCH 098/978] Migrate exec command to cobra Signed-off-by: Akihiro Suda Upstream-commit: b32ff5a1cd1b0db2207e17a5a2325cce819b4ce1 Component: cli --- components/cli/cobraadaptor/adaptor.go | 1 + components/cli/usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index f2c64c9f47..6614f1fa69 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -52,6 +52,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { container.NewCopyCommand(dockerCli), container.NewCreateCommand(dockerCli), container.NewDiffCommand(dockerCli), + container.NewExecCommand(dockerCli), container.NewExportCommand(dockerCli), container.NewKillCommand(dockerCli), container.NewLogsCommand(dockerCli), diff --git a/components/cli/usage.go b/components/cli/usage.go index 56e406e3a3..0ad053d734 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -8,7 +8,6 @@ type Command struct { // DockerCommandUsage lists the top level docker commands and their short usage var DockerCommandUsage = []Command{ - {"exec", "Run a command in a running container"}, {"inspect", "Return low-level information on a container, image or task"}, } From 91dde0851f271e0584377d2e2f836de54b0f4743 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 23 Jun 2016 11:09:49 -0400 Subject: [PATCH 099/978] Convert inspect to cobra. Signed-off-by: Daniel Nephin Upstream-commit: 39c47a0e24d9de5e4048c9c9dc915f33382ee4c6 Component: cli --- components/cli/cobraadaptor/adaptor.go | 1 + components/cli/usage.go | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index 6614f1fa69..2df553bea1 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -83,6 +83,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { image.NewTagCommand(dockerCli), network.NewNetworkCommand(dockerCli), system.NewEventsCommand(dockerCli), + system.NewInspectCommand(dockerCli), registry.NewLoginCommand(dockerCli), registry.NewLogoutCommand(dockerCli), system.NewVersionCommand(dockerCli), diff --git a/components/cli/usage.go b/components/cli/usage.go index 0ad053d734..a8a0328643 100644 --- a/components/cli/usage.go +++ b/components/cli/usage.go @@ -7,9 +7,7 @@ type Command struct { } // DockerCommandUsage lists the top level docker commands and their short usage -var DockerCommandUsage = []Command{ - {"inspect", "Return low-level information on a container, image or task"}, -} +var DockerCommandUsage = []Command{} // DockerCommands stores all the docker command var DockerCommands = make(map[string]Command) From 88b6751cda7584a59f12091ec31057f63a705f6b Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 21 Jun 2016 16:42:47 -0400 Subject: [PATCH 100/978] Convert dockerd to use cobra and pflag Signed-off-by: Daniel Nephin Upstream-commit: 23dd85befd52f6d2cf25dbb7d8866bc6f72f50c8 Component: cli --- components/cli/docker.go | 1 + 1 file changed, 1 insertion(+) diff --git a/components/cli/docker.go b/components/cli/docker.go index 0c727e32c9..7346d913ef 100644 --- a/components/cli/docker.go +++ b/components/cli/docker.go @@ -101,6 +101,7 @@ func initClientFlags(commonFlags *cliflags.CommonFlags) *cliflags.ClientFlags { clientFlags.PostParse = func() { clientFlags.Common.PostParse() + cliflags.SetDaemonLogLevel(commonOpts.LogLevel) if clientFlags.ConfigDir != "" { cliconfig.SetConfigDir(clientFlags.ConfigDir) From 8d4a32d8bc99252e89d6d190ac23548dd4cf9a5c Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 21 Jun 2016 16:42:47 -0400 Subject: [PATCH 101/978] Convert dockerd to use cobra and pflag Signed-off-by: Daniel Nephin Upstream-commit: d08dd40a8882c2c9afb575b0f145fd29922153de Component: cli --- components/cli/flags/client.go | 8 ++-- components/cli/flags/common.go | 75 ++++++++++++++++------------------ 2 files changed, 40 insertions(+), 43 deletions(-) diff --git a/components/cli/flags/client.go b/components/cli/flags/client.go index cc7309db4b..eadbc143b7 100644 --- a/components/cli/flags/client.go +++ b/components/cli/flags/client.go @@ -1,11 +1,13 @@ package flags -import flag "github.com/docker/docker/pkg/mflag" +import ( + "github.com/spf13/pflag" +) // ClientFlags represents flags for the docker client. type ClientFlags struct { - FlagSet *flag.FlagSet - Common *CommonFlags + FlagSet *pflag.FlagSet + Common *CommonOptions PostParse func() ConfigDir string diff --git a/components/cli/flags/common.go b/components/cli/flags/common.go index 4726b04f2a..a3579bff6d 100644 --- a/components/cli/flags/common.go +++ b/components/cli/flags/common.go @@ -8,8 +8,8 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/docker/cliconfig" "github.com/docker/docker/opts" - flag "github.com/docker/docker/pkg/mflag" "github.com/docker/go-connections/tlsconfig" + "github.com/spf13/pflag" ) const ( @@ -21,8 +21,8 @@ const ( DefaultKeyFile = "key.pem" // DefaultCertFile is the default filename for the cert pem file DefaultCertFile = "cert.pem" - // TLSVerifyKey is the default flag name for the tls verification option - TLSVerifyKey = "tlsverify" + // FlagTLSVerify is the flag name for the tls verification option + FlagTLSVerify = "tlsverify" ) var ( @@ -30,11 +30,8 @@ var ( dockerTLSVerify = os.Getenv("DOCKER_TLS_VERIFY") != "" ) -// CommonFlags are flags common to both the client and the daemon. -type CommonFlags struct { - FlagSet *flag.FlagSet - PostParse func() - +// CommonOptions are options common to both the client and the daemon. +type CommonOptions struct { Debug bool Hosts []string LogLevel string @@ -44,62 +41,60 @@ type CommonFlags struct { TrustKey string } -// InitCommonFlags initializes flags common to both client and daemon -func InitCommonFlags() *CommonFlags { - var commonFlags = &CommonFlags{FlagSet: new(flag.FlagSet)} +// NewCommonOptions returns a new CommonOptions +func NewCommonOptions() *CommonOptions { + return &CommonOptions{ + TLSOptions: &tlsconfig.Options{}, + } +} +// InstallFlags adds flags for the common options on the FlagSet +func (commonOpts *CommonOptions) InstallFlags(flags *pflag.FlagSet) { if dockerCertPath == "" { dockerCertPath = cliconfig.ConfigDir() } - commonFlags.PostParse = func() { postParseCommon(commonFlags) } + flags.BoolVarP(&commonOpts.Debug, "debug", "D", false, "Enable debug mode") + flags.StringVarP(&commonOpts.LogLevel, "log-level", "l", "info", "Set the logging level") + flags.BoolVar(&commonOpts.TLS, "tls", false, "Use TLS; implied by --tlsverify") + flags.BoolVar(&commonOpts.TLSVerify, FlagTLSVerify, dockerTLSVerify, "Use TLS and verify the remote") - cmd := commonFlags.FlagSet + // TODO use flag flags.String("identity"}, "i", "", "Path to libtrust key file") - cmd.BoolVar(&commonFlags.Debug, []string{"D", "-debug"}, false, "Enable debug mode") - cmd.StringVar(&commonFlags.LogLevel, []string{"l", "-log-level"}, "info", "Set the logging level") - cmd.BoolVar(&commonFlags.TLS, []string{"-tls"}, false, "Use TLS; implied by --tlsverify") - cmd.BoolVar(&commonFlags.TLSVerify, []string{"-tlsverify"}, dockerTLSVerify, "Use TLS and verify the remote") + tlsOptions := commonOpts.TLSOptions + flags.StringVar(&tlsOptions.CAFile, "tlscacert", filepath.Join(dockerCertPath, DefaultCaFile), "Trust certs signed only by this CA") + flags.StringVar(&tlsOptions.CertFile, "tlscert", filepath.Join(dockerCertPath, DefaultCertFile), "Path to TLS certificate file") + flags.StringVar(&tlsOptions.KeyFile, "tlskey", filepath.Join(dockerCertPath, DefaultKeyFile), "Path to TLS key file") - // TODO use flag flag.String([]string{"i", "-identity"}, "", "Path to libtrust key file") - - var tlsOptions tlsconfig.Options - commonFlags.TLSOptions = &tlsOptions - cmd.StringVar(&tlsOptions.CAFile, []string{"-tlscacert"}, filepath.Join(dockerCertPath, DefaultCaFile), "Trust certs signed only by this CA") - cmd.StringVar(&tlsOptions.CertFile, []string{"-tlscert"}, filepath.Join(dockerCertPath, DefaultCertFile), "Path to TLS certificate file") - cmd.StringVar(&tlsOptions.KeyFile, []string{"-tlskey"}, filepath.Join(dockerCertPath, DefaultKeyFile), "Path to TLS key file") - - cmd.Var(opts.NewNamedListOptsRef("hosts", &commonFlags.Hosts, opts.ValidateHost), []string{"H", "-host"}, "Daemon socket(s) to connect to") - return commonFlags + hostOpt := opts.NewNamedListOptsRef("hosts", &commonOpts.Hosts, opts.ValidateHost) + flags.VarP(hostOpt, "-host", "H", "Daemon socket(s) to connect to") } -func postParseCommon(commonFlags *CommonFlags) { - cmd := commonFlags.FlagSet - - SetDaemonLogLevel(commonFlags.LogLevel) - +// SetDefaultOptions sets default values for options after flag parsing is +// complete +func (commonOpts *CommonOptions) SetDefaultOptions(flags *pflag.FlagSet) { // Regardless of whether the user sets it to true or false, if they // specify --tlsverify at all then we need to turn on tls // TLSVerify can be true even if not set due to DOCKER_TLS_VERIFY env var, so we need // to check that here as well - if cmd.IsSet("-"+TLSVerifyKey) || commonFlags.TLSVerify { - commonFlags.TLS = true + if flags.Changed(FlagTLSVerify) || commonOpts.TLSVerify { + commonOpts.TLS = true } - if !commonFlags.TLS { - commonFlags.TLSOptions = nil + if !commonOpts.TLS { + commonOpts.TLSOptions = nil } else { - tlsOptions := commonFlags.TLSOptions - tlsOptions.InsecureSkipVerify = !commonFlags.TLSVerify + tlsOptions := commonOpts.TLSOptions + tlsOptions.InsecureSkipVerify = !commonOpts.TLSVerify // Reset CertFile and KeyFile to empty string if the user did not specify // the respective flags and the respective default files were not found. - if !cmd.IsSet("-tlscert") { + if !flags.Changed("tlscert") { if _, err := os.Stat(tlsOptions.CertFile); os.IsNotExist(err) { tlsOptions.CertFile = "" } } - if !cmd.IsSet("-tlskey") { + if !flags.Changed("tlskey") { if _, err := os.Stat(tlsOptions.KeyFile); os.IsNotExist(err) { tlsOptions.KeyFile = "" } From c526dbb69a7c900a17ca273e974ceffe8fa93d8b Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 22 Jun 2016 13:08:04 -0400 Subject: [PATCH 102/978] Convert docker root command to use pflag and cobra Fix the daemon proxy for cobra commands. Signed-off-by: Daniel Nephin Upstream-commit: 08784d7e0ec0e398c2eff368ae2d727ed1de4ef3 Component: cli --- components/cli/daemon.go | 18 ------ components/cli/daemon_none.go | 14 +++- components/cli/daemon_unix.go | 40 +++++++++--- components/cli/docker.go | 118 +++++++++++++++------------------- components/cli/docker_test.go | 12 +++- 5 files changed, 103 insertions(+), 99 deletions(-) delete mode 100644 components/cli/daemon.go diff --git a/components/cli/daemon.go b/components/cli/daemon.go deleted file mode 100644 index 8fe3484761..0000000000 --- a/components/cli/daemon.go +++ /dev/null @@ -1,18 +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{} -} - -// Command returns a cli command handler if one exists -func (p DaemonProxy) Command(name string) func(...string) error { - return map[string]func(...string) error{ - "daemon": p.CmdDaemon, - }[name] -} diff --git a/components/cli/daemon_none.go b/components/cli/daemon_none.go index d66bf1a546..c57896ed71 100644 --- a/components/cli/daemon_none.go +++ b/components/cli/daemon_none.go @@ -4,12 +4,22 @@ package main import ( "fmt" + "github.com/spf13/cobra" "runtime" "strings" ) -// CmdDaemon reports on an error on windows, because there is no exec -func (p DaemonProxy) CmdDaemon(args ...string) error { +func newDaemonCommand() *cobra.Command { + return &cobra.Command{ + Use: "daemon", + Hidden: true, + RunE: func(cmd *cobra.Command, args []string) error { + return runDaemon() + }, + } +} + +func runDaemon() error { return fmt.Errorf( "`docker daemon` is not supported on %s. Please run `dockerd` directly", strings.Title(runtime.GOOS)) diff --git a/components/cli/daemon_unix.go b/components/cli/daemon_unix.go index d515b82914..30a40a8611 100644 --- a/components/cli/daemon_unix.go +++ b/components/cli/daemon_unix.go @@ -3,23 +3,37 @@ package main import ( + "fmt" + "os" "os/exec" "path/filepath" "syscall" + + "github.com/spf13/cobra" ) -// CmdDaemon execs dockerd with the same flags -func (p DaemonProxy) CmdDaemon(args ...string) error { - // Special case for handling `docker help daemon`. When pkg/mflag is removed - // we can support this on the daemon side, but that is not possible with - // pkg/mflag because it uses os.Exit(1) instead of returning an error on - // unexpected args. - if len(args) == 0 || args[0] != "--help" { - // Use os.Args[1:] so that "global" args are passed to dockerd - args = stripDaemonArg(os.Args[1:]) - } +const daemonBinary = "dockerd" +func newDaemonCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "daemon", + Hidden: true, + RunE: func(cmd *cobra.Command, args []string) error { + return runDaemon() + }, + } + cmd.SetHelpFunc(helpFunc) + return cmd +} + +// CmdDaemon execs dockerd with the same flags +func runDaemon() error { + // Use os.Args[1:] so that "global" args are passed to dockerd + return execDaemon(stripDaemonArg(os.Args[1:])) +} + +func execDaemon(args []string) error { binaryPath, err := findDaemonBinary() if err != nil { return err @@ -31,6 +45,12 @@ func (p DaemonProxy) CmdDaemon(args ...string) error { os.Environ()) } +func helpFunc(cmd *cobra.Command, args []string) { + if err := execDaemon([]string{"--help"}); err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) + } +} + // findDaemonBinary looks for the path to the dockerd binary starting with // the directory of the current executable (if one exists) and followed by $PATH func findDaemonBinary() (string, error) { diff --git a/components/cli/docker.go b/components/cli/docker.go index 7346d913ef..bd04a3a1bd 100644 --- a/components/cli/docker.go +++ b/components/cli/docker.go @@ -12,64 +12,52 @@ import ( cliflags "github.com/docker/docker/cli/flags" "github.com/docker/docker/cliconfig" "github.com/docker/docker/dockerversion" - flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/pkg/term" "github.com/docker/docker/utils" + "github.com/spf13/cobra" + "github.com/spf13/pflag" ) -var ( - commonFlags = cliflags.InitCommonFlags() - clientFlags = initClientFlags(commonFlags) - flHelp = flag.Bool([]string{"h", "-help"}, false, "Print usage") - flVersion = flag.Bool([]string{"v", "-version"}, false, "Print version information and quit") -) +func newDockerCommand(dockerCli *client.DockerCli, opts *cliflags.ClientOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: "docker [OPTIONS] COMMAND [arg...]", + Short: "A self-sufficient runtime for containers.", + SilenceUsage: true, + SilenceErrors: true, + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if opts.Version { + showVersion() + return nil + } + fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + return nil + }, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + dockerPreRun(cmd.Flags(), opts) + return dockerCli.Initialize(opts) + }, + } + cobraadaptor.SetupRootCommand(cmd, dockerCli) + + flags := cmd.Flags() + flags.BoolVarP(&opts.Version, "version", "v", false, "Print version information and quit") + flags.StringVar(&opts.ConfigDir, "config", cliconfig.ConfigDir(), "Location of client config files") + opts.Common.InstallFlags(flags) + + return cmd +} 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) + opts := cliflags.NewClientOptions() + dockerCli := client.NewDockerCli(stdin, stdout, stderr, opts) + cmd := newDockerCommand(dockerCli, opts) - cobraAdaptor := cobraadaptor.NewCobraAdaptor(clientFlags) - - 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" - - dockerCommands := append(cli.DockerCommandUsage, cobraAdaptor.Usage()...) - for _, cmd := range sortCommands(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(), cobraAdaptor) - if err := c.Run(flag.Args()...); err != nil { + if err := cmd.Execute(); err != nil { if sterr, ok := err.(cli.StatusError); ok { if sterr.Status != "" { fmt.Fprintln(stderr, sterr.Status) @@ -94,26 +82,22 @@ func showVersion() { } } -func initClientFlags(commonFlags *cliflags.CommonFlags) *cliflags.ClientFlags { - clientFlags := &cliflags.ClientFlags{FlagSet: new(flag.FlagSet), Common: commonFlags} - client := clientFlags.FlagSet - client.StringVar(&clientFlags.ConfigDir, []string{"-config"}, cliconfig.ConfigDir(), "Location of client config files") +func dockerPreRun(flags *pflag.FlagSet, opts *cliflags.ClientOptions) { + opts.Common.SetDefaultOptions(flags) + cliflags.SetDaemonLogLevel(opts.Common.LogLevel) - clientFlags.PostParse = func() { - clientFlags.Common.PostParse() - cliflags.SetDaemonLogLevel(commonOpts.LogLevel) - - 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() - } + // TODO: remove this, set a default in New, and pass it in opts + if opts.ConfigDir != "" { + cliconfig.SetConfigDir(opts.ConfigDir) + } + + if opts.Common.TrustKey == "" { + opts.Common.TrustKey = filepath.Join( + cliconfig.ConfigDir(), + cliflags.DefaultTrustKeyFile) + } + + if opts.Common.Debug { + utils.EnableDebug() } - return clientFlags } diff --git a/components/cli/docker_test.go b/components/cli/docker_test.go index 5708c96cb5..a1e84f1396 100644 --- a/components/cli/docker_test.go +++ b/components/cli/docker_test.go @@ -6,13 +6,21 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/docker/utils" + + "github.com/docker/docker/api/client" + cliflags "github.com/docker/docker/cli/flags" ) func TestClientDebugEnabled(t *testing.T) { defer utils.DisableDebug() - clientFlags.Common.FlagSet.Parse([]string{"-D"}) - clientFlags.PostParse() + opts := cliflags.NewClientOptions() + cmd := newDockerCommand(&client.DockerCli{}, opts) + + opts.Common.Debug = true + if err := cmd.PersistentPreRunE(cmd, []string{}); err != nil { + t.Fatalf("Unexpected error: %s", err.Error()) + } if os.Getenv("DEBUG") != "1" { t.Fatal("expected debug enabled, got false") From 93466186ca96bc763daccb4fe0f5e2c059f02377 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 22 Jun 2016 13:08:04 -0400 Subject: [PATCH 103/978] Convert docker root command to use pflag and cobra Fix the daemon proxy for cobra commands. Signed-off-by: Daniel Nephin Upstream-commit: 82a8cc1556c1aa5be2ceb0406d7828b8360d0be6 Component: cli --- components/cli/cobraadaptor/adaptor.go | 65 +++----------------------- components/cli/flags/client.go | 17 ++++--- 2 files changed, 14 insertions(+), 68 deletions(-) diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index 2df553bea1..d7747351c6 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -14,33 +14,18 @@ import ( "github.com/docker/docker/api/client/system" "github.com/docker/docker/api/client/volume" "github.com/docker/docker/cli" - cliflags "github.com/docker/docker/cli/flags" - "github.com/docker/docker/pkg/term" "github.com/spf13/cobra" ) -// CobraAdaptor is an adaptor for supporting spf13/cobra commands in the -// docker/cli framework -type CobraAdaptor struct { - rootCmd *cobra.Command - dockerCli *client.DockerCli -} - -// NewCobraAdaptor returns a new handler -func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { - stdin, stdout, stderr := term.StdStreams() - dockerCli := client.NewDockerCli(stdin, stdout, stderr, clientFlags) - - var rootCmd = &cobra.Command{ - Use: "docker [OPTIONS]", - Short: "A self-sufficient runtime for containers", - SilenceUsage: true, - SilenceErrors: true, - } +// SetupRootCommand sets default usage, help, and error handling for the +// root command. +// TODO: move to cmd/docker/docker? +// TODO: split into common setup and client setup +func SetupRootCommand(rootCmd *cobra.Command, dockerCli *client.DockerCli) { rootCmd.SetUsageTemplate(usageTemplate) rootCmd.SetHelpTemplate(helpTemplate) rootCmd.SetFlagErrorFunc(cli.FlagErrorFunc) - rootCmd.SetOutput(stdout) + rootCmd.SetOutput(dockerCli.Out()) rootCmd.AddCommand( node.NewNodeCommand(dockerCli), service.NewServiceCommand(dockerCli), @@ -94,44 +79,6 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { rootCmd.PersistentFlags().BoolP("help", "h", false, "Print usage") rootCmd.PersistentFlags().MarkShorthandDeprecated("help", "please use --help") - - return CobraAdaptor{ - rootCmd: rootCmd, - dockerCli: dockerCli, - } -} - -// Usage returns the list of commands and their short usage string for -// all top level cobra commands. -func (c CobraAdaptor) Usage() []cli.Command { - cmds := []cli.Command{} - for _, cmd := range c.rootCmd.Commands() { - if cmd.Name() != "" { - cmds = append(cmds, cli.Command{Name: cmd.Name(), Description: cmd.Short}) - } - } - return cmds -} - -func (c CobraAdaptor) run(cmd string, args []string) error { - if err := c.dockerCli.Initialize(); err != nil { - return err - } - // Prepend the command name to support normal cobra command delegation - c.rootCmd.SetArgs(append([]string{cmd}, args...)) - return c.rootCmd.Execute() -} - -// Command returns a cli command handler if one exists -func (c CobraAdaptor) Command(name string) func(...string) error { - for _, cmd := range c.rootCmd.Commands() { - if cmd.Name() == name { - return func(args ...string) error { - return c.run(name, args) - } - } - } - return nil } // GetRootCommand returns the root command. Required to generate the man pages diff --git a/components/cli/flags/client.go b/components/cli/flags/client.go index eadbc143b7..9b6940f6bd 100644 --- a/components/cli/flags/client.go +++ b/components/cli/flags/client.go @@ -1,14 +1,13 @@ package flags -import ( - "github.com/spf13/pflag" -) - -// ClientFlags represents flags for the docker client. -type ClientFlags struct { - FlagSet *pflag.FlagSet +// ClientOptions are the options used to configure the client cli +type ClientOptions struct { Common *CommonOptions - PostParse func() - ConfigDir string + Version bool +} + +// NewClientOptions returns a new ClientOptions +func NewClientOptions() *ClientOptions { + return &ClientOptions{Common: NewCommonOptions()} } From 7e9f26ef705cd4c338b76571dedd3241252b0efe Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 22 Jun 2016 18:36:51 -0400 Subject: [PATCH 104/978] Update unit tests for new cobra root command. Cleanup cobra integration Update windows files for cobra and pflags Cleanup SetupRootcmd, and remove unnecessary SetFlagErrorFunc. Use cobra command traversal Signed-off-by: Daniel Nephin Upstream-commit: 58a14cd18cb99ccf02a615e8f87aad99f64b8a42 Component: cli --- components/cli/docker.go | 93 ++++++++++++++++++++++++++++------- components/cli/docker_test.go | 6 +-- 2 files changed, 77 insertions(+), 22 deletions(-) diff --git a/components/cli/docker.go b/components/cli/docker.go index bd04a3a1bd..0ae3906abb 100644 --- a/components/cli/docker.go +++ b/components/cli/docker.go @@ -3,10 +3,20 @@ package main import ( "fmt" "os" - "path/filepath" "github.com/Sirupsen/logrus" "github.com/docker/docker/api/client" + "github.com/docker/docker/api/client/container" + "github.com/docker/docker/api/client/image" + "github.com/docker/docker/api/client/network" + "github.com/docker/docker/api/client/node" + "github.com/docker/docker/api/client/plugin" + "github.com/docker/docker/api/client/registry" + "github.com/docker/docker/api/client/service" + "github.com/docker/docker/api/client/stack" + "github.com/docker/docker/api/client/swarm" + "github.com/docker/docker/api/client/system" + "github.com/docker/docker/api/client/volume" "github.com/docker/docker/cli" "github.com/docker/docker/cli/cobraadaptor" cliflags "github.com/docker/docker/cli/flags" @@ -18,13 +28,15 @@ import ( "github.com/spf13/pflag" ) -func newDockerCommand(dockerCli *client.DockerCli, opts *cliflags.ClientOptions) *cobra.Command { +func newDockerCommand(dockerCli *client.DockerCli) *cobra.Command { + opts := cliflags.NewClientOptions() cmd := &cobra.Command{ - Use: "docker [OPTIONS] COMMAND [arg...]", - Short: "A self-sufficient runtime for containers.", - SilenceUsage: true, - SilenceErrors: true, - Args: cli.NoArgs, + Use: "docker [OPTIONS] COMMAND [arg...]", + Short: "A self-sufficient runtime for containers.", + SilenceUsage: true, + SilenceErrors: true, + TraverseChildren: true, + Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { if opts.Version { showVersion() @@ -38,13 +50,66 @@ func newDockerCommand(dockerCli *client.DockerCli, opts *cliflags.ClientOptions) return dockerCli.Initialize(opts) }, } - cobraadaptor.SetupRootCommand(cmd, dockerCli) + cobraadaptor.SetupRootCommand(cmd) flags := cmd.Flags() flags.BoolVarP(&opts.Version, "version", "v", false, "Print version information and quit") flags.StringVar(&opts.ConfigDir, "config", cliconfig.ConfigDir(), "Location of client config files") opts.Common.InstallFlags(flags) + cmd.SetOutput(dockerCli.Out()) + cmd.AddCommand( + newDaemonCommand(), + node.NewNodeCommand(dockerCli), + service.NewServiceCommand(dockerCli), + stack.NewStackCommand(dockerCli), + stack.NewTopLevelDeployCommand(dockerCli), + swarm.NewSwarmCommand(dockerCli), + container.NewAttachCommand(dockerCli), + container.NewCommitCommand(dockerCli), + container.NewCopyCommand(dockerCli), + container.NewCreateCommand(dockerCli), + container.NewDiffCommand(dockerCli), + container.NewExecCommand(dockerCli), + container.NewExportCommand(dockerCli), + container.NewKillCommand(dockerCli), + container.NewLogsCommand(dockerCli), + container.NewPauseCommand(dockerCli), + container.NewPortCommand(dockerCli), + container.NewPsCommand(dockerCli), + container.NewRenameCommand(dockerCli), + container.NewRestartCommand(dockerCli), + container.NewRmCommand(dockerCli), + container.NewRunCommand(dockerCli), + container.NewStartCommand(dockerCli), + container.NewStatsCommand(dockerCli), + container.NewStopCommand(dockerCli), + container.NewTopCommand(dockerCli), + container.NewUnpauseCommand(dockerCli), + container.NewUpdateCommand(dockerCli), + container.NewWaitCommand(dockerCli), + image.NewBuildCommand(dockerCli), + image.NewHistoryCommand(dockerCli), + image.NewImagesCommand(dockerCli), + image.NewLoadCommand(dockerCli), + image.NewRemoveCommand(dockerCli), + image.NewSaveCommand(dockerCli), + image.NewPullCommand(dockerCli), + image.NewPushCommand(dockerCli), + image.NewSearchCommand(dockerCli), + image.NewImportCommand(dockerCli), + image.NewTagCommand(dockerCli), + network.NewNetworkCommand(dockerCli), + system.NewEventsCommand(dockerCli), + system.NewInspectCommand(dockerCli), + registry.NewLoginCommand(dockerCli), + registry.NewLogoutCommand(dockerCli), + system.NewVersionCommand(dockerCli), + volume.NewVolumeCommand(dockerCli), + system.NewInfoCommand(dockerCli), + ) + plugin.NewPluginCommand(cmd, dockerCli) + return cmd } @@ -53,9 +118,8 @@ func main() { stdin, stdout, stderr := term.StdStreams() logrus.SetOutput(stderr) - opts := cliflags.NewClientOptions() - dockerCli := client.NewDockerCli(stdin, stdout, stderr, opts) - cmd := newDockerCommand(dockerCli, opts) + dockerCli := client.NewDockerCli(stdin, stdout, stderr) + cmd := newDockerCommand(dockerCli) if err := cmd.Execute(); err != nil { if sterr, ok := err.(cli.StatusError); ok { @@ -86,17 +150,10 @@ func dockerPreRun(flags *pflag.FlagSet, opts *cliflags.ClientOptions) { opts.Common.SetDefaultOptions(flags) cliflags.SetDaemonLogLevel(opts.Common.LogLevel) - // TODO: remove this, set a default in New, and pass it in opts if opts.ConfigDir != "" { cliconfig.SetConfigDir(opts.ConfigDir) } - if opts.Common.TrustKey == "" { - opts.Common.TrustKey = filepath.Join( - cliconfig.ConfigDir(), - cliflags.DefaultTrustKeyFile) - } - if opts.Common.Debug { utils.EnableDebug() } diff --git a/components/cli/docker_test.go b/components/cli/docker_test.go index a1e84f1396..72d2311521 100644 --- a/components/cli/docker_test.go +++ b/components/cli/docker_test.go @@ -8,16 +8,14 @@ import ( "github.com/docker/docker/utils" "github.com/docker/docker/api/client" - cliflags "github.com/docker/docker/cli/flags" ) func TestClientDebugEnabled(t *testing.T) { defer utils.DisableDebug() - opts := cliflags.NewClientOptions() - cmd := newDockerCommand(&client.DockerCli{}, opts) + cmd := newDockerCommand(&client.DockerCli{}) + cmd.Flags().Set("debug", "true") - opts.Common.Debug = true if err := cmd.PersistentPreRunE(cmd, []string{}); err != nil { t.Fatalf("Unexpected error: %s", err.Error()) } From fbca637dc35d620d182d5d33a2363b4a0e2e2abb Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 22 Jun 2016 18:36:51 -0400 Subject: [PATCH 105/978] Update unit tests for new cobra root command. Cleanup cobra integration Update windows files for cobra and pflags Cleanup SetupRootcmd, and remove unnecessary SetFlagErrorFunc. Use cobra command traversal Signed-off-by: Daniel Nephin Upstream-commit: fc1a3d79f8a2e8b31532c3736a59f00b8035100e Component: cli --- components/cli/cobraadaptor/adaptor.go | 86 ++++++-------------------- components/cli/flagerrors.go | 21 ------- components/cli/flags/common.go | 7 +-- 3 files changed, 21 insertions(+), 93 deletions(-) delete mode 100644 components/cli/flagerrors.go diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobraadaptor/adaptor.go index d7747351c6..67263e1577 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobraadaptor/adaptor.go @@ -1,81 +1,17 @@ package cobraadaptor import ( - "github.com/docker/docker/api/client" - "github.com/docker/docker/api/client/container" - "github.com/docker/docker/api/client/image" - "github.com/docker/docker/api/client/network" - "github.com/docker/docker/api/client/node" - "github.com/docker/docker/api/client/plugin" - "github.com/docker/docker/api/client/registry" - "github.com/docker/docker/api/client/service" - "github.com/docker/docker/api/client/stack" - "github.com/docker/docker/api/client/swarm" - "github.com/docker/docker/api/client/system" - "github.com/docker/docker/api/client/volume" - "github.com/docker/docker/cli" + "fmt" + "github.com/spf13/cobra" ) // SetupRootCommand sets default usage, help, and error handling for the // root command. -// TODO: move to cmd/docker/docker? -// TODO: split into common setup and client setup -func SetupRootCommand(rootCmd *cobra.Command, dockerCli *client.DockerCli) { +func SetupRootCommand(rootCmd *cobra.Command) { rootCmd.SetUsageTemplate(usageTemplate) rootCmd.SetHelpTemplate(helpTemplate) - rootCmd.SetFlagErrorFunc(cli.FlagErrorFunc) - rootCmd.SetOutput(dockerCli.Out()) - rootCmd.AddCommand( - node.NewNodeCommand(dockerCli), - service.NewServiceCommand(dockerCli), - stack.NewStackCommand(dockerCli), - stack.NewTopLevelDeployCommand(dockerCli), - swarm.NewSwarmCommand(dockerCli), - container.NewAttachCommand(dockerCli), - container.NewCommitCommand(dockerCli), - container.NewCopyCommand(dockerCli), - container.NewCreateCommand(dockerCli), - container.NewDiffCommand(dockerCli), - container.NewExecCommand(dockerCli), - container.NewExportCommand(dockerCli), - container.NewKillCommand(dockerCli), - container.NewLogsCommand(dockerCli), - container.NewPauseCommand(dockerCli), - container.NewPortCommand(dockerCli), - container.NewPsCommand(dockerCli), - container.NewRenameCommand(dockerCli), - container.NewRestartCommand(dockerCli), - container.NewRmCommand(dockerCli), - container.NewRunCommand(dockerCli), - container.NewStartCommand(dockerCli), - container.NewStatsCommand(dockerCli), - container.NewStopCommand(dockerCli), - container.NewTopCommand(dockerCli), - container.NewUnpauseCommand(dockerCli), - container.NewUpdateCommand(dockerCli), - container.NewWaitCommand(dockerCli), - image.NewBuildCommand(dockerCli), - image.NewHistoryCommand(dockerCli), - image.NewImagesCommand(dockerCli), - image.NewLoadCommand(dockerCli), - image.NewRemoveCommand(dockerCli), - image.NewSaveCommand(dockerCli), - image.NewPullCommand(dockerCli), - image.NewPushCommand(dockerCli), - image.NewSearchCommand(dockerCli), - image.NewImportCommand(dockerCli), - image.NewTagCommand(dockerCli), - network.NewNetworkCommand(dockerCli), - system.NewEventsCommand(dockerCli), - system.NewInspectCommand(dockerCli), - registry.NewLoginCommand(dockerCli), - registry.NewLogoutCommand(dockerCli), - system.NewVersionCommand(dockerCli), - volume.NewVolumeCommand(dockerCli), - system.NewInfoCommand(dockerCli), - ) - plugin.NewPluginCommand(rootCmd, dockerCli) + rootCmd.SetFlagErrorFunc(FlagErrorFunc) rootCmd.PersistentFlags().BoolP("help", "h", false, "Print usage") rootCmd.PersistentFlags().MarkShorthandDeprecated("help", "please use --help") @@ -87,6 +23,20 @@ func (c CobraAdaptor) GetRootCommand() *cobra.Command { return c.rootCmd } +// FlagErrorFunc prints an error messages which matches the format of the +// docker/docker/cli error messages +func FlagErrorFunc(cmd *cobra.Command, err error) error { + if err == nil { + return err + } + + usage := "" + if cmd.HasSubCommands() { + usage = "\n\n" + cmd.UsageString() + } + return fmt.Errorf("%s\nSee '%s --help'.%s", err, cmd.CommandPath(), usage) +} + var usageTemplate = `Usage: {{if not .HasSubCommands}}{{.UseLine}}{{end}}{{if .HasSubCommands}}{{ .CommandPath}} COMMAND{{end}} {{ .Short | trim }}{{if gt .Aliases 0}} diff --git a/components/cli/flagerrors.go b/components/cli/flagerrors.go deleted file mode 100644 index aab8a98845..0000000000 --- a/components/cli/flagerrors.go +++ /dev/null @@ -1,21 +0,0 @@ -package cli - -import ( - "fmt" - - "github.com/spf13/cobra" -) - -// FlagErrorFunc prints an error messages which matches the format of the -// docker/docker/cli error messages -func FlagErrorFunc(cmd *cobra.Command, err error) error { - if err == nil { - return err - } - - usage := "" - if cmd.HasSubCommands() { - usage = "\n\n" + cmd.UsageString() - } - return fmt.Errorf("%s\nSee '%s --help'.%s", err, cmd.CommandPath(), usage) -} diff --git a/components/cli/flags/common.go b/components/cli/flags/common.go index a3579bff6d..2318b9d975 100644 --- a/components/cli/flags/common.go +++ b/components/cli/flags/common.go @@ -43,9 +43,7 @@ type CommonOptions struct { // NewCommonOptions returns a new CommonOptions func NewCommonOptions() *CommonOptions { - return &CommonOptions{ - TLSOptions: &tlsconfig.Options{}, - } + return &CommonOptions{} } // InstallFlags adds flags for the common options on the FlagSet @@ -61,13 +59,14 @@ func (commonOpts *CommonOptions) InstallFlags(flags *pflag.FlagSet) { // TODO use flag flags.String("identity"}, "i", "", "Path to libtrust key file") + commonOpts.TLSOptions = &tlsconfig.Options{} tlsOptions := commonOpts.TLSOptions flags.StringVar(&tlsOptions.CAFile, "tlscacert", filepath.Join(dockerCertPath, DefaultCaFile), "Trust certs signed only by this CA") flags.StringVar(&tlsOptions.CertFile, "tlscert", filepath.Join(dockerCertPath, DefaultCertFile), "Path to TLS certificate file") flags.StringVar(&tlsOptions.KeyFile, "tlskey", filepath.Join(dockerCertPath, DefaultKeyFile), "Path to TLS key file") hostOpt := opts.NewNamedListOptsRef("hosts", &commonOpts.Hosts, opts.ValidateHost) - flags.VarP(hostOpt, "-host", "H", "Daemon socket(s) to connect to") + flags.VarP(hostOpt, "host", "H", "Daemon socket(s) to connect to") } // SetDefaultOptions sets default values for options after flag parsing is From e062c54a99b0308c292a0ea2c77eeb89587c7643 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 23 Jun 2016 11:25:51 -0400 Subject: [PATCH 106/978] Remove old cli framework. Also consolidate the leftover packages under cli. Remove pkg/mflag. Make manpage generation work with new cobra layout. Remove remaining mflag and fix tests after rebase with master. Signed-off-by: Daniel Nephin Upstream-commit: 3b178887a7bc9048cc0a86a1b912c7557ba85d81 Component: cli --- components/cli/docker.go | 68 +++--------------------------------- components/cli/usage.go | 22 ------------ components/cli/usage_test.go | 15 -------- 3 files changed, 4 insertions(+), 101 deletions(-) delete mode 100644 components/cli/usage.go delete mode 100644 components/cli/usage_test.go diff --git a/components/cli/docker.go b/components/cli/docker.go index 0ae3906abb..8d7861847f 100644 --- a/components/cli/docker.go +++ b/components/cli/docker.go @@ -6,19 +6,8 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/docker/api/client" - "github.com/docker/docker/api/client/container" - "github.com/docker/docker/api/client/image" - "github.com/docker/docker/api/client/network" - "github.com/docker/docker/api/client/node" - "github.com/docker/docker/api/client/plugin" - "github.com/docker/docker/api/client/registry" - "github.com/docker/docker/api/client/service" - "github.com/docker/docker/api/client/stack" - "github.com/docker/docker/api/client/swarm" - "github.com/docker/docker/api/client/system" - "github.com/docker/docker/api/client/volume" + "github.com/docker/docker/api/client/command" "github.com/docker/docker/cli" - "github.com/docker/docker/cli/cobraadaptor" cliflags "github.com/docker/docker/cli/flags" "github.com/docker/docker/cliconfig" "github.com/docker/docker/dockerversion" @@ -50,7 +39,7 @@ func newDockerCommand(dockerCli *client.DockerCli) *cobra.Command { return dockerCli.Initialize(opts) }, } - cobraadaptor.SetupRootCommand(cmd) + cli.SetupRootCommand(cmd) flags := cmd.Flags() flags.BoolVarP(&opts.Version, "version", "v", false, "Print version information and quit") @@ -58,57 +47,8 @@ func newDockerCommand(dockerCli *client.DockerCli) *cobra.Command { opts.Common.InstallFlags(flags) cmd.SetOutput(dockerCli.Out()) - cmd.AddCommand( - newDaemonCommand(), - node.NewNodeCommand(dockerCli), - service.NewServiceCommand(dockerCli), - stack.NewStackCommand(dockerCli), - stack.NewTopLevelDeployCommand(dockerCli), - swarm.NewSwarmCommand(dockerCli), - container.NewAttachCommand(dockerCli), - container.NewCommitCommand(dockerCli), - container.NewCopyCommand(dockerCli), - container.NewCreateCommand(dockerCli), - container.NewDiffCommand(dockerCli), - container.NewExecCommand(dockerCli), - container.NewExportCommand(dockerCli), - container.NewKillCommand(dockerCli), - container.NewLogsCommand(dockerCli), - container.NewPauseCommand(dockerCli), - container.NewPortCommand(dockerCli), - container.NewPsCommand(dockerCli), - container.NewRenameCommand(dockerCli), - container.NewRestartCommand(dockerCli), - container.NewRmCommand(dockerCli), - container.NewRunCommand(dockerCli), - container.NewStartCommand(dockerCli), - container.NewStatsCommand(dockerCli), - container.NewStopCommand(dockerCli), - container.NewTopCommand(dockerCli), - container.NewUnpauseCommand(dockerCli), - container.NewUpdateCommand(dockerCli), - container.NewWaitCommand(dockerCli), - image.NewBuildCommand(dockerCli), - image.NewHistoryCommand(dockerCli), - image.NewImagesCommand(dockerCli), - image.NewLoadCommand(dockerCli), - image.NewRemoveCommand(dockerCli), - image.NewSaveCommand(dockerCli), - image.NewPullCommand(dockerCli), - image.NewPushCommand(dockerCli), - image.NewSearchCommand(dockerCli), - image.NewImportCommand(dockerCli), - image.NewTagCommand(dockerCli), - network.NewNetworkCommand(dockerCli), - system.NewEventsCommand(dockerCli), - system.NewInspectCommand(dockerCli), - registry.NewLoginCommand(dockerCli), - registry.NewLogoutCommand(dockerCli), - system.NewVersionCommand(dockerCli), - volume.NewVolumeCommand(dockerCli), - system.NewInfoCommand(dockerCli), - ) - plugin.NewPluginCommand(cmd, dockerCli) + cmd.AddCommand(newDaemonCommand()) + command.AddCommands(cmd, dockerCli) return cmd } diff --git a/components/cli/usage.go b/components/cli/usage.go deleted file mode 100644 index 792d178073..0000000000 --- a/components/cli/usage.go +++ /dev/null @@ -1,22 +0,0 @@ -package main - -import ( - "sort" - - "github.com/docker/docker/cli" -) - -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 } - -// TODO(tiborvass): do not show 'daemon' on client-only binaries - -func sortCommands(commands []cli.Command) []cli.Command { - dockerCommands := make([]cli.Command, len(commands)) - copy(dockerCommands, commands) - sort.Sort(byName(dockerCommands)) - return dockerCommands -} diff --git a/components/cli/usage_test.go b/components/cli/usage_test.go deleted file mode 100644 index 0453265db8..0000000000 --- a/components/cli/usage_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package main - -import ( - "sort" - "testing" - - "github.com/docker/docker/cli" -) - -// Tests if the subcommands of docker are sorted -func TestDockerSubcommandsAreSorted(t *testing.T) { - if !sort.IsSorted(byName(cli.DockerCommandUsage)) { - t.Fatal("Docker subcommands are not in sorted order") - } -} From 594214ababda3bb88f3e8306863f5603dca83980 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 23 Jun 2016 11:25:51 -0400 Subject: [PATCH 107/978] Remove old cli framework. Also consolidate the leftover packages under cli. Remove pkg/mflag. Make manpage generation work with new cobra layout. Remove remaining mflag and fix tests after rebase with master. Signed-off-by: Daniel Nephin Upstream-commit: 38aca22dcdfe7a1c3d3628340f520b9ea3591115 Component: cli --- components/cli/cli.go | 191 ------------------ .../cli/{cobraadaptor/adaptor.go => cobra.go} | 8 +- components/cli/error.go | 15 +- components/cli/usage.go | 19 -- 4 files changed, 15 insertions(+), 218 deletions(-) delete mode 100644 components/cli/cli.go rename components/cli/{cobraadaptor/adaptor.go => cobra.go} (86%) delete mode 100644 components/cli/usage.go diff --git a/components/cli/cli.go b/components/cli/cli.go deleted file mode 100644 index 8d21cda69d..0000000000 --- a/components/cli/cli.go +++ /dev/null @@ -1,191 +0,0 @@ -package cli - -import ( - "errors" - "fmt" - "io" - "os" - "strings" - - flag "github.com/docker/docker/pkg/mflag" -) - -// Cli represents a command line interface. -type Cli struct { - Stderr io.Writer - handlers []Handler - Usage func() -} - -// Handler holds the different commands Cli will call -// It should have methods with names starting with `Cmd` like: -// func (h myHandler) CmdFoo(args ...string) error -type Handler interface { - Command(name string) func(...string) error -} - -// Initializer can be optionally implemented by a Handler to -// initialize before each call to one of its commands. -type Initializer interface { - Initialize() error -} - -// New instantiates a ready-to-use Cli. -func New(handlers ...Handler) *Cli { - // make the generic Cli object the first cli handler - // in order to handle `docker help` appropriately - cli := new(Cli) - cli.handlers = append([]Handler{cli}, handlers...) - return cli -} - -var errCommandNotFound = errors.New("command not found") - -func (cli *Cli) command(args ...string) (func(...string) error, error) { - for _, c := range cli.handlers { - if c == nil { - continue - } - if cmd := c.Command(strings.Join(args, " ")); cmd != nil { - if ci, ok := c.(Initializer); ok { - if err := ci.Initialize(); err != nil { - return nil, err - } - } - return cmd, nil - } - } - return nil, errCommandNotFound -} - -// Run executes the specified command. -func (cli *Cli) Run(args ...string) error { - if len(args) > 1 { - command, err := cli.command(args[:2]...) - if err == nil { - return command(args[2:]...) - } - if err != errCommandNotFound { - return err - } - } - if len(args) > 0 { - command, err := cli.command(args[0]) - if err != nil { - if err == errCommandNotFound { - cli.noSuchCommand(args[0]) - return nil - } - return err - } - return command(args[1:]...) - } - return cli.CmdHelp() -} - -func (cli *Cli) noSuchCommand(command string) { - if cli.Stderr == nil { - cli.Stderr = os.Stderr - } - fmt.Fprintf(cli.Stderr, "docker: '%s' is not a docker command.\nSee 'docker --help'.\n", command) - os.Exit(1) -} - -// Command returns a command handler, or nil if the command does not exist -func (cli *Cli) Command(name string) func(...string) error { - return map[string]func(...string) error{ - "help": cli.CmdHelp, - }[name] -} - -// CmdHelp displays information on a Docker command. -// -// If more than one command is specified, information is only shown for the first command. -// -// Usage: docker help COMMAND or docker COMMAND --help -func (cli *Cli) CmdHelp(args ...string) error { - if len(args) > 1 { - command, err := cli.command(args[:2]...) - if err == nil { - command("--help") - return nil - } - if err != errCommandNotFound { - return err - } - } - if len(args) > 0 { - command, err := cli.command(args[0]) - if err != nil { - if err == errCommandNotFound { - cli.noSuchCommand(args[0]) - return nil - } - return err - } - command("--help") - return nil - } - - if cli.Usage == nil { - flag.Usage() - } else { - cli.Usage() - } - - return nil -} - -// Subcmd is a subcommand of the main "docker" command. -// A subcommand represents an action that can be performed -// from the Docker command line client. -// -// To see all available subcommands, run "docker --help". -func Subcmd(name string, synopses []string, description string, exitOnError bool) *flag.FlagSet { - var errorHandling flag.ErrorHandling - if exitOnError { - errorHandling = flag.ExitOnError - } else { - errorHandling = flag.ContinueOnError - } - flags := flag.NewFlagSet(name, errorHandling) - flags.Usage = func() { - flags.ShortUsage() - flags.PrintDefaults() - } - - flags.ShortUsage = func() { - if len(synopses) == 0 { - synopses = []string{""} - } - - // Allow for multiple command usage synopses. - for i, synopsis := range synopses { - lead := "\t" - if i == 0 { - // First line needs the word 'Usage'. - lead = "Usage:\t" - } - - if synopsis != "" { - synopsis = " " + synopsis - } - - fmt.Fprintf(flags.Out(), "\n%sdocker %s%s", lead, name, synopsis) - } - - fmt.Fprintf(flags.Out(), "\n\n%s\n", description) - } - - return flags -} - -// StatusError reports an unsuccessful exit by a command. -type StatusError struct { - Status string - StatusCode int -} - -func (e StatusError) Error() string { - return fmt.Sprintf("Status: %s, Code: %d", e.Status, e.StatusCode) -} diff --git a/components/cli/cobraadaptor/adaptor.go b/components/cli/cobra.go similarity index 86% rename from components/cli/cobraadaptor/adaptor.go rename to components/cli/cobra.go index 67263e1577..5e20c96003 100644 --- a/components/cli/cobraadaptor/adaptor.go +++ b/components/cli/cobra.go @@ -1,4 +1,4 @@ -package cobraadaptor +package cli import ( "fmt" @@ -17,12 +17,6 @@ func SetupRootCommand(rootCmd *cobra.Command) { rootCmd.PersistentFlags().MarkShorthandDeprecated("help", "please use --help") } -// GetRootCommand returns the root command. Required to generate the man pages -// and reference docs from a script outside this package. -func (c CobraAdaptor) GetRootCommand() *cobra.Command { - return c.rootCmd -} - // FlagErrorFunc prints an error messages which matches the format of the // docker/docker/cli error messages func FlagErrorFunc(cmd *cobra.Command, err error) error { diff --git a/components/cli/error.go b/components/cli/error.go index e421c7f7c7..62f62433b8 100644 --- a/components/cli/error.go +++ b/components/cli/error.go @@ -1,6 +1,9 @@ package cli -import "strings" +import ( + "fmt" + "strings" +) // Errors is a list of errors. // Useful in a loop if you don't want to return the error right away and you want to display after the loop, @@ -18,3 +21,13 @@ func (errList Errors) Error() string { } return strings.Join(out, ", ") } + +// StatusError reports an unsuccessful exit by a command. +type StatusError struct { + Status string + StatusCode int +} + +func (e StatusError) Error() string { + return fmt.Sprintf("Status: %s, Code: %d", e.Status, e.StatusCode) +} diff --git a/components/cli/usage.go b/components/cli/usage.go deleted file mode 100644 index a8a0328643..0000000000 --- a/components/cli/usage.go +++ /dev/null @@ -1,19 +0,0 @@ -package cli - -// Command is the struct containing the command name and description -type Command struct { - Name string - Description string -} - -// DockerCommandUsage lists the top level docker commands and their short usage -var DockerCommandUsage = []Command{} - -// DockerCommands stores all the docker command -var DockerCommands = make(map[string]Command) - -func init() { - for _, cmd := range DockerCommandUsage { - DockerCommands[cmd.Name] = cmd - } -} From e110c778f4f01567e86ad0e53225a763c666377e Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 3 Aug 2016 12:20:46 -0400 Subject: [PATCH 108/978] Fix tests and windows service. Support args to RunCommand Fix docker help text test. Fix for ipv6 tests. Fix TLSverify option. Fix TestDaemonDiscoveryBackendConfigReload Use tempfile for another test. Restore missing flag. Fix tests for removal of shlex. Signed-off-by: Daniel Nephin Upstream-commit: 9af25060cd47681b359fe736b31336013022d949 Component: cli --- components/cli/daemon_none.go | 3 ++- components/cli/docker.go | 21 ++++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/components/cli/daemon_none.go b/components/cli/daemon_none.go index c57896ed71..65f9f37be2 100644 --- a/components/cli/daemon_none.go +++ b/components/cli/daemon_none.go @@ -4,9 +4,10 @@ package main import ( "fmt" - "github.com/spf13/cobra" "runtime" "strings" + + "github.com/spf13/cobra" ) func newDaemonCommand() *cobra.Command { diff --git a/components/cli/docker.go b/components/cli/docker.go index 8d7861847f..38907970d3 100644 --- a/components/cli/docker.go +++ b/components/cli/docker.go @@ -19,13 +19,15 @@ import ( func newDockerCommand(dockerCli *client.DockerCli) *cobra.Command { opts := cliflags.NewClientOptions() + var flags *pflag.FlagSet + cmd := &cobra.Command{ Use: "docker [OPTIONS] COMMAND [arg...]", Short: "A self-sufficient runtime for containers.", SilenceUsage: true, SilenceErrors: true, TraverseChildren: true, - Args: cli.NoArgs, + Args: noArgs, RunE: func(cmd *cobra.Command, args []string) error { if opts.Version { showVersion() @@ -35,13 +37,15 @@ func newDockerCommand(dockerCli *client.DockerCli) *cobra.Command { return nil }, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - dockerPreRun(cmd.Flags(), opts) + // flags must be the top-level command flags, not cmd.Flags() + opts.Common.SetDefaultOptions(flags) + dockerPreRun(opts) return dockerCli.Initialize(opts) }, } cli.SetupRootCommand(cmd) - flags := cmd.Flags() + flags = cmd.Flags() flags.BoolVarP(&opts.Version, "version", "v", false, "Print version information and quit") flags.StringVar(&opts.ConfigDir, "config", cliconfig.ConfigDir(), "Location of client config files") opts.Common.InstallFlags(flags) @@ -53,6 +57,14 @@ func newDockerCommand(dockerCli *client.DockerCli) *cobra.Command { return cmd } +func noArgs(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return nil + } + return fmt.Errorf( + "docker: '%s' is not a docker command.\nSee 'docker --help'%s", args[0], ".") +} + func main() { // Set terminal emulation based on platform as required. stdin, stdout, stderr := term.StdStreams() @@ -86,8 +98,7 @@ func showVersion() { } } -func dockerPreRun(flags *pflag.FlagSet, opts *cliflags.ClientOptions) { - opts.Common.SetDefaultOptions(flags) +func dockerPreRun(opts *cliflags.ClientOptions) { cliflags.SetDaemonLogLevel(opts.Common.LogLevel) if opts.ConfigDir != "" { From 29b743a9811707789fb93b2c7e69c79214dd9ce2 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 3 Aug 2016 12:20:46 -0400 Subject: [PATCH 109/978] Fix tests and windows service. Support args to RunCommand Fix docker help text test. Fix for ipv6 tests. Fix TLSverify option. Fix TestDaemonDiscoveryBackendConfigReload Use tempfile for another test. Restore missing flag. Fix tests for removal of shlex. Signed-off-by: Daniel Nephin Upstream-commit: 2791c2ec2812855760f01f5c50d9bb92d445ac62 Component: cli --- components/cli/cobra.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/components/cli/cobra.go b/components/cli/cobra.go index 5e20c96003..f924e67b27 100644 --- a/components/cli/cobra.go +++ b/components/cli/cobra.go @@ -28,7 +28,10 @@ func FlagErrorFunc(cmd *cobra.Command, err error) error { if cmd.HasSubCommands() { usage = "\n\n" + cmd.UsageString() } - return fmt.Errorf("%s\nSee '%s --help'.%s", err, cmd.CommandPath(), usage) + return StatusError{ + Status: fmt.Sprintf("%s\nSee '%s --help'.%s", err, cmd.CommandPath(), usage), + StatusCode: 125, + } } var usageTemplate = `Usage: {{if not .HasSubCommands}}{{.UseLine}}{{end}}{{if .HasSubCommands}}{{ .CommandPath}} COMMAND{{end}} From c3ab654101b0920bc6239d04f0ae1ee9517e521a Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 26 Aug 2016 12:19:02 -0400 Subject: [PATCH 110/978] Fix daemon command proxy. Signed-off-by: Daniel Nephin Upstream-commit: ad96b991e960ba1e90f33d7bd52826aa44c2fc3e Component: cli --- components/cli/daemon_none_test.go | 17 +++++++---------- components/cli/daemon_unit_test.go | 30 ++++++++++++++++++++++++++++++ components/cli/daemon_unix.go | 6 ++++-- 3 files changed, 41 insertions(+), 12 deletions(-) create mode 100644 components/cli/daemon_unit_test.go diff --git a/components/cli/daemon_none_test.go b/components/cli/daemon_none_test.go index d75453bcc5..32032fe1b3 100644 --- a/components/cli/daemon_none_test.go +++ b/components/cli/daemon_none_test.go @@ -3,18 +3,15 @@ package main import ( - "strings" "testing" + + "github.com/docker/docker/pkg/testutil/assert" ) -func TestCmdDaemon(t *testing.T) { - proxy := NewDaemonProxy() - err := proxy.CmdDaemon("--help") - if err == nil { - t.Fatal("Expected CmdDaemon to fail on Windows.") - } +func TestDaemonCommand(t *testing.T) { + cmd := newDaemonCommand() + cmd.SetArgs([]string{"--help"}) + err := cmd.Execute() - if !strings.Contains(err.Error(), "Please run `dockerd`") { - t.Fatalf("Expected an error about running dockerd, got %s", err) - } + assert.Error(t, err, "Please run `dockerd`") } diff --git a/components/cli/daemon_unit_test.go b/components/cli/daemon_unit_test.go new file mode 100644 index 0000000000..26348a8843 --- /dev/null +++ b/components/cli/daemon_unit_test.go @@ -0,0 +1,30 @@ +// +build daemon + +package main + +import ( + "testing" + + "github.com/docker/docker/pkg/testutil/assert" + "github.com/spf13/cobra" +) + +func stubRun(cmd *cobra.Command, args []string) error { + return nil +} + +func TestDaemonCommandHelp(t *testing.T) { + cmd := newDaemonCommand() + cmd.RunE = stubRun + cmd.SetArgs([]string{"--help"}) + err := cmd.Execute() + assert.NilError(t, err) +} + +func TestDaemonCommand(t *testing.T) { + cmd := newDaemonCommand() + cmd.RunE = stubRun + cmd.SetArgs([]string{"--containerd", "/foo"}) + err := cmd.Execute() + assert.NilError(t, err) +} diff --git a/components/cli/daemon_unix.go b/components/cli/daemon_unix.go index 30a40a8611..754bdeece3 100644 --- a/components/cli/daemon_unix.go +++ b/components/cli/daemon_unix.go @@ -17,8 +17,10 @@ const daemonBinary = "dockerd" func newDaemonCommand() *cobra.Command { cmd := &cobra.Command{ - Use: "daemon", - Hidden: true, + Use: "daemon", + Hidden: true, + Args: cobra.ArbitraryArgs, + DisableFlagParsing: true, RunE: func(cmd *cobra.Command, args []string) error { return runDaemon() }, From f7f411a109aaf9cf0a0b1a6d9d4a3c02129ebfbc Mon Sep 17 00:00:00 2001 From: Cao Weiwei Date: Sun, 28 Aug 2016 21:30:14 +0800 Subject: [PATCH 111/978] Fix typo Signed-off-by: Cao Weiwei Upstream-commit: efdd29abcf8ab0a0cf2d706f4e9baa77a2f3393b Component: cli --- components/cli/cobra.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/cobra.go b/components/cli/cobra.go index f924e67b27..836196d76e 100644 --- a/components/cli/cobra.go +++ b/components/cli/cobra.go @@ -17,7 +17,7 @@ func SetupRootCommand(rootCmd *cobra.Command) { rootCmd.PersistentFlags().MarkShorthandDeprecated("help", "please use --help") } -// FlagErrorFunc prints an error messages which matches the format of the +// FlagErrorFunc prints an error message which matches the format of the // docker/docker/cli error messages func FlagErrorFunc(cmd *cobra.Command, err error) error { if err == nil { From f7bdcb129720ff270f4d6ee4e96d37c1516b729f Mon Sep 17 00:00:00 2001 From: Michael Crosby Date: Tue, 6 Sep 2016 11:46:37 -0700 Subject: [PATCH 112/978] Move engine-api client package This moves the engine-api client package to `/docker/docker/client`. Signed-off-by: Michael Crosby Upstream-commit: 3fff6acaa19d7f2812a68f7fe49cfac5fa8dbade Component: cli --- components/cli/checkpoint_create.go | 13 + components/cli/checkpoint_create_test.go | 73 +++++ components/cli/checkpoint_delete.go | 12 + components/cli/checkpoint_delete_test.go | 47 ++++ components/cli/checkpoint_list.go | 22 ++ components/cli/checkpoint_list_test.go | 57 ++++ components/cli/client.go | 156 +++++++++++ components/cli/client_mock_test.go | 76 ++++++ components/cli/client_test.go | 249 ++++++++++++++++++ components/cli/client_unix.go | 6 + components/cli/client_windows.go | 4 + components/cli/container_attach.go | 34 +++ components/cli/container_commit.go | 53 ++++ components/cli/container_commit_test.go | 96 +++++++ components/cli/container_copy.go | 97 +++++++ components/cli/container_copy_test.go | 244 +++++++++++++++++ components/cli/container_create.go | 46 ++++ components/cli/container_create_test.go | 77 ++++++ components/cli/container_diff.go | 23 ++ components/cli/container_diff_test.go | 61 +++++ components/cli/container_exec.go | 49 ++++ components/cli/container_exec_test.go | 157 +++++++++++ components/cli/container_export.go | 20 ++ components/cli/container_export_test.go | 50 ++++ components/cli/container_inspect.go | 54 ++++ components/cli/container_inspect_test.go | 125 +++++++++ components/cli/container_kill.go | 17 ++ components/cli/container_kill_test.go | 46 ++++ components/cli/container_list.go | 56 ++++ components/cli/container_list_test.go | 96 +++++++ components/cli/container_logs.go | 52 ++++ components/cli/container_logs_test.go | 133 ++++++++++ components/cli/container_pause.go | 10 + components/cli/container_pause_test.go | 41 +++ components/cli/container_remove.go | 27 ++ components/cli/container_remove_test.go | 59 +++++ components/cli/container_rename.go | 16 ++ components/cli/container_rename_test.go | 46 ++++ components/cli/container_resize.go | 29 ++ components/cli/container_resize_test.go | 82 ++++++ components/cli/container_restart.go | 22 ++ components/cli/container_restart_test.go | 48 ++++ components/cli/container_start.go | 21 ++ components/cli/container_start_test.go | 58 ++++ components/cli/container_stats.go | 24 ++ components/cli/container_stats_test.go | 70 +++++ components/cli/container_stop.go | 21 ++ components/cli/container_stop_test.go | 48 ++++ components/cli/container_top.go | 28 ++ components/cli/container_top_test.go | 74 ++++++ components/cli/container_unpause.go | 10 + components/cli/container_unpause_test.go | 41 +++ components/cli/container_update.go | 23 ++ components/cli/container_update_test.go | 59 +++++ components/cli/container_wait.go | 26 ++ components/cli/container_wait_test.go | 70 +++++ components/cli/errors.go | 208 +++++++++++++++ components/cli/events.go | 48 ++++ components/cli/events_test.go | 126 +++++++++ components/cli/hijack.go | 174 ++++++++++++ components/cli/image_build.go | 123 +++++++++ components/cli/image_build_test.go | 230 ++++++++++++++++ components/cli/image_create.go | 34 +++ components/cli/image_create_test.go | 76 ++++++ components/cli/image_history.go | 22 ++ components/cli/image_history_test.go | 60 +++++ components/cli/image_import.go | 37 +++ components/cli/image_import_test.go | 81 ++++++ components/cli/image_inspect.go | 33 +++ components/cli/image_inspect_test.go | 71 +++++ components/cli/image_list.go | 40 +++ components/cli/image_list_test.go | 122 +++++++++ components/cli/image_load.go | 30 +++ components/cli/image_load_test.go | 95 +++++++ components/cli/image_pull.go | 46 ++++ components/cli/image_pull_test.go | 199 ++++++++++++++ components/cli/image_push.go | 54 ++++ components/cli/image_push_test.go | 180 +++++++++++++ components/cli/image_remove.go | 31 +++ components/cli/image_remove_test.go | 95 +++++++ components/cli/image_save.go | 22 ++ components/cli/image_save_test.go | 58 ++++ components/cli/image_search.go | 51 ++++ components/cli/image_search_test.go | 165 ++++++++++++ components/cli/image_tag.go | 34 +++ components/cli/image_tag_test.go | 121 +++++++++ components/cli/info.go | 26 ++ components/cli/info_test.go | 76 ++++++ components/cli/interface.go | 135 ++++++++++ components/cli/interface_experimental.go | 37 +++ components/cli/interface_stable.go | 11 + components/cli/login.go | 28 ++ components/cli/network_connect.go | 18 ++ components/cli/network_connect_test.go | 107 ++++++++ components/cli/network_create.go | 25 ++ components/cli/network_create_test.go | 72 +++++ components/cli/network_disconnect.go | 14 + components/cli/network_disconnect_test.go | 64 +++++ components/cli/network_inspect.go | 38 +++ components/cli/network_inspect_test.go | 69 +++++ components/cli/network_list.go | 31 +++ components/cli/network_list_test.go | 108 ++++++++ components/cli/network_remove.go | 10 + components/cli/network_remove_test.go | 47 ++++ components/cli/node_inspect.go | 33 +++ components/cli/node_inspect_test.go | 65 +++++ components/cli/node_list.go | 36 +++ components/cli/node_list_test.go | 94 +++++++ components/cli/node_remove.go | 21 ++ components/cli/node_remove_test.go | 69 +++++ components/cli/node_update.go | 18 ++ components/cli/node_update_test.go | 49 ++++ components/cli/plugin_disable.go | 14 + components/cli/plugin_disable_test.go | 49 ++++ components/cli/plugin_enable.go | 14 + components/cli/plugin_enable_test.go | 49 ++++ components/cli/plugin_inspect.go | 30 +++ components/cli/plugin_inspect_test.go | 56 ++++ components/cli/plugin_install.go | 59 +++++ components/cli/plugin_list.go | 23 ++ components/cli/plugin_list_test.go | 61 +++++ components/cli/plugin_push.go | 15 ++ components/cli/plugin_push_test.go | 53 ++++ components/cli/plugin_remove.go | 22 ++ components/cli/plugin_remove_test.go | 51 ++++ components/cli/plugin_set.go | 14 + components/cli/plugin_set_test.go | 49 ++++ components/cli/request.go | 208 +++++++++++++++ components/cli/request_test.go | 91 +++++++ components/cli/service_create.go | 30 +++ components/cli/service_create_test.go | 57 ++++ components/cli/service_inspect.go | 33 +++ components/cli/service_inspect_test.go | 65 +++++ components/cli/service_list.go | 35 +++ components/cli/service_list_test.go | 94 +++++++ components/cli/service_remove.go | 10 + components/cli/service_remove_test.go | 47 ++++ components/cli/service_update.go | 30 +++ components/cli/service_update_test.go | 77 ++++++ components/cli/swarm_init.go | 21 ++ components/cli/swarm_init_test.go | 54 ++++ components/cli/swarm_inspect.go | 21 ++ components/cli/swarm_inspect_test.go | 56 ++++ components/cli/swarm_join.go | 13 + components/cli/swarm_join_test.go | 51 ++++ components/cli/swarm_leave.go | 18 ++ components/cli/swarm_leave_test.go | 66 +++++ components/cli/swarm_update.go | 21 ++ components/cli/swarm_update_test.go | 49 ++++ components/cli/task_inspect.go | 34 +++ components/cli/task_inspect_test.go | 54 ++++ components/cli/task_list.go | 35 +++ components/cli/task_list_test.go | 94 +++++++ components/cli/testdata/ca.pem | 18 ++ components/cli/testdata/cert.pem | 18 ++ components/cli/testdata/key.pem | 27 ++ components/cli/transport/cancellable/LICENSE | 27 ++ .../cli/transport/cancellable/canceler.go | 23 ++ .../transport/cancellable/canceler_go14.go | 27 ++ .../cli/transport/cancellable/cancellable.go | 115 ++++++++ components/cli/transport/client.go | 47 ++++ components/cli/transport/tlsconfig_clone.go | 11 + .../cli/transport/tlsconfig_clone_go17.go | 33 +++ components/cli/transport/transport.go | 57 ++++ components/cli/version.go | 21 ++ components/cli/volume_create.go | 20 ++ components/cli/volume_create_test.go | 74 ++++++ components/cli/volume_inspect.go | 38 +++ components/cli/volume_inspect_test.go | 76 ++++++ components/cli/volume_list.go | 32 +++ components/cli/volume_list_test.go | 97 +++++++ components/cli/volume_remove.go | 18 ++ components/cli/volume_remove_test.go | 47 ++++ 173 files changed, 9970 insertions(+) create mode 100644 components/cli/checkpoint_create.go create mode 100644 components/cli/checkpoint_create_test.go create mode 100644 components/cli/checkpoint_delete.go create mode 100644 components/cli/checkpoint_delete_test.go create mode 100644 components/cli/checkpoint_list.go create mode 100644 components/cli/checkpoint_list_test.go create mode 100644 components/cli/client.go create mode 100644 components/cli/client_mock_test.go create mode 100644 components/cli/client_test.go create mode 100644 components/cli/client_unix.go create mode 100644 components/cli/client_windows.go create mode 100644 components/cli/container_attach.go create mode 100644 components/cli/container_commit.go create mode 100644 components/cli/container_commit_test.go create mode 100644 components/cli/container_copy.go create mode 100644 components/cli/container_copy_test.go create mode 100644 components/cli/container_create.go create mode 100644 components/cli/container_create_test.go create mode 100644 components/cli/container_diff.go create mode 100644 components/cli/container_diff_test.go create mode 100644 components/cli/container_exec.go create mode 100644 components/cli/container_exec_test.go create mode 100644 components/cli/container_export.go create mode 100644 components/cli/container_export_test.go create mode 100644 components/cli/container_inspect.go create mode 100644 components/cli/container_inspect_test.go create mode 100644 components/cli/container_kill.go create mode 100644 components/cli/container_kill_test.go create mode 100644 components/cli/container_list.go create mode 100644 components/cli/container_list_test.go create mode 100644 components/cli/container_logs.go create mode 100644 components/cli/container_logs_test.go create mode 100644 components/cli/container_pause.go create mode 100644 components/cli/container_pause_test.go create mode 100644 components/cli/container_remove.go create mode 100644 components/cli/container_remove_test.go create mode 100644 components/cli/container_rename.go create mode 100644 components/cli/container_rename_test.go create mode 100644 components/cli/container_resize.go create mode 100644 components/cli/container_resize_test.go create mode 100644 components/cli/container_restart.go create mode 100644 components/cli/container_restart_test.go create mode 100644 components/cli/container_start.go create mode 100644 components/cli/container_start_test.go create mode 100644 components/cli/container_stats.go create mode 100644 components/cli/container_stats_test.go create mode 100644 components/cli/container_stop.go create mode 100644 components/cli/container_stop_test.go create mode 100644 components/cli/container_top.go create mode 100644 components/cli/container_top_test.go create mode 100644 components/cli/container_unpause.go create mode 100644 components/cli/container_unpause_test.go create mode 100644 components/cli/container_update.go create mode 100644 components/cli/container_update_test.go create mode 100644 components/cli/container_wait.go create mode 100644 components/cli/container_wait_test.go create mode 100644 components/cli/errors.go create mode 100644 components/cli/events.go create mode 100644 components/cli/events_test.go create mode 100644 components/cli/hijack.go create mode 100644 components/cli/image_build.go create mode 100644 components/cli/image_build_test.go create mode 100644 components/cli/image_create.go create mode 100644 components/cli/image_create_test.go create mode 100644 components/cli/image_history.go create mode 100644 components/cli/image_history_test.go create mode 100644 components/cli/image_import.go create mode 100644 components/cli/image_import_test.go create mode 100644 components/cli/image_inspect.go create mode 100644 components/cli/image_inspect_test.go create mode 100644 components/cli/image_list.go create mode 100644 components/cli/image_list_test.go create mode 100644 components/cli/image_load.go create mode 100644 components/cli/image_load_test.go create mode 100644 components/cli/image_pull.go create mode 100644 components/cli/image_pull_test.go create mode 100644 components/cli/image_push.go create mode 100644 components/cli/image_push_test.go create mode 100644 components/cli/image_remove.go create mode 100644 components/cli/image_remove_test.go create mode 100644 components/cli/image_save.go create mode 100644 components/cli/image_save_test.go create mode 100644 components/cli/image_search.go create mode 100644 components/cli/image_search_test.go create mode 100644 components/cli/image_tag.go create mode 100644 components/cli/image_tag_test.go create mode 100644 components/cli/info.go create mode 100644 components/cli/info_test.go create mode 100644 components/cli/interface.go create mode 100644 components/cli/interface_experimental.go create mode 100644 components/cli/interface_stable.go create mode 100644 components/cli/login.go create mode 100644 components/cli/network_connect.go create mode 100644 components/cli/network_connect_test.go create mode 100644 components/cli/network_create.go create mode 100644 components/cli/network_create_test.go create mode 100644 components/cli/network_disconnect.go create mode 100644 components/cli/network_disconnect_test.go create mode 100644 components/cli/network_inspect.go create mode 100644 components/cli/network_inspect_test.go create mode 100644 components/cli/network_list.go create mode 100644 components/cli/network_list_test.go create mode 100644 components/cli/network_remove.go create mode 100644 components/cli/network_remove_test.go create mode 100644 components/cli/node_inspect.go create mode 100644 components/cli/node_inspect_test.go create mode 100644 components/cli/node_list.go create mode 100644 components/cli/node_list_test.go create mode 100644 components/cli/node_remove.go create mode 100644 components/cli/node_remove_test.go create mode 100644 components/cli/node_update.go create mode 100644 components/cli/node_update_test.go create mode 100644 components/cli/plugin_disable.go create mode 100644 components/cli/plugin_disable_test.go create mode 100644 components/cli/plugin_enable.go create mode 100644 components/cli/plugin_enable_test.go create mode 100644 components/cli/plugin_inspect.go create mode 100644 components/cli/plugin_inspect_test.go create mode 100644 components/cli/plugin_install.go create mode 100644 components/cli/plugin_list.go create mode 100644 components/cli/plugin_list_test.go create mode 100644 components/cli/plugin_push.go create mode 100644 components/cli/plugin_push_test.go create mode 100644 components/cli/plugin_remove.go create mode 100644 components/cli/plugin_remove_test.go create mode 100644 components/cli/plugin_set.go create mode 100644 components/cli/plugin_set_test.go create mode 100644 components/cli/request.go create mode 100644 components/cli/request_test.go create mode 100644 components/cli/service_create.go create mode 100644 components/cli/service_create_test.go create mode 100644 components/cli/service_inspect.go create mode 100644 components/cli/service_inspect_test.go create mode 100644 components/cli/service_list.go create mode 100644 components/cli/service_list_test.go create mode 100644 components/cli/service_remove.go create mode 100644 components/cli/service_remove_test.go create mode 100644 components/cli/service_update.go create mode 100644 components/cli/service_update_test.go create mode 100644 components/cli/swarm_init.go create mode 100644 components/cli/swarm_init_test.go create mode 100644 components/cli/swarm_inspect.go create mode 100644 components/cli/swarm_inspect_test.go create mode 100644 components/cli/swarm_join.go create mode 100644 components/cli/swarm_join_test.go create mode 100644 components/cli/swarm_leave.go create mode 100644 components/cli/swarm_leave_test.go create mode 100644 components/cli/swarm_update.go create mode 100644 components/cli/swarm_update_test.go create mode 100644 components/cli/task_inspect.go create mode 100644 components/cli/task_inspect_test.go create mode 100644 components/cli/task_list.go create mode 100644 components/cli/task_list_test.go create mode 100644 components/cli/testdata/ca.pem create mode 100644 components/cli/testdata/cert.pem create mode 100644 components/cli/testdata/key.pem create mode 100644 components/cli/transport/cancellable/LICENSE create mode 100644 components/cli/transport/cancellable/canceler.go create mode 100644 components/cli/transport/cancellable/canceler_go14.go create mode 100644 components/cli/transport/cancellable/cancellable.go create mode 100644 components/cli/transport/client.go create mode 100644 components/cli/transport/tlsconfig_clone.go create mode 100644 components/cli/transport/tlsconfig_clone_go17.go create mode 100644 components/cli/transport/transport.go create mode 100644 components/cli/version.go create mode 100644 components/cli/volume_create.go create mode 100644 components/cli/volume_create_test.go create mode 100644 components/cli/volume_inspect.go create mode 100644 components/cli/volume_inspect_test.go create mode 100644 components/cli/volume_list.go create mode 100644 components/cli/volume_list_test.go create mode 100644 components/cli/volume_remove.go create mode 100644 components/cli/volume_remove_test.go diff --git a/components/cli/checkpoint_create.go b/components/cli/checkpoint_create.go new file mode 100644 index 0000000000..0effe498be --- /dev/null +++ b/components/cli/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/components/cli/checkpoint_create_test.go b/components/cli/checkpoint_create_test.go new file mode 100644 index 0000000000..e2ae36e1e0 --- /dev/null +++ b/components/cli/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/components/cli/checkpoint_delete.go b/components/cli/checkpoint_delete.go new file mode 100644 index 0000000000..a4e9ed0c06 --- /dev/null +++ b/components/cli/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/components/cli/checkpoint_delete_test.go b/components/cli/checkpoint_delete_test.go new file mode 100644 index 0000000000..097ab37693 --- /dev/null +++ b/components/cli/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/components/cli/checkpoint_list.go b/components/cli/checkpoint_list.go new file mode 100644 index 0000000000..bb471e0056 --- /dev/null +++ b/components/cli/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/components/cli/checkpoint_list_test.go b/components/cli/checkpoint_list_test.go new file mode 100644 index 0000000000..5960436eb1 --- /dev/null +++ b/components/cli/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/components/cli/client.go b/components/cli/client.go new file mode 100644 index 0000000000..6a85121c6d --- /dev/null +++ b/components/cli/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/components/cli/client_mock_test.go b/components/cli/client_mock_test.go new file mode 100644 index 0000000000..33c247266c --- /dev/null +++ b/components/cli/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/components/cli/client_test.go b/components/cli/client_test.go new file mode 100644 index 0000000000..60af3db029 --- /dev/null +++ b/components/cli/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/components/cli/client_unix.go b/components/cli/client_unix.go new file mode 100644 index 0000000000..89de892c85 --- /dev/null +++ b/components/cli/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/components/cli/client_windows.go b/components/cli/client_windows.go new file mode 100644 index 0000000000..07c0c7a774 --- /dev/null +++ b/components/cli/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/components/cli/container_attach.go b/components/cli/container_attach.go new file mode 100644 index 0000000000..7cfc860fcc --- /dev/null +++ b/components/cli/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/components/cli/container_commit.go b/components/cli/container_commit.go new file mode 100644 index 0000000000..363950cc24 --- /dev/null +++ b/components/cli/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/components/cli/container_commit_test.go b/components/cli/container_commit_test.go new file mode 100644 index 0000000000..3fc3e5cfd0 --- /dev/null +++ b/components/cli/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/components/cli/container_copy.go b/components/cli/container_copy.go new file mode 100644 index 0000000000..8380eeabc9 --- /dev/null +++ b/components/cli/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/components/cli/container_copy_test.go b/components/cli/container_copy_test.go new file mode 100644 index 0000000000..39cd05ac2d --- /dev/null +++ b/components/cli/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/components/cli/container_create.go b/components/cli/container_create.go new file mode 100644 index 0000000000..a862172956 --- /dev/null +++ b/components/cli/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/components/cli/container_create_test.go b/components/cli/container_create_test.go new file mode 100644 index 0000000000..4c14cdc5d1 --- /dev/null +++ b/components/cli/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/components/cli/container_diff.go b/components/cli/container_diff.go new file mode 100644 index 0000000000..1e3e554fc5 --- /dev/null +++ b/components/cli/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/components/cli/container_diff_test.go b/components/cli/container_diff_test.go new file mode 100644 index 0000000000..03ea3354d2 --- /dev/null +++ b/components/cli/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/components/cli/container_exec.go b/components/cli/container_exec.go new file mode 100644 index 0000000000..34173d3194 --- /dev/null +++ b/components/cli/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/components/cli/container_exec_test.go b/components/cli/container_exec_test.go new file mode 100644 index 0000000000..abe824e47b --- /dev/null +++ b/components/cli/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/components/cli/container_export.go b/components/cli/container_export.go new file mode 100644 index 0000000000..52194f3d34 --- /dev/null +++ b/components/cli/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/components/cli/container_export_test.go b/components/cli/container_export_test.go new file mode 100644 index 0000000000..10eba33d2f --- /dev/null +++ b/components/cli/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/components/cli/container_inspect.go b/components/cli/container_inspect.go new file mode 100644 index 0000000000..17f1809747 --- /dev/null +++ b/components/cli/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/components/cli/container_inspect_test.go b/components/cli/container_inspect_test.go new file mode 100644 index 0000000000..0dc8ac3753 --- /dev/null +++ b/components/cli/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/components/cli/container_kill.go b/components/cli/container_kill.go new file mode 100644 index 0000000000..29f80c73ad --- /dev/null +++ b/components/cli/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/components/cli/container_kill_test.go b/components/cli/container_kill_test.go new file mode 100644 index 0000000000..a34a7b5b11 --- /dev/null +++ b/components/cli/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/components/cli/container_list.go b/components/cli/container_list.go new file mode 100644 index 0000000000..a8945d84f1 --- /dev/null +++ b/components/cli/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/components/cli/container_list_test.go b/components/cli/container_list_test.go new file mode 100644 index 0000000000..3aa2101f27 --- /dev/null +++ b/components/cli/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/components/cli/container_logs.go b/components/cli/container_logs.go new file mode 100644 index 0000000000..69056b6321 --- /dev/null +++ b/components/cli/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/components/cli/container_logs_test.go b/components/cli/container_logs_test.go new file mode 100644 index 0000000000..d7f0adc9c0 --- /dev/null +++ b/components/cli/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/components/cli/container_pause.go b/components/cli/container_pause.go new file mode 100644 index 0000000000..412067a782 --- /dev/null +++ b/components/cli/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/components/cli/container_pause_test.go b/components/cli/container_pause_test.go new file mode 100644 index 0000000000..ebd12a6ac7 --- /dev/null +++ b/components/cli/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/components/cli/container_remove.go b/components/cli/container_remove.go new file mode 100644 index 0000000000..3a79590ced --- /dev/null +++ b/components/cli/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/components/cli/container_remove_test.go b/components/cli/container_remove_test.go new file mode 100644 index 0000000000..6e135d6ef2 --- /dev/null +++ b/components/cli/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/components/cli/container_rename.go b/components/cli/container_rename.go new file mode 100644 index 0000000000..0e718da7c6 --- /dev/null +++ b/components/cli/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/components/cli/container_rename_test.go b/components/cli/container_rename_test.go new file mode 100644 index 0000000000..9344bab7db --- /dev/null +++ b/components/cli/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/components/cli/container_resize.go b/components/cli/container_resize.go new file mode 100644 index 0000000000..a7f38b024b --- /dev/null +++ b/components/cli/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/components/cli/container_resize_test.go b/components/cli/container_resize_test.go new file mode 100644 index 0000000000..e0056c88d1 --- /dev/null +++ b/components/cli/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/components/cli/container_restart.go b/components/cli/container_restart.go new file mode 100644 index 0000000000..74d7455f02 --- /dev/null +++ b/components/cli/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/components/cli/container_restart_test.go b/components/cli/container_restart_test.go new file mode 100644 index 0000000000..080656d368 --- /dev/null +++ b/components/cli/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/components/cli/container_start.go b/components/cli/container_start.go new file mode 100644 index 0000000000..44bb0080c0 --- /dev/null +++ b/components/cli/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/components/cli/container_start_test.go b/components/cli/container_start_test.go new file mode 100644 index 0000000000..79f85b332a --- /dev/null +++ b/components/cli/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/components/cli/container_stats.go b/components/cli/container_stats.go new file mode 100644 index 0000000000..2cc67c3af1 --- /dev/null +++ b/components/cli/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/components/cli/container_stats_test.go b/components/cli/container_stats_test.go new file mode 100644 index 0000000000..22ecd6170f --- /dev/null +++ b/components/cli/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/components/cli/container_stop.go b/components/cli/container_stop.go new file mode 100644 index 0000000000..b5418ae8c8 --- /dev/null +++ b/components/cli/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/components/cli/container_stop_test.go b/components/cli/container_stop_test.go new file mode 100644 index 0000000000..4b052f9908 --- /dev/null +++ b/components/cli/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/components/cli/container_top.go b/components/cli/container_top.go new file mode 100644 index 0000000000..4e7270ea22 --- /dev/null +++ b/components/cli/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/components/cli/container_top_test.go b/components/cli/container_top_test.go new file mode 100644 index 0000000000..4df7d82d84 --- /dev/null +++ b/components/cli/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/components/cli/container_unpause.go b/components/cli/container_unpause.go new file mode 100644 index 0000000000..5c76211256 --- /dev/null +++ b/components/cli/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/components/cli/container_unpause_test.go b/components/cli/container_unpause_test.go new file mode 100644 index 0000000000..a5b21bf56c --- /dev/null +++ b/components/cli/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/components/cli/container_update.go b/components/cli/container_update.go new file mode 100644 index 0000000000..48b75bee30 --- /dev/null +++ b/components/cli/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/components/cli/container_update_test.go b/components/cli/container_update_test.go new file mode 100644 index 0000000000..46e34d6936 --- /dev/null +++ b/components/cli/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/components/cli/container_wait.go b/components/cli/container_wait.go new file mode 100644 index 0000000000..8a858f0ea3 --- /dev/null +++ b/components/cli/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/components/cli/container_wait_test.go b/components/cli/container_wait_test.go new file mode 100644 index 0000000000..bf2ba6b925 --- /dev/null +++ b/components/cli/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/components/cli/errors.go b/components/cli/errors.go new file mode 100644 index 0000000000..71e25a7ae1 --- /dev/null +++ b/components/cli/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/components/cli/events.go b/components/cli/events.go new file mode 100644 index 0000000000..0ba7114f94 --- /dev/null +++ b/components/cli/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/components/cli/events_test.go b/components/cli/events_test.go new file mode 100644 index 0000000000..f7cb33f611 --- /dev/null +++ b/components/cli/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/components/cli/hijack.go b/components/cli/hijack.go new file mode 100644 index 0000000000..9376d21b97 --- /dev/null +++ b/components/cli/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/components/cli/image_build.go b/components/cli/image_build.go new file mode 100644 index 0000000000..8dd6744859 --- /dev/null +++ b/components/cli/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/components/cli/image_build_test.go b/components/cli/image_build_test.go new file mode 100644 index 0000000000..8261c54854 --- /dev/null +++ b/components/cli/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/components/cli/image_create.go b/components/cli/image_create.go new file mode 100644 index 0000000000..cf023a7186 --- /dev/null +++ b/components/cli/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/components/cli/image_create_test.go b/components/cli/image_create_test.go new file mode 100644 index 0000000000..a2e001be5d --- /dev/null +++ b/components/cli/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/components/cli/image_history.go b/components/cli/image_history.go new file mode 100644 index 0000000000..acb1ee9278 --- /dev/null +++ b/components/cli/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/components/cli/image_history_test.go b/components/cli/image_history_test.go new file mode 100644 index 0000000000..c9516151b7 --- /dev/null +++ b/components/cli/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/components/cli/image_import.go b/components/cli/image_import.go new file mode 100644 index 0000000000..c6f154b249 --- /dev/null +++ b/components/cli/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/components/cli/image_import_test.go b/components/cli/image_import_test.go new file mode 100644 index 0000000000..b64ca74d7b --- /dev/null +++ b/components/cli/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/components/cli/image_inspect.go b/components/cli/image_inspect.go new file mode 100644 index 0000000000..b3a64ce2f8 --- /dev/null +++ b/components/cli/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/components/cli/image_inspect_test.go b/components/cli/image_inspect_test.go new file mode 100644 index 0000000000..5c7ca2721f --- /dev/null +++ b/components/cli/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/components/cli/image_list.go b/components/cli/image_list.go new file mode 100644 index 0000000000..00f27dc0c9 --- /dev/null +++ b/components/cli/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/components/cli/image_list_test.go b/components/cli/image_list_test.go new file mode 100644 index 0000000000..99ed1964a2 --- /dev/null +++ b/components/cli/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/components/cli/image_load.go b/components/cli/image_load.go new file mode 100644 index 0000000000..77aaf1af36 --- /dev/null +++ b/components/cli/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/components/cli/image_load_test.go b/components/cli/image_load_test.go new file mode 100644 index 0000000000..0ee7cf35a6 --- /dev/null +++ b/components/cli/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/components/cli/image_pull.go b/components/cli/image_pull.go new file mode 100644 index 0000000000..3bffdb70e8 --- /dev/null +++ b/components/cli/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/components/cli/image_pull_test.go b/components/cli/image_pull_test.go new file mode 100644 index 0000000000..c33a6dcc8a --- /dev/null +++ b/components/cli/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/components/cli/image_push.go b/components/cli/image_push.go new file mode 100644 index 0000000000..8e73d28f56 --- /dev/null +++ b/components/cli/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/components/cli/image_push_test.go b/components/cli/image_push_test.go new file mode 100644 index 0000000000..d32f3ef3c7 --- /dev/null +++ b/components/cli/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/components/cli/image_remove.go b/components/cli/image_remove.go new file mode 100644 index 0000000000..839e5311c4 --- /dev/null +++ b/components/cli/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/components/cli/image_remove_test.go b/components/cli/image_remove_test.go new file mode 100644 index 0000000000..696d06729d --- /dev/null +++ b/components/cli/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/components/cli/image_save.go b/components/cli/image_save.go new file mode 100644 index 0000000000..ecac880a32 --- /dev/null +++ b/components/cli/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/components/cli/image_save_test.go b/components/cli/image_save_test.go new file mode 100644 index 0000000000..8ee40c43ae --- /dev/null +++ b/components/cli/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/components/cli/image_search.go b/components/cli/image_search.go new file mode 100644 index 0000000000..b0fcd5c23d --- /dev/null +++ b/components/cli/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/components/cli/image_search_test.go b/components/cli/image_search_test.go new file mode 100644 index 0000000000..2f21b2cc14 --- /dev/null +++ b/components/cli/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/components/cli/image_tag.go b/components/cli/image_tag.go new file mode 100644 index 0000000000..bdbf94add2 --- /dev/null +++ b/components/cli/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/components/cli/image_tag_test.go b/components/cli/image_tag_test.go new file mode 100644 index 0000000000..f3571dfdd3 --- /dev/null +++ b/components/cli/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/components/cli/info.go b/components/cli/info.go new file mode 100644 index 0000000000..ac07961224 --- /dev/null +++ b/components/cli/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/components/cli/info_test.go b/components/cli/info_test.go new file mode 100644 index 0000000000..9d51b1a78b --- /dev/null +++ b/components/cli/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/components/cli/interface.go b/components/cli/interface.go new file mode 100644 index 0000000000..1bfeb6aeb6 --- /dev/null +++ b/components/cli/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/components/cli/interface_experimental.go b/components/cli/interface_experimental.go new file mode 100644 index 0000000000..1ddc517c9a --- /dev/null +++ b/components/cli/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/components/cli/interface_stable.go b/components/cli/interface_stable.go new file mode 100644 index 0000000000..496f522d51 --- /dev/null +++ b/components/cli/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/components/cli/login.go b/components/cli/login.go new file mode 100644 index 0000000000..d8d277ccba --- /dev/null +++ b/components/cli/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/components/cli/network_connect.go b/components/cli/network_connect.go new file mode 100644 index 0000000000..c022c17b5b --- /dev/null +++ b/components/cli/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/components/cli/network_connect_test.go b/components/cli/network_connect_test.go new file mode 100644 index 0000000000..95b149e685 --- /dev/null +++ b/components/cli/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/components/cli/network_create.go b/components/cli/network_create.go new file mode 100644 index 0000000000..4067a541ff --- /dev/null +++ b/components/cli/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/components/cli/network_create_test.go b/components/cli/network_create_test.go new file mode 100644 index 0000000000..611ed8173e --- /dev/null +++ b/components/cli/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/components/cli/network_disconnect.go b/components/cli/network_disconnect.go new file mode 100644 index 0000000000..24b58e3c12 --- /dev/null +++ b/components/cli/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/components/cli/network_disconnect_test.go b/components/cli/network_disconnect_test.go new file mode 100644 index 0000000000..d9dbb67159 --- /dev/null +++ b/components/cli/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/components/cli/network_inspect.go b/components/cli/network_inspect.go new file mode 100644 index 0000000000..5ad4ea5bf3 --- /dev/null +++ b/components/cli/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/components/cli/network_inspect_test.go b/components/cli/network_inspect_test.go new file mode 100644 index 0000000000..a6eb626c67 --- /dev/null +++ b/components/cli/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/components/cli/network_list.go b/components/cli/network_list.go new file mode 100644 index 0000000000..e566a93e23 --- /dev/null +++ b/components/cli/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/components/cli/network_list_test.go b/components/cli/network_list_test.go new file mode 100644 index 0000000000..cb66139271 --- /dev/null +++ b/components/cli/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/components/cli/network_remove.go b/components/cli/network_remove.go new file mode 100644 index 0000000000..6bd6748924 --- /dev/null +++ b/components/cli/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/components/cli/network_remove_test.go b/components/cli/network_remove_test.go new file mode 100644 index 0000000000..d8cfa0ed6e --- /dev/null +++ b/components/cli/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/components/cli/node_inspect.go b/components/cli/node_inspect.go new file mode 100644 index 0000000000..abf505d29c --- /dev/null +++ b/components/cli/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/components/cli/node_inspect_test.go b/components/cli/node_inspect_test.go new file mode 100644 index 0000000000..bf67728311 --- /dev/null +++ b/components/cli/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/components/cli/node_list.go b/components/cli/node_list.go new file mode 100644 index 0000000000..0716875ccc --- /dev/null +++ b/components/cli/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/components/cli/node_list_test.go b/components/cli/node_list_test.go new file mode 100644 index 0000000000..899ac7f455 --- /dev/null +++ b/components/cli/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/components/cli/node_remove.go b/components/cli/node_remove.go new file mode 100644 index 0000000000..0a77f3d578 --- /dev/null +++ b/components/cli/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/components/cli/node_remove_test.go b/components/cli/node_remove_test.go new file mode 100644 index 0000000000..9fdf2d7eb3 --- /dev/null +++ b/components/cli/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/components/cli/node_update.go b/components/cli/node_update.go new file mode 100644 index 0000000000..3ca9760282 --- /dev/null +++ b/components/cli/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/components/cli/node_update_test.go b/components/cli/node_update_test.go new file mode 100644 index 0000000000..1acf65854a --- /dev/null +++ b/components/cli/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/components/cli/plugin_disable.go b/components/cli/plugin_disable.go new file mode 100644 index 0000000000..893fc6e823 --- /dev/null +++ b/components/cli/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/components/cli/plugin_disable_test.go b/components/cli/plugin_disable_test.go new file mode 100644 index 0000000000..f37c157866 --- /dev/null +++ b/components/cli/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/components/cli/plugin_enable.go b/components/cli/plugin_enable.go new file mode 100644 index 0000000000..84422abc79 --- /dev/null +++ b/components/cli/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/components/cli/plugin_enable_test.go b/components/cli/plugin_enable_test.go new file mode 100644 index 0000000000..fc0fe226a9 --- /dev/null +++ b/components/cli/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/components/cli/plugin_inspect.go b/components/cli/plugin_inspect.go new file mode 100644 index 0000000000..7ba8db57a8 --- /dev/null +++ b/components/cli/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/components/cli/plugin_inspect_test.go b/components/cli/plugin_inspect_test.go new file mode 100644 index 0000000000..19f829b2de --- /dev/null +++ b/components/cli/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/components/cli/plugin_install.go b/components/cli/plugin_install.go new file mode 100644 index 0000000000..9ee32eea92 --- /dev/null +++ b/components/cli/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/components/cli/plugin_list.go b/components/cli/plugin_list.go new file mode 100644 index 0000000000..48b470247b --- /dev/null +++ b/components/cli/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/components/cli/plugin_list_test.go b/components/cli/plugin_list_test.go new file mode 100644 index 0000000000..92aee61187 --- /dev/null +++ b/components/cli/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/components/cli/plugin_push.go b/components/cli/plugin_push.go new file mode 100644 index 0000000000..3afea5ed79 --- /dev/null +++ b/components/cli/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/components/cli/plugin_push_test.go b/components/cli/plugin_push_test.go new file mode 100644 index 0000000000..b77ea00273 --- /dev/null +++ b/components/cli/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/components/cli/plugin_remove.go b/components/cli/plugin_remove.go new file mode 100644 index 0000000000..1483f2854d --- /dev/null +++ b/components/cli/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/components/cli/plugin_remove_test.go b/components/cli/plugin_remove_test.go new file mode 100644 index 0000000000..de565f441b --- /dev/null +++ b/components/cli/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/components/cli/plugin_set.go b/components/cli/plugin_set.go new file mode 100644 index 0000000000..fb40f38b22 --- /dev/null +++ b/components/cli/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/components/cli/plugin_set_test.go b/components/cli/plugin_set_test.go new file mode 100644 index 0000000000..128dee04be --- /dev/null +++ b/components/cli/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/components/cli/request.go b/components/cli/request.go new file mode 100644 index 0000000000..024e973520 --- /dev/null +++ b/components/cli/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/components/cli/request_test.go b/components/cli/request_test.go new file mode 100644 index 0000000000..446adf9c66 --- /dev/null +++ b/components/cli/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/components/cli/service_create.go b/components/cli/service_create.go new file mode 100644 index 0000000000..3d1be225bd --- /dev/null +++ b/components/cli/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/components/cli/service_create_test.go b/components/cli/service_create_test.go new file mode 100644 index 0000000000..a79f040c0a --- /dev/null +++ b/components/cli/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/components/cli/service_inspect.go b/components/cli/service_inspect.go new file mode 100644 index 0000000000..ca71cbde1a --- /dev/null +++ b/components/cli/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/components/cli/service_inspect_test.go b/components/cli/service_inspect_test.go new file mode 100644 index 0000000000..e4eafff5d7 --- /dev/null +++ b/components/cli/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/components/cli/service_list.go b/components/cli/service_list.go new file mode 100644 index 0000000000..4ebc9f3011 --- /dev/null +++ b/components/cli/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/components/cli/service_list_test.go b/components/cli/service_list_test.go new file mode 100644 index 0000000000..6e6851a3a5 --- /dev/null +++ b/components/cli/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/components/cli/service_remove.go b/components/cli/service_remove.go new file mode 100644 index 0000000000..a9331f92c2 --- /dev/null +++ b/components/cli/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/components/cli/service_remove_test.go b/components/cli/service_remove_test.go new file mode 100644 index 0000000000..e1316f959b --- /dev/null +++ b/components/cli/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/components/cli/service_update.go b/components/cli/service_update.go new file mode 100644 index 0000000000..c5d07e8394 --- /dev/null +++ b/components/cli/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/components/cli/service_update_test.go b/components/cli/service_update_test.go new file mode 100644 index 0000000000..bd616c09bf --- /dev/null +++ b/components/cli/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/components/cli/swarm_init.go b/components/cli/swarm_init.go new file mode 100644 index 0000000000..fd45d066e3 --- /dev/null +++ b/components/cli/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/components/cli/swarm_init_test.go b/components/cli/swarm_init_test.go new file mode 100644 index 0000000000..077c8c4efb --- /dev/null +++ b/components/cli/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/components/cli/swarm_inspect.go b/components/cli/swarm_inspect.go new file mode 100644 index 0000000000..6d95cfc05e --- /dev/null +++ b/components/cli/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/components/cli/swarm_inspect_test.go b/components/cli/swarm_inspect_test.go new file mode 100644 index 0000000000..7143e77181 --- /dev/null +++ b/components/cli/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/components/cli/swarm_join.go b/components/cli/swarm_join.go new file mode 100644 index 0000000000..cda99930eb --- /dev/null +++ b/components/cli/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/components/cli/swarm_join_test.go b/components/cli/swarm_join_test.go new file mode 100644 index 0000000000..922716d85f --- /dev/null +++ b/components/cli/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/components/cli/swarm_leave.go b/components/cli/swarm_leave.go new file mode 100644 index 0000000000..a4df732174 --- /dev/null +++ b/components/cli/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/components/cli/swarm_leave_test.go b/components/cli/swarm_leave_test.go new file mode 100644 index 0000000000..d0bef2b257 --- /dev/null +++ b/components/cli/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/components/cli/swarm_update.go b/components/cli/swarm_update.go new file mode 100644 index 0000000000..f0be145ba2 --- /dev/null +++ b/components/cli/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/components/cli/swarm_update_test.go b/components/cli/swarm_update_test.go new file mode 100644 index 0000000000..ecf1731e5b --- /dev/null +++ b/components/cli/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/components/cli/task_inspect.go b/components/cli/task_inspect.go new file mode 100644 index 0000000000..bc8058fc32 --- /dev/null +++ b/components/cli/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/components/cli/task_inspect_test.go b/components/cli/task_inspect_test.go new file mode 100644 index 0000000000..2c73b37642 --- /dev/null +++ b/components/cli/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/components/cli/task_list.go b/components/cli/task_list.go new file mode 100644 index 0000000000..07c8324c83 --- /dev/null +++ b/components/cli/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/components/cli/task_list_test.go b/components/cli/task_list_test.go new file mode 100644 index 0000000000..b520ab589f --- /dev/null +++ b/components/cli/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/components/cli/testdata/ca.pem b/components/cli/testdata/ca.pem new file mode 100644 index 0000000000..ad14d47065 --- /dev/null +++ b/components/cli/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/components/cli/testdata/cert.pem b/components/cli/testdata/cert.pem new file mode 100644 index 0000000000..9000ffb32b --- /dev/null +++ b/components/cli/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/components/cli/testdata/key.pem b/components/cli/testdata/key.pem new file mode 100644 index 0000000000..c0869dfc1a --- /dev/null +++ b/components/cli/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/components/cli/transport/cancellable/LICENSE b/components/cli/transport/cancellable/LICENSE new file mode 100644 index 0000000000..6a66aea5ea --- /dev/null +++ b/components/cli/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/components/cli/transport/cancellable/canceler.go b/components/cli/transport/cancellable/canceler.go new file mode 100644 index 0000000000..62770b777b --- /dev/null +++ b/components/cli/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/components/cli/transport/cancellable/canceler_go14.go b/components/cli/transport/cancellable/canceler_go14.go new file mode 100644 index 0000000000..dd2723d94f --- /dev/null +++ b/components/cli/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/components/cli/transport/cancellable/cancellable.go b/components/cli/transport/cancellable/cancellable.go new file mode 100644 index 0000000000..1f8eac5c1c --- /dev/null +++ b/components/cli/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/components/cli/transport/client.go b/components/cli/transport/client.go new file mode 100644 index 0000000000..13d4b3ab3d --- /dev/null +++ b/components/cli/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/components/cli/transport/tlsconfig_clone.go b/components/cli/transport/tlsconfig_clone.go new file mode 100644 index 0000000000..033d5dc0f2 --- /dev/null +++ b/components/cli/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/components/cli/transport/tlsconfig_clone_go17.go b/components/cli/transport/tlsconfig_clone_go17.go new file mode 100644 index 0000000000..a28c9141b2 --- /dev/null +++ b/components/cli/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/components/cli/transport/transport.go b/components/cli/transport/transport.go new file mode 100644 index 0000000000..ff28af1855 --- /dev/null +++ b/components/cli/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/components/cli/version.go b/components/cli/version.go new file mode 100644 index 0000000000..933ceb4a49 --- /dev/null +++ b/components/cli/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/components/cli/volume_create.go b/components/cli/volume_create.go new file mode 100644 index 0000000000..f3a79f1e11 --- /dev/null +++ b/components/cli/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/components/cli/volume_create_test.go b/components/cli/volume_create_test.go new file mode 100644 index 0000000000..d3cfa7132f --- /dev/null +++ b/components/cli/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/components/cli/volume_inspect.go b/components/cli/volume_inspect.go new file mode 100644 index 0000000000..3860e9b22c --- /dev/null +++ b/components/cli/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/components/cli/volume_inspect_test.go b/components/cli/volume_inspect_test.go new file mode 100644 index 0000000000..4b9f47358d --- /dev/null +++ b/components/cli/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/components/cli/volume_list.go b/components/cli/volume_list.go new file mode 100644 index 0000000000..44f03cfac7 --- /dev/null +++ b/components/cli/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/components/cli/volume_list_test.go b/components/cli/volume_list_test.go new file mode 100644 index 0000000000..d30d9fcd52 --- /dev/null +++ b/components/cli/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/components/cli/volume_remove.go b/components/cli/volume_remove.go new file mode 100644 index 0000000000..3d5aeff252 --- /dev/null +++ b/components/cli/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/components/cli/volume_remove_test.go b/components/cli/volume_remove_test.go new file mode 100644 index 0000000000..0675bfd458 --- /dev/null +++ b/components/cli/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 eb96b2810d86607259f09116f0ee3545215158f6 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 8 Sep 2016 04:38:55 +0000 Subject: [PATCH 113/978] client: transport: fix tlsconfig Clone() on different Golang versions Signed-off-by: Akihiro Suda Upstream-commit: d675c815775ec4814c84c3bb4514721f332b1bca Component: cli --- components/cli/transport/tlsconfig_clone.go | 2 +- .../cli/transport/tlsconfig_clone_go16.go | 31 +++++++++++++++++++ .../cli/transport/tlsconfig_clone_go17.go | 2 +- 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 components/cli/transport/tlsconfig_clone_go16.go diff --git a/components/cli/transport/tlsconfig_clone.go b/components/cli/transport/tlsconfig_clone.go index 033d5dc0f2..034bc01d33 100644 --- a/components/cli/transport/tlsconfig_clone.go +++ b/components/cli/transport/tlsconfig_clone.go @@ -1,4 +1,4 @@ -// +build !go1.7,!windows +// +build go1.8 package transport diff --git a/components/cli/transport/tlsconfig_clone_go16.go b/components/cli/transport/tlsconfig_clone_go16.go new file mode 100644 index 0000000000..12f13e4694 --- /dev/null +++ b/components/cli/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/components/cli/transport/tlsconfig_clone_go17.go b/components/cli/transport/tlsconfig_clone_go17.go index a28c9141b2..50bf389e43 100644 --- a/components/cli/transport/tlsconfig_clone_go17.go +++ b/components/cli/transport/tlsconfig_clone_go17.go @@ -1,4 +1,4 @@ -// +build go1.7 +// +build go1.7,!go1.8 package transport From 964a552e76d54209e030a457916368f2892cfd48 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 8 Sep 2016 13:11:39 -0400 Subject: [PATCH 114/978] Move api/client -> cli/command Using gomvpkg -from github.com/docker/docker/api/client -to github.com/docker/docker/cli/command -vcs_mv_cmd 'git mv {{.Src}} {{.Dst}}' Signed-off-by: Daniel Nephin Upstream-commit: 3bd1eb4b762a5860f6fd552020d5a5305b1453e0 Component: cli --- .../cli/command/bundlefile/bundlefile.go | 71 +++ .../cli/command/bundlefile/bundlefile_test.go | 79 +++ components/cli/command/cli.go | 164 +++++ components/cli/command/commands/commands.go | 71 +++ components/cli/command/container/attach.go | 130 ++++ components/cli/command/container/commit.go | 76 +++ components/cli/command/container/cp.go | 303 +++++++++ components/cli/command/container/create.go | 217 +++++++ components/cli/command/container/diff.go | 58 ++ components/cli/command/container/exec.go | 192 ++++++ components/cli/command/container/exec_test.go | 117 ++++ components/cli/command/container/export.go | 59 ++ components/cli/command/container/hijack.go | 121 ++++ components/cli/command/container/kill.go | 53 ++ components/cli/command/container/logs.go | 87 +++ components/cli/command/container/pause.go | 48 ++ components/cli/command/container/port.go | 78 +++ components/cli/command/container/ps.go | 142 +++++ components/cli/command/container/ps_test.go | 74 +++ components/cli/command/container/rename.go | 51 ++ components/cli/command/container/restart.go | 55 ++ components/cli/command/container/rm.go | 76 +++ components/cli/command/container/run.go | 288 +++++++++ components/cli/command/container/start.go | 161 +++++ components/cli/command/container/stats.go | 233 +++++++ .../cli/command/container/stats_helpers.go | 238 +++++++ .../cli/command/container/stats_unit_test.go | 45 ++ components/cli/command/container/stop.go | 56 ++ components/cli/command/container/top.go | 58 ++ components/cli/command/container/tty.go | 103 +++ components/cli/command/container/unpause.go | 49 ++ components/cli/command/container/update.go | 157 +++++ components/cli/command/container/utils.go | 92 +++ components/cli/command/container/wait.go | 50 ++ components/cli/command/credentials.go | 44 ++ components/cli/command/formatter/container.go | 208 ++++++ .../cli/command/formatter/container_test.go | 404 ++++++++++++ components/cli/command/formatter/custom.go | 50 ++ .../cli/command/formatter/custom_test.go | 28 + components/cli/command/formatter/formatter.go | 90 +++ components/cli/command/formatter/image.go | 229 +++++++ .../cli/command/formatter/image_test.go | 345 ++++++++++ components/cli/command/formatter/network.go | 129 ++++ .../cli/command/formatter/network_test.go | 201 ++++++ components/cli/command/formatter/volume.go | 114 ++++ .../cli/command/formatter/volume_test.go | 183 ++++++ .../cli/command/idresolver/idresolver.go | 70 ++ components/cli/command/image/build.go | 452 +++++++++++++ components/cli/command/image/history.go | 99 +++ components/cli/command/image/images.go | 103 +++ components/cli/command/image/import.go | 88 +++ components/cli/command/image/load.go | 67 ++ components/cli/command/image/pull.go | 93 +++ components/cli/command/image/push.go | 61 ++ components/cli/command/image/remove.go | 70 ++ components/cli/command/image/save.go | 57 ++ components/cli/command/image/search.go | 135 ++++ components/cli/command/image/tag.go | 41 ++ components/cli/command/in.go | 75 +++ components/cli/command/inspect/inspector.go | 195 ++++++ .../cli/command/inspect/inspector_test.go | 221 +++++++ components/cli/command/network/cmd.go | 31 + components/cli/command/network/connect.go | 64 ++ components/cli/command/network/create.go | 225 +++++++ components/cli/command/network/disconnect.go | 41 ++ components/cli/command/network/inspect.go | 45 ++ components/cli/command/network/list.go | 96 +++ components/cli/command/network/remove.go | 43 ++ components/cli/command/node/cmd.go | 47 ++ components/cli/command/node/demote.go | 36 ++ components/cli/command/node/inspect.go | 144 +++++ components/cli/command/node/list.go | 111 ++++ components/cli/command/node/opts.go | 60 ++ components/cli/command/node/promote.go | 36 ++ components/cli/command/node/ps.go | 69 ++ components/cli/command/node/remove.go | 46 ++ components/cli/command/node/update.go | 121 ++++ components/cli/command/out.go | 69 ++ components/cli/command/plugin/cmd.go | 12 + .../cli/command/plugin/cmd_experimental.go | 36 ++ components/cli/command/plugin/disable.go | 45 ++ components/cli/command/plugin/enable.go | 45 ++ components/cli/command/plugin/inspect.go | 59 ++ components/cli/command/plugin/install.go | 103 +++ components/cli/command/plugin/list.go | 62 ++ components/cli/command/plugin/push.go | 55 ++ components/cli/command/plugin/remove.go | 69 ++ components/cli/command/plugin/set.go | 42 ++ components/cli/command/registry.go | 193 ++++++ components/cli/command/registry/login.go | 85 +++ components/cli/command/registry/logout.go | 77 +++ components/cli/command/service/cmd.go | 32 + components/cli/command/service/create.go | 72 +++ components/cli/command/service/inspect.go | 188 ++++++ .../cli/command/service/inspect_test.go | 84 +++ components/cli/command/service/list.go | 133 ++++ components/cli/command/service/opts.go | 567 +++++++++++++++++ components/cli/command/service/opts_test.go | 176 ++++++ components/cli/command/service/ps.go | 71 +++ components/cli/command/service/remove.go | 47 ++ components/cli/command/service/scale.go | 88 +++ components/cli/command/service/update.go | 504 +++++++++++++++ components/cli/command/service/update_test.go | 198 ++++++ components/cli/command/stack/cmd.go | 39 ++ components/cli/command/stack/cmd_stub.go | 18 + components/cli/command/stack/common.go | 50 ++ components/cli/command/stack/config.go | 41 ++ components/cli/command/stack/deploy.go | 236 +++++++ components/cli/command/stack/opts.go | 49 ++ components/cli/command/stack/ps.go | 72 +++ components/cli/command/stack/remove.go | 75 +++ components/cli/command/stack/services.go | 87 +++ components/cli/command/swarm/cmd.go | 30 + components/cli/command/swarm/init.go | 81 +++ components/cli/command/swarm/join.go | 75 +++ components/cli/command/swarm/join_token.go | 105 +++ components/cli/command/swarm/leave.go | 44 ++ components/cli/command/swarm/opts.go | 179 ++++++ components/cli/command/swarm/opts_test.go | 37 ++ components/cli/command/swarm/update.go | 82 +++ components/cli/command/system/events.go | 115 ++++ components/cli/command/system/events_utils.go | 66 ++ components/cli/command/system/info.go | 261 ++++++++ components/cli/command/system/inspect.go | 136 ++++ components/cli/command/system/version.go | 110 ++++ components/cli/command/task/print.go | 100 +++ components/cli/command/trust.go | 598 ++++++++++++++++++ components/cli/command/trust_test.go | 56 ++ components/cli/command/utils.go | 59 ++ components/cli/command/volume/cmd.go | 48 ++ components/cli/command/volume/create.go | 110 ++++ components/cli/command/volume/inspect.go | 55 ++ components/cli/command/volume/list.go | 108 ++++ components/cli/command/volume/remove.go | 68 ++ 134 files changed, 15221 insertions(+) create mode 100644 components/cli/command/bundlefile/bundlefile.go create mode 100644 components/cli/command/bundlefile/bundlefile_test.go create mode 100644 components/cli/command/cli.go create mode 100644 components/cli/command/commands/commands.go create mode 100644 components/cli/command/container/attach.go create mode 100644 components/cli/command/container/commit.go create mode 100644 components/cli/command/container/cp.go create mode 100644 components/cli/command/container/create.go create mode 100644 components/cli/command/container/diff.go create mode 100644 components/cli/command/container/exec.go create mode 100644 components/cli/command/container/exec_test.go create mode 100644 components/cli/command/container/export.go create mode 100644 components/cli/command/container/hijack.go create mode 100644 components/cli/command/container/kill.go create mode 100644 components/cli/command/container/logs.go create mode 100644 components/cli/command/container/pause.go create mode 100644 components/cli/command/container/port.go create mode 100644 components/cli/command/container/ps.go create mode 100644 components/cli/command/container/ps_test.go create mode 100644 components/cli/command/container/rename.go create mode 100644 components/cli/command/container/restart.go create mode 100644 components/cli/command/container/rm.go create mode 100644 components/cli/command/container/run.go create mode 100644 components/cli/command/container/start.go create mode 100644 components/cli/command/container/stats.go create mode 100644 components/cli/command/container/stats_helpers.go create mode 100644 components/cli/command/container/stats_unit_test.go create mode 100644 components/cli/command/container/stop.go create mode 100644 components/cli/command/container/top.go create mode 100644 components/cli/command/container/tty.go create mode 100644 components/cli/command/container/unpause.go create mode 100644 components/cli/command/container/update.go create mode 100644 components/cli/command/container/utils.go create mode 100644 components/cli/command/container/wait.go create mode 100644 components/cli/command/credentials.go create mode 100644 components/cli/command/formatter/container.go create mode 100644 components/cli/command/formatter/container_test.go create mode 100644 components/cli/command/formatter/custom.go create mode 100644 components/cli/command/formatter/custom_test.go create mode 100644 components/cli/command/formatter/formatter.go create mode 100644 components/cli/command/formatter/image.go create mode 100644 components/cli/command/formatter/image_test.go create mode 100644 components/cli/command/formatter/network.go create mode 100644 components/cli/command/formatter/network_test.go create mode 100644 components/cli/command/formatter/volume.go create mode 100644 components/cli/command/formatter/volume_test.go create mode 100644 components/cli/command/idresolver/idresolver.go create mode 100644 components/cli/command/image/build.go create mode 100644 components/cli/command/image/history.go create mode 100644 components/cli/command/image/images.go create mode 100644 components/cli/command/image/import.go create mode 100644 components/cli/command/image/load.go create mode 100644 components/cli/command/image/pull.go create mode 100644 components/cli/command/image/push.go create mode 100644 components/cli/command/image/remove.go create mode 100644 components/cli/command/image/save.go create mode 100644 components/cli/command/image/search.go create mode 100644 components/cli/command/image/tag.go create mode 100644 components/cli/command/in.go create mode 100644 components/cli/command/inspect/inspector.go create mode 100644 components/cli/command/inspect/inspector_test.go create mode 100644 components/cli/command/network/cmd.go create mode 100644 components/cli/command/network/connect.go create mode 100644 components/cli/command/network/create.go create mode 100644 components/cli/command/network/disconnect.go create mode 100644 components/cli/command/network/inspect.go create mode 100644 components/cli/command/network/list.go create mode 100644 components/cli/command/network/remove.go create mode 100644 components/cli/command/node/cmd.go create mode 100644 components/cli/command/node/demote.go create mode 100644 components/cli/command/node/inspect.go create mode 100644 components/cli/command/node/list.go create mode 100644 components/cli/command/node/opts.go create mode 100644 components/cli/command/node/promote.go create mode 100644 components/cli/command/node/ps.go create mode 100644 components/cli/command/node/remove.go create mode 100644 components/cli/command/node/update.go create mode 100644 components/cli/command/out.go create mode 100644 components/cli/command/plugin/cmd.go create mode 100644 components/cli/command/plugin/cmd_experimental.go create mode 100644 components/cli/command/plugin/disable.go create mode 100644 components/cli/command/plugin/enable.go create mode 100644 components/cli/command/plugin/inspect.go create mode 100644 components/cli/command/plugin/install.go create mode 100644 components/cli/command/plugin/list.go create mode 100644 components/cli/command/plugin/push.go create mode 100644 components/cli/command/plugin/remove.go create mode 100644 components/cli/command/plugin/set.go create mode 100644 components/cli/command/registry.go create mode 100644 components/cli/command/registry/login.go create mode 100644 components/cli/command/registry/logout.go create mode 100644 components/cli/command/service/cmd.go create mode 100644 components/cli/command/service/create.go create mode 100644 components/cli/command/service/inspect.go create mode 100644 components/cli/command/service/inspect_test.go create mode 100644 components/cli/command/service/list.go create mode 100644 components/cli/command/service/opts.go create mode 100644 components/cli/command/service/opts_test.go create mode 100644 components/cli/command/service/ps.go create mode 100644 components/cli/command/service/remove.go create mode 100644 components/cli/command/service/scale.go create mode 100644 components/cli/command/service/update.go create mode 100644 components/cli/command/service/update_test.go create mode 100644 components/cli/command/stack/cmd.go create mode 100644 components/cli/command/stack/cmd_stub.go create mode 100644 components/cli/command/stack/common.go create mode 100644 components/cli/command/stack/config.go create mode 100644 components/cli/command/stack/deploy.go create mode 100644 components/cli/command/stack/opts.go create mode 100644 components/cli/command/stack/ps.go create mode 100644 components/cli/command/stack/remove.go create mode 100644 components/cli/command/stack/services.go create mode 100644 components/cli/command/swarm/cmd.go create mode 100644 components/cli/command/swarm/init.go create mode 100644 components/cli/command/swarm/join.go create mode 100644 components/cli/command/swarm/join_token.go create mode 100644 components/cli/command/swarm/leave.go create mode 100644 components/cli/command/swarm/opts.go create mode 100644 components/cli/command/swarm/opts_test.go create mode 100644 components/cli/command/swarm/update.go create mode 100644 components/cli/command/system/events.go create mode 100644 components/cli/command/system/events_utils.go create mode 100644 components/cli/command/system/info.go create mode 100644 components/cli/command/system/inspect.go create mode 100644 components/cli/command/system/version.go create mode 100644 components/cli/command/task/print.go create mode 100644 components/cli/command/trust.go create mode 100644 components/cli/command/trust_test.go create mode 100644 components/cli/command/utils.go create mode 100644 components/cli/command/volume/cmd.go create mode 100644 components/cli/command/volume/create.go create mode 100644 components/cli/command/volume/inspect.go create mode 100644 components/cli/command/volume/list.go create mode 100644 components/cli/command/volume/remove.go diff --git a/components/cli/command/bundlefile/bundlefile.go b/components/cli/command/bundlefile/bundlefile.go new file mode 100644 index 0000000000..75c2d07433 --- /dev/null +++ b/components/cli/command/bundlefile/bundlefile.go @@ -0,0 +1,71 @@ +// +build experimental + +package bundlefile + +import ( + "encoding/json" + "fmt" + "io" +) + +// Bundlefile stores the contents of a bundlefile +type Bundlefile struct { + Version string + Services map[string]Service +} + +// Service is a service from a bundlefile +type Service struct { + Image string + Command []string `json:",omitempty"` + Args []string `json:",omitempty"` + Env []string `json:",omitempty"` + Labels map[string]string `json:",omitempty"` + Ports []Port `json:",omitempty"` + WorkingDir *string `json:",omitempty"` + User *string `json:",omitempty"` + Networks []string `json:",omitempty"` +} + +// Port is a port as defined in a bundlefile +type Port struct { + Protocol string + Port uint32 +} + +// LoadFile loads a bundlefile from a path to the file +func LoadFile(reader io.Reader) (*Bundlefile, error) { + bundlefile := &Bundlefile{} + + decoder := json.NewDecoder(reader) + if err := decoder.Decode(bundlefile); err != nil { + switch jsonErr := err.(type) { + case *json.SyntaxError: + return nil, fmt.Errorf( + "JSON syntax error at byte %v: %s", + jsonErr.Offset, + jsonErr.Error()) + case *json.UnmarshalTypeError: + return nil, fmt.Errorf( + "Unexpected type at byte %v. Expected %s but received %s.", + jsonErr.Offset, + jsonErr.Type, + jsonErr.Value) + } + return nil, err + } + + return bundlefile, nil +} + +// Print writes the contents of the bundlefile to the output writer +// as human readable json +func Print(out io.Writer, bundle *Bundlefile) error { + bytes, err := json.MarshalIndent(*bundle, "", " ") + if err != nil { + return err + } + + _, err = out.Write(bytes) + return err +} diff --git a/components/cli/command/bundlefile/bundlefile_test.go b/components/cli/command/bundlefile/bundlefile_test.go new file mode 100644 index 0000000000..1ff8235ff8 --- /dev/null +++ b/components/cli/command/bundlefile/bundlefile_test.go @@ -0,0 +1,79 @@ +// +build experimental + +package bundlefile + +import ( + "bytes" + "strings" + "testing" + + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestLoadFileV01Success(t *testing.T) { + reader := strings.NewReader(`{ + "Version": "0.1", + "Services": { + "redis": { + "Image": "redis@sha256:4b24131101fa0117bcaa18ac37055fffd9176aa1a240392bb8ea85e0be50f2ce", + "Networks": ["default"] + }, + "web": { + "Image": "dockercloud/hello-world@sha256:fe79a2cfbd17eefc344fb8419420808df95a1e22d93b7f621a7399fd1e9dca1d", + "Networks": ["default"], + "User": "web" + } + } + }`) + + bundle, err := LoadFile(reader) + assert.NilError(t, err) + assert.Equal(t, bundle.Version, "0.1") + assert.Equal(t, len(bundle.Services), 2) +} + +func TestLoadFileSyntaxError(t *testing.T) { + reader := strings.NewReader(`{ + "Version": "0.1", + "Services": unquoted string + }`) + + _, err := LoadFile(reader) + assert.Error(t, err, "syntax error at byte 37: invalid character 'u'") +} + +func TestLoadFileTypeError(t *testing.T) { + reader := strings.NewReader(`{ + "Version": "0.1", + "Services": { + "web": { + "Image": "redis", + "Networks": "none" + } + } + }`) + + _, err := LoadFile(reader) + assert.Error(t, err, "Unexpected type at byte 94. Expected []string but received string") +} + +func TestPrint(t *testing.T) { + var buffer bytes.Buffer + bundle := &Bundlefile{ + Version: "0.1", + Services: map[string]Service{ + "web": { + Image: "image", + Command: []string{"echo", "something"}, + }, + }, + } + assert.NilError(t, Print(&buffer, bundle)) + output := buffer.String() + assert.Contains(t, output, "\"Image\": \"image\"") + assert.Contains(t, output, + `"Command": [ + "echo", + "something" + ]`) +} diff --git a/components/cli/command/cli.go b/components/cli/command/cli.go new file mode 100644 index 0000000000..6194c7fe93 --- /dev/null +++ b/components/cli/command/cli.go @@ -0,0 +1,164 @@ +package command + +import ( + "errors" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "runtime" + + "github.com/docker/docker/api" + cliflags "github.com/docker/docker/cli/flags" + "github.com/docker/docker/cliconfig" + "github.com/docker/docker/cliconfig/configfile" + "github.com/docker/docker/cliconfig/credentials" + "github.com/docker/docker/client" + "github.com/docker/docker/dockerversion" + dopts "github.com/docker/docker/opts" + "github.com/docker/go-connections/sockets" + "github.com/docker/go-connections/tlsconfig" +) + +// DockerCli represents the docker command line client. +// Instances of the client can be returned from NewDockerCli. +type DockerCli struct { + configFile *configfile.ConfigFile + in *InStream + out *OutStream + err io.Writer + keyFile string + client client.APIClient +} + +// Client returns the APIClient +func (cli *DockerCli) Client() client.APIClient { + return cli.client +} + +// Out returns the writer used for stdout +func (cli *DockerCli) Out() *OutStream { + return cli.out +} + +// Err returns the writer used for stderr +func (cli *DockerCli) Err() io.Writer { + return cli.err +} + +// In returns the reader used for stdin +func (cli *DockerCli) In() *InStream { + return cli.in +} + +// ConfigFile returns the ConfigFile +func (cli *DockerCli) ConfigFile() *configfile.ConfigFile { + return cli.configFile +} + +// Initialize the dockerCli runs initialization that must happen after command +// line flags are parsed. +func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error { + cli.configFile = LoadDefaultConfigFile(cli.err) + + var err error + cli.client, err = NewAPIClientFromFlags(opts.Common, cli.configFile) + if err != nil { + return err + } + if opts.Common.TrustKey == "" { + cli.keyFile = filepath.Join(cliconfig.ConfigDir(), cliflags.DefaultTrustKeyFile) + } else { + cli.keyFile = opts.Common.TrustKey + } + + return nil +} + +// NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err. +func NewDockerCli(in io.ReadCloser, out, err io.Writer) *DockerCli { + return &DockerCli{in: NewInStream(in), out: NewOutStream(out), err: err} +} + +// LoadDefaultConfigFile attempts to load the default config file and returns +// an initialized ConfigFile struct if none is found. +func LoadDefaultConfigFile(err io.Writer) *configfile.ConfigFile { + configFile, e := cliconfig.Load(cliconfig.ConfigDir()) + if e != nil { + fmt.Fprintf(err, "WARNING: Error loading config file:%v\n", e) + } + if !configFile.ContainsAuth() { + credentials.DetectDefaultStore(configFile) + } + return configFile +} + +// NewAPIClientFromFlags creates a new APIClient from command line flags +func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.ConfigFile) (client.APIClient, error) { + host, err := getServerHost(opts.Hosts, opts.TLSOptions) + if err != nil { + return &client.Client{}, err + } + + customHeaders := configFile.HTTPHeaders + if customHeaders == nil { + customHeaders = map[string]string{} + } + customHeaders["User-Agent"] = clientUserAgent() + + verStr := api.DefaultVersion + if tmpStr := os.Getenv("DOCKER_API_VERSION"); tmpStr != "" { + verStr = tmpStr + } + + httpClient, err := newHTTPClient(host, opts.TLSOptions) + if err != nil { + return &client.Client{}, err + } + + return client.NewClient(host, verStr, httpClient, customHeaders) +} + +func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (host string, err error) { + switch len(hosts) { + case 0: + host = os.Getenv("DOCKER_HOST") + case 1: + host = hosts[0] + default: + return "", errors.New("Please specify only one -H") + } + + host, err = dopts.ParseHost(tlsOptions != nil, host) + return +} + +func newHTTPClient(host string, tlsOptions *tlsconfig.Options) (*http.Client, error) { + if tlsOptions == nil { + // let the api client configure the default transport. + return nil, nil + } + + config, err := tlsconfig.Client(*tlsOptions) + if err != nil { + return nil, err + } + tr := &http.Transport{ + TLSClientConfig: config, + } + proto, addr, _, err := client.ParseHost(host) + if err != nil { + return nil, err + } + + sockets.ConfigureTransport(tr, proto, addr) + + return &http.Client{ + Transport: tr, + }, nil +} + +func clientUserAgent() string { + return "Docker-Client/" + dockerversion.Version + " (" + runtime.GOOS + ")" +} diff --git a/components/cli/command/commands/commands.go b/components/cli/command/commands/commands.go new file mode 100644 index 0000000000..3eb1828d57 --- /dev/null +++ b/components/cli/command/commands/commands.go @@ -0,0 +1,71 @@ +package commands + +import ( + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/container" + "github.com/docker/docker/cli/command/image" + "github.com/docker/docker/cli/command/network" + "github.com/docker/docker/cli/command/node" + "github.com/docker/docker/cli/command/plugin" + "github.com/docker/docker/cli/command/registry" + "github.com/docker/docker/cli/command/service" + "github.com/docker/docker/cli/command/stack" + "github.com/docker/docker/cli/command/swarm" + "github.com/docker/docker/cli/command/system" + "github.com/docker/docker/cli/command/volume" + "github.com/spf13/cobra" +) + +// AddCommands adds all the commands from api/client to the root command +func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) { + cmd.AddCommand( + node.NewNodeCommand(dockerCli), + service.NewServiceCommand(dockerCli), + stack.NewStackCommand(dockerCli), + stack.NewTopLevelDeployCommand(dockerCli), + swarm.NewSwarmCommand(dockerCli), + container.NewAttachCommand(dockerCli), + container.NewCommitCommand(dockerCli), + container.NewCopyCommand(dockerCli), + container.NewCreateCommand(dockerCli), + container.NewDiffCommand(dockerCli), + container.NewExecCommand(dockerCli), + container.NewExportCommand(dockerCli), + container.NewKillCommand(dockerCli), + container.NewLogsCommand(dockerCli), + container.NewPauseCommand(dockerCli), + container.NewPortCommand(dockerCli), + container.NewPsCommand(dockerCli), + container.NewRenameCommand(dockerCli), + container.NewRestartCommand(dockerCli), + container.NewRmCommand(dockerCli), + container.NewRunCommand(dockerCli), + container.NewStartCommand(dockerCli), + container.NewStatsCommand(dockerCli), + container.NewStopCommand(dockerCli), + container.NewTopCommand(dockerCli), + container.NewUnpauseCommand(dockerCli), + container.NewUpdateCommand(dockerCli), + container.NewWaitCommand(dockerCli), + image.NewBuildCommand(dockerCli), + image.NewHistoryCommand(dockerCli), + image.NewImagesCommand(dockerCli), + image.NewLoadCommand(dockerCli), + image.NewRemoveCommand(dockerCli), + image.NewSaveCommand(dockerCli), + image.NewPullCommand(dockerCli), + image.NewPushCommand(dockerCli), + image.NewSearchCommand(dockerCli), + image.NewImportCommand(dockerCli), + image.NewTagCommand(dockerCli), + network.NewNetworkCommand(dockerCli), + system.NewEventsCommand(dockerCli), + system.NewInspectCommand(dockerCli), + registry.NewLoginCommand(dockerCli), + registry.NewLogoutCommand(dockerCli), + system.NewVersionCommand(dockerCli), + volume.NewVolumeCommand(dockerCli), + system.NewInfoCommand(dockerCli), + ) + plugin.NewPluginCommand(cmd, dockerCli) +} diff --git a/components/cli/command/container/attach.go b/components/cli/command/container/attach.go new file mode 100644 index 0000000000..a1fe58dea7 --- /dev/null +++ b/components/cli/command/container/attach.go @@ -0,0 +1,130 @@ +package container + +import ( + "fmt" + "io" + "net/http/httputil" + + "golang.org/x/net/context" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/pkg/signal" + "github.com/spf13/cobra" +) + +type attachOptions struct { + noStdin bool + proxy bool + detachKeys string + + container string +} + +// NewAttachCommand creates a new cobra.Command for `docker attach` +func NewAttachCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts attachOptions + + cmd := &cobra.Command{ + Use: "attach [OPTIONS] CONTAINER", + Short: "Attach to a running container", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.container = args[0] + return runAttach(dockerCli, &opts) + }, + } + + flags := cmd.Flags() + flags.BoolVar(&opts.noStdin, "no-stdin", false, "Do not attach STDIN") + flags.BoolVar(&opts.proxy, "sig-proxy", true, "Proxy all received signals to the process") + flags.StringVar(&opts.detachKeys, "detach-keys", "", "Override the key sequence for detaching a container") + return cmd +} + +func runAttach(dockerCli *command.DockerCli, opts *attachOptions) error { + ctx := context.Background() + client := dockerCli.Client() + + c, err := client.ContainerInspect(ctx, opts.container) + if err != nil { + return err + } + + if !c.State.Running { + return fmt.Errorf("You cannot attach to a stopped container, start it first") + } + + if c.State.Paused { + return fmt.Errorf("You cannot attach to a paused container, unpause it first") + } + + if err := dockerCli.In().CheckTty(!opts.noStdin, c.Config.Tty); err != nil { + return err + } + + if opts.detachKeys != "" { + dockerCli.ConfigFile().DetachKeys = opts.detachKeys + } + + options := types.ContainerAttachOptions{ + Stream: true, + Stdin: !opts.noStdin && c.Config.OpenStdin, + Stdout: true, + Stderr: true, + DetachKeys: dockerCli.ConfigFile().DetachKeys, + } + + var in io.ReadCloser + if options.Stdin { + in = dockerCli.In() + } + + if opts.proxy && !c.Config.Tty { + sigc := ForwardAllSignals(ctx, dockerCli, opts.container) + defer signal.StopCatch(sigc) + } + + resp, errAttach := client.ContainerAttach(ctx, opts.container, options) + if errAttach != nil && errAttach != httputil.ErrPersistEOF { + // ContainerAttach returns an ErrPersistEOF (connection closed) + // means server met an error and put it in Hijacked connection + // keep the error and read detailed error message from hijacked connection later + return errAttach + } + defer resp.Close() + + if c.Config.Tty && dockerCli.Out().IsTerminal() { + height, width := dockerCli.Out().GetTtySize() + // To handle the case where a user repeatedly attaches/detaches without resizing their + // terminal, the only way to get the shell prompt to display for attaches 2+ is to artificially + // resize it, then go back to normal. Without this, every attach after the first will + // require the user to manually resize or hit enter. + resizeTtyTo(ctx, client, opts.container, height+1, width+1, false) + + // After the above resizing occurs, the call to MonitorTtySize below will handle resetting back + // to the actual size. + if err := MonitorTtySize(ctx, dockerCli, opts.container, false); err != nil { + logrus.Debugf("Error monitoring TTY size: %s", err) + } + } + if err := holdHijackedConnection(ctx, dockerCli, c.Config.Tty, in, dockerCli.Out(), dockerCli.Err(), resp); err != nil { + return err + } + + if errAttach != nil { + return errAttach + } + + _, status, err := getExitCode(dockerCli, ctx, opts.container) + if err != nil { + return err + } + if status != 0 { + return cli.StatusError{StatusCode: status} + } + + return nil +} diff --git a/components/cli/command/container/commit.go b/components/cli/command/container/commit.go new file mode 100644 index 0000000000..cf8d0102a6 --- /dev/null +++ b/components/cli/command/container/commit.go @@ -0,0 +1,76 @@ +package container + +import ( + "fmt" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + dockeropts "github.com/docker/docker/opts" + "github.com/spf13/cobra" +) + +type commitOptions struct { + container string + reference string + + pause bool + comment string + author string + changes dockeropts.ListOpts +} + +// NewCommitCommand creates a new cobra.Command for `docker commit` +func NewCommitCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts commitOptions + + cmd := &cobra.Command{ + Use: "commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]", + Short: "Create a new image from a container's changes", + Args: cli.RequiresRangeArgs(1, 2), + RunE: func(cmd *cobra.Command, args []string) error { + opts.container = args[0] + if len(args) > 1 { + opts.reference = args[1] + } + return runCommit(dockerCli, &opts) + }, + } + + flags := cmd.Flags() + flags.SetInterspersed(false) + + flags.BoolVarP(&opts.pause, "pause", "p", true, "Pause container during commit") + flags.StringVarP(&opts.comment, "message", "m", "", "Commit message") + flags.StringVarP(&opts.author, "author", "a", "", "Author (e.g., \"John Hannibal Smith \")") + + opts.changes = dockeropts.NewListOpts(nil) + flags.VarP(&opts.changes, "change", "c", "Apply Dockerfile instruction to the created image") + + return cmd +} + +func runCommit(dockerCli *command.DockerCli, opts *commitOptions) error { + ctx := context.Background() + + name := opts.container + reference := opts.reference + + options := types.ContainerCommitOptions{ + Reference: reference, + Comment: opts.comment, + Author: opts.author, + Changes: opts.changes.GetAll(), + Pause: opts.pause, + } + + response, err := dockerCli.Client().ContainerCommit(ctx, name, options) + if err != nil { + return err + } + + fmt.Fprintln(dockerCli.Out(), response.ID) + return nil +} diff --git a/components/cli/command/container/cp.go b/components/cli/command/container/cp.go new file mode 100644 index 0000000000..17ab2accf9 --- /dev/null +++ b/components/cli/command/container/cp.go @@ -0,0 +1,303 @@ +package container + +import ( + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/system" + "github.com/spf13/cobra" +) + +type copyOptions struct { + source string + destination string + followLink bool +} + +type copyDirection int + +const ( + fromContainer copyDirection = (1 << iota) + toContainer + acrossContainers = fromContainer | toContainer +) + +type cpConfig struct { + followLink bool +} + +// NewCopyCommand creates a new `docker cp` command +func NewCopyCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts copyOptions + + cmd := &cobra.Command{ + Use: `cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH|- + docker cp [OPTIONS] SRC_PATH|- CONTAINER:DEST_PATH`, + Short: "Copy files/folders between a container and the local filesystem", + Long: strings.Join([]string{ + "Copy files/folders between a container and the local filesystem\n", + "\nUse '-' as the source to read a tar archive from stdin\n", + "and extract it to a directory destination in a container.\n", + "Use '-' as the destination to stream a tar archive of a\n", + "container source to stdout.", + }, ""), + Args: cli.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + if args[0] == "" { + return fmt.Errorf("source can not be empty") + } + if args[1] == "" { + return fmt.Errorf("destination can not be empty") + } + opts.source = args[0] + opts.destination = args[1] + return runCopy(dockerCli, opts) + }, + } + + flags := cmd.Flags() + + flags.BoolVarP(&opts.followLink, "follow-link", "L", false, "Always follow symbol link in SRC_PATH") + + return cmd +} + +func runCopy(dockerCli *command.DockerCli, opts copyOptions) error { + srcContainer, srcPath := splitCpArg(opts.source) + dstContainer, dstPath := splitCpArg(opts.destination) + + var direction copyDirection + if srcContainer != "" { + direction |= fromContainer + } + if dstContainer != "" { + direction |= toContainer + } + + cpParam := &cpConfig{ + followLink: opts.followLink, + } + + ctx := context.Background() + + switch direction { + case fromContainer: + return copyFromContainer(ctx, dockerCli, srcContainer, srcPath, dstPath, cpParam) + case toContainer: + return copyToContainer(ctx, dockerCli, srcPath, dstContainer, dstPath, cpParam) + case acrossContainers: + // Copying between containers isn't supported. + return fmt.Errorf("copying between containers is not supported") + default: + // User didn't specify any container. + return fmt.Errorf("must specify at least one container source") + } +} + +func statContainerPath(ctx context.Context, dockerCli *command.DockerCli, containerName, path string) (types.ContainerPathStat, error) { + return dockerCli.Client().ContainerStatPath(ctx, containerName, path) +} + +func resolveLocalPath(localPath string) (absPath string, err error) { + if absPath, err = filepath.Abs(localPath); err != nil { + return + } + + return archive.PreserveTrailingDotOrSeparator(absPath, localPath), nil +} + +func copyFromContainer(ctx context.Context, dockerCli *command.DockerCli, srcContainer, srcPath, dstPath string, cpParam *cpConfig) (err error) { + if dstPath != "-" { + // Get an absolute destination path. + dstPath, err = resolveLocalPath(dstPath) + if err != nil { + return err + } + } + + // if client requests to follow symbol link, then must decide target file to be copied + var rebaseName string + if cpParam.followLink { + srcStat, err := statContainerPath(ctx, dockerCli, srcContainer, srcPath) + + // If the destination is a symbolic link, we should follow it. + if err == nil && srcStat.Mode&os.ModeSymlink != 0 { + linkTarget := srcStat.LinkTarget + if !system.IsAbs(linkTarget) { + // Join with the parent directory. + srcParent, _ := archive.SplitPathDirEntry(srcPath) + linkTarget = filepath.Join(srcParent, linkTarget) + } + + linkTarget, rebaseName = archive.GetRebaseName(srcPath, linkTarget) + srcPath = linkTarget + } + + } + + content, stat, err := dockerCli.Client().CopyFromContainer(ctx, srcContainer, srcPath) + if err != nil { + return err + } + defer content.Close() + + if dstPath == "-" { + // Send the response to STDOUT. + _, err = io.Copy(os.Stdout, content) + + return err + } + + // Prepare source copy info. + srcInfo := archive.CopyInfo{ + Path: srcPath, + Exists: true, + IsDir: stat.Mode.IsDir(), + RebaseName: rebaseName, + } + + preArchive := content + if len(srcInfo.RebaseName) != 0 { + _, srcBase := archive.SplitPathDirEntry(srcInfo.Path) + preArchive = archive.RebaseArchiveEntries(content, srcBase, srcInfo.RebaseName) + } + // See comments in the implementation of `archive.CopyTo` for exactly what + // goes into deciding how and whether the source archive needs to be + // altered for the correct copy behavior. + return archive.CopyTo(preArchive, srcInfo, dstPath) +} + +func copyToContainer(ctx context.Context, dockerCli *command.DockerCli, srcPath, dstContainer, dstPath string, cpParam *cpConfig) (err error) { + if srcPath != "-" { + // Get an absolute source path. + srcPath, err = resolveLocalPath(srcPath) + if err != nil { + return err + } + } + + // In order to get the copy behavior right, we need to know information + // about both the source and destination. The API is a simple tar + // archive/extract API but we can use the stat info header about the + // destination to be more informed about exactly what the destination is. + + // Prepare destination copy info by stat-ing the container path. + dstInfo := archive.CopyInfo{Path: dstPath} + dstStat, err := statContainerPath(ctx, dockerCli, dstContainer, dstPath) + + // If the destination is a symbolic link, we should evaluate it. + if err == nil && dstStat.Mode&os.ModeSymlink != 0 { + linkTarget := dstStat.LinkTarget + if !system.IsAbs(linkTarget) { + // Join with the parent directory. + dstParent, _ := archive.SplitPathDirEntry(dstPath) + linkTarget = filepath.Join(dstParent, linkTarget) + } + + dstInfo.Path = linkTarget + dstStat, err = statContainerPath(ctx, dockerCli, dstContainer, linkTarget) + } + + // Ignore any error and assume that the parent directory of the destination + // path exists, in which case the copy may still succeed. If there is any + // type of conflict (e.g., non-directory overwriting an existing directory + // or vice versa) the extraction will fail. If the destination simply did + // not exist, but the parent directory does, the extraction will still + // succeed. + if err == nil { + dstInfo.Exists, dstInfo.IsDir = true, dstStat.Mode.IsDir() + } + + var ( + content io.Reader + resolvedDstPath string + ) + + if srcPath == "-" { + // Use STDIN. + content = os.Stdin + resolvedDstPath = dstInfo.Path + if !dstInfo.IsDir { + return fmt.Errorf("destination %q must be a directory", fmt.Sprintf("%s:%s", dstContainer, dstPath)) + } + } else { + // Prepare source copy info. + srcInfo, err := archive.CopyInfoSourcePath(srcPath, cpParam.followLink) + if err != nil { + return err + } + + srcArchive, err := archive.TarResource(srcInfo) + if err != nil { + return err + } + defer srcArchive.Close() + + // With the stat info about the local source as well as the + // destination, we have enough information to know whether we need to + // alter the archive that we upload so that when the server extracts + // it to the specified directory in the container we get the desired + // copy behavior. + + // See comments in the implementation of `archive.PrepareArchiveCopy` + // for exactly what goes into deciding how and whether the source + // archive needs to be altered for the correct copy behavior when it is + // extracted. This function also infers from the source and destination + // info which directory to extract to, which may be the parent of the + // destination that the user specified. + dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, dstInfo) + if err != nil { + return err + } + defer preparedArchive.Close() + + resolvedDstPath = dstDir + content = preparedArchive + } + + options := types.CopyToContainerOptions{ + AllowOverwriteDirWithFile: false, + } + + return dockerCli.Client().CopyToContainer(ctx, dstContainer, resolvedDstPath, content, options) +} + +// We use `:` as a delimiter between CONTAINER and PATH, but `:` could also be +// in a valid LOCALPATH, like `file:name.txt`. We can resolve this ambiguity by +// requiring a LOCALPATH with a `:` to be made explicit with a relative or +// absolute path: +// `/path/to/file:name.txt` or `./file:name.txt` +// +// This is apparently how `scp` handles this as well: +// http://www.cyberciti.biz/faq/rsync-scp-file-name-with-colon-punctuation-in-it/ +// +// We can't simply check for a filepath separator because container names may +// have a separator, e.g., "host0/cname1" if container is in a Docker cluster, +// so we have to check for a `/` or `.` prefix. Also, in the case of a Windows +// client, a `:` could be part of an absolute Windows path, in which case it +// is immediately proceeded by a backslash. +func splitCpArg(arg string) (container, path string) { + if system.IsAbs(arg) { + // Explicit local absolute path, e.g., `C:\foo` or `/foo`. + return "", arg + } + + parts := strings.SplitN(arg, ":", 2) + + if len(parts) == 1 || strings.HasPrefix(parts[0], ".") { + // Either there's no `:` in the arg + // OR it's an explicit local relative path like `./file:name.txt`. + return "", arg + } + + return parts[0], parts[1] +} diff --git a/components/cli/command/container/create.go b/components/cli/command/container/create.go new file mode 100644 index 0000000000..95e8d95ed9 --- /dev/null +++ b/components/cli/command/container/create.go @@ -0,0 +1,217 @@ +package container + +import ( + "fmt" + "io" + "os" + + "golang.org/x/net/context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/pkg/jsonmessage" + // FIXME migrate to docker/distribution/reference + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + networktypes "github.com/docker/docker/api/types/network" + apiclient "github.com/docker/docker/client" + "github.com/docker/docker/reference" + "github.com/docker/docker/registry" + runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +type createOptions struct { + name string +} + +// NewCreateCommand creates a new cobra.Command for `docker create` +func NewCreateCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts createOptions + var copts *runconfigopts.ContainerOptions + + cmd := &cobra.Command{ + Use: "create [OPTIONS] IMAGE [COMMAND] [ARG...]", + Short: "Create a new container", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + copts.Image = args[0] + if len(args) > 1 { + copts.Args = args[1:] + } + return runCreate(dockerCli, cmd.Flags(), &opts, copts) + }, + } + + flags := cmd.Flags() + flags.SetInterspersed(false) + + flags.StringVar(&opts.name, "name", "", "Assign a name to the container") + + // Add an explicit help that doesn't have a `-h` to prevent the conflict + // with hostname + flags.Bool("help", false, "Print usage") + + command.AddTrustedFlags(flags, true) + copts = runconfigopts.AddFlags(flags) + return cmd +} + +func runCreate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *createOptions, copts *runconfigopts.ContainerOptions) error { + config, hostConfig, networkingConfig, err := runconfigopts.Parse(flags, copts) + if err != nil { + reportError(dockerCli.Err(), "create", err.Error(), true) + return cli.StatusError{StatusCode: 125} + } + response, err := createContainer(context.Background(), dockerCli, config, hostConfig, networkingConfig, hostConfig.ContainerIDFile, opts.name) + if err != nil { + return err + } + fmt.Fprintf(dockerCli.Out(), "%s\n", response.ID) + return nil +} + +func pullImage(ctx context.Context, dockerCli *command.DockerCli, image string, out io.Writer) error { + ref, err := reference.ParseNamed(image) + if err != nil { + return err + } + + // Resolve the Repository name from fqn to RepositoryInfo + repoInfo, err := registry.ParseRepositoryInfo(ref) + if err != nil { + return err + } + + authConfig := dockerCli.ResolveAuthConfig(ctx, repoInfo.Index) + encodedAuth, err := command.EncodeAuthToBase64(authConfig) + if err != nil { + return err + } + + options := types.ImageCreateOptions{ + RegistryAuth: encodedAuth, + } + + responseBody, err := dockerCli.Client().ImageCreate(ctx, image, options) + if err != nil { + return err + } + defer responseBody.Close() + + return jsonmessage.DisplayJSONMessagesStream( + responseBody, + out, + dockerCli.Out().FD(), + dockerCli.Out().IsTerminal(), + nil) +} + +type cidFile struct { + path string + file *os.File + written bool +} + +func (cid *cidFile) Close() error { + cid.file.Close() + + if !cid.written { + if err := os.Remove(cid.path); err != nil { + return fmt.Errorf("failed to remove the CID file '%s': %s \n", cid.path, err) + } + } + + return nil +} + +func (cid *cidFile) Write(id string) error { + if _, err := cid.file.Write([]byte(id)); err != nil { + return fmt.Errorf("Failed to write the container ID to the file: %s", err) + } + cid.written = true + return nil +} + +func newCIDFile(path string) (*cidFile, error) { + if _, err := os.Stat(path); err == nil { + return nil, fmt.Errorf("Container ID file found, make sure the other container isn't running or delete %s", path) + } + + f, err := os.Create(path) + if err != nil { + return nil, fmt.Errorf("Failed to create the container ID file: %s", err) + } + + return &cidFile{path: path, file: f}, nil +} + +func createContainer(ctx context.Context, dockerCli *command.DockerCli, config *container.Config, hostConfig *container.HostConfig, networkingConfig *networktypes.NetworkingConfig, cidfile, name string) (*types.ContainerCreateResponse, error) { + stderr := dockerCli.Err() + + var containerIDFile *cidFile + if cidfile != "" { + var err error + if containerIDFile, err = newCIDFile(cidfile); err != nil { + return nil, err + } + defer containerIDFile.Close() + } + + var trustedRef reference.Canonical + _, ref, err := reference.ParseIDOrReference(config.Image) + if err != nil { + return nil, err + } + if ref != nil { + ref = reference.WithDefaultTag(ref) + + if ref, ok := ref.(reference.NamedTagged); ok && command.IsTrusted() { + var err error + trustedRef, err = dockerCli.TrustedReference(ctx, ref) + if err != nil { + return nil, err + } + config.Image = trustedRef.String() + } + } + + //create the container + response, err := dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, name) + + //if image not found try to pull it + if err != nil { + if apiclient.IsErrImageNotFound(err) && ref != nil { + fmt.Fprintf(stderr, "Unable to find image '%s' locally\n", ref.String()) + + // we don't want to write to stdout anything apart from container.ID + if err = pullImage(ctx, dockerCli, config.Image, stderr); err != nil { + return nil, err + } + if ref, ok := ref.(reference.NamedTagged); ok && trustedRef != nil { + if err := dockerCli.TagTrusted(ctx, trustedRef, ref); err != nil { + return nil, err + } + } + // Retry + var retryErr error + response, retryErr = dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, name) + if retryErr != nil { + return nil, retryErr + } + } else { + return nil, err + } + } + + for _, warning := range response.Warnings { + fmt.Fprintf(stderr, "WARNING: %s\n", warning) + } + if containerIDFile != nil { + if err = containerIDFile.Write(response.ID); err != nil { + return nil, err + } + } + return &response, nil +} diff --git a/components/cli/command/container/diff.go b/components/cli/command/container/diff.go new file mode 100644 index 0000000000..12d6591014 --- /dev/null +++ b/components/cli/command/container/diff.go @@ -0,0 +1,58 @@ +package container + +import ( + "fmt" + + "golang.org/x/net/context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/pkg/archive" + "github.com/spf13/cobra" +) + +type diffOptions struct { + container string +} + +// NewDiffCommand creates a new cobra.Command for `docker diff` +func NewDiffCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts diffOptions + + return &cobra.Command{ + Use: "diff CONTAINER", + Short: "Inspect changes on a container's filesystem", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.container = args[0] + return runDiff(dockerCli, &opts) + }, + } +} + +func runDiff(dockerCli *command.DockerCli, opts *diffOptions) error { + if opts.container == "" { + return fmt.Errorf("Container name cannot be empty") + } + ctx := context.Background() + + changes, err := dockerCli.Client().ContainerDiff(ctx, opts.container) + if err != nil { + return err + } + + for _, change := range changes { + var kind string + switch change.Kind { + case archive.ChangeModify: + kind = "C" + case archive.ChangeAdd: + kind = "A" + case archive.ChangeDelete: + kind = "D" + } + fmt.Fprintf(dockerCli.Out(), "%s %s\n", kind, change.Path) + } + + return nil +} diff --git a/components/cli/command/container/exec.go b/components/cli/command/container/exec.go new file mode 100644 index 0000000000..1682a7ca64 --- /dev/null +++ b/components/cli/command/container/exec.go @@ -0,0 +1,192 @@ +package container + +import ( + "fmt" + "io" + + "golang.org/x/net/context" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + apiclient "github.com/docker/docker/client" + "github.com/docker/docker/pkg/promise" + "github.com/spf13/cobra" +) + +type execOptions struct { + detachKeys string + interactive bool + tty bool + detach bool + user string + privileged bool +} + +// NewExecCommand creats a new cobra.Command for `docker exec` +func NewExecCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts execOptions + + cmd := &cobra.Command{ + Use: "exec [OPTIONS] CONTAINER COMMAND [ARG...]", + Short: "Run a command in a running container", + Args: cli.RequiresMinArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + container := args[0] + execCmd := args[1:] + return runExec(dockerCli, &opts, container, execCmd) + }, + } + + flags := cmd.Flags() + flags.SetInterspersed(false) + + flags.StringVarP(&opts.detachKeys, "detach-keys", "", "", "Override the key sequence for detaching a container") + flags.BoolVarP(&opts.interactive, "interactive", "i", false, "Keep STDIN open even if not attached") + flags.BoolVarP(&opts.tty, "tty", "t", false, "Allocate a pseudo-TTY") + flags.BoolVarP(&opts.detach, "detach", "d", false, "Detached mode: run command in the background") + flags.StringVarP(&opts.user, "user", "u", "", "Username or UID (format: [:])") + flags.BoolVarP(&opts.privileged, "privileged", "", false, "Give extended privileges to the command") + + return cmd +} + +func runExec(dockerCli *command.DockerCli, opts *execOptions, container string, execCmd []string) error { + execConfig, err := parseExec(opts, container, execCmd) + // just in case the ParseExec does not exit + if container == "" || err != nil { + return cli.StatusError{StatusCode: 1} + } + + if opts.detachKeys != "" { + dockerCli.ConfigFile().DetachKeys = opts.detachKeys + } + + // Send client escape keys + execConfig.DetachKeys = dockerCli.ConfigFile().DetachKeys + + ctx := context.Background() + client := dockerCli.Client() + + response, err := client.ContainerExecCreate(ctx, container, *execConfig) + if err != nil { + return err + } + + execID := response.ID + if execID == "" { + fmt.Fprintf(dockerCli.Out(), "exec ID empty") + return nil + } + + //Temp struct for execStart so that we don't need to transfer all the execConfig + if !execConfig.Detach { + if err := dockerCli.In().CheckTty(execConfig.AttachStdin, execConfig.Tty); err != nil { + return err + } + } else { + execStartCheck := types.ExecStartCheck{ + Detach: execConfig.Detach, + Tty: execConfig.Tty, + } + + if err := client.ContainerExecStart(ctx, execID, execStartCheck); err != nil { + return err + } + // For now don't print this - wait for when we support exec wait() + // fmt.Fprintf(dockerCli.Out(), "%s\n", execID) + return nil + } + + // Interactive exec requested. + var ( + out, stderr io.Writer + in io.ReadCloser + errCh chan error + ) + + if execConfig.AttachStdin { + in = dockerCli.In() + } + if execConfig.AttachStdout { + out = dockerCli.Out() + } + if execConfig.AttachStderr { + if execConfig.Tty { + stderr = dockerCli.Out() + } else { + stderr = dockerCli.Err() + } + } + + resp, err := client.ContainerExecAttach(ctx, execID, *execConfig) + if err != nil { + return err + } + defer resp.Close() + errCh = promise.Go(func() error { + return holdHijackedConnection(ctx, dockerCli, execConfig.Tty, in, out, stderr, resp) + }) + + if execConfig.Tty && dockerCli.In().IsTerminal() { + if err := MonitorTtySize(ctx, dockerCli, execID, true); err != nil { + fmt.Fprintf(dockerCli.Err(), "Error monitoring TTY size: %s\n", err) + } + } + + if err := <-errCh; err != nil { + logrus.Debugf("Error hijack: %s", err) + return err + } + + var status int + if _, status, err = getExecExitCode(ctx, client, execID); err != nil { + return err + } + + if status != 0 { + return cli.StatusError{StatusCode: status} + } + + return nil +} + +// getExecExitCode perform an inspect on the exec command. It returns +// the running state and the exit code. +func getExecExitCode(ctx context.Context, client apiclient.ContainerAPIClient, execID string) (bool, int, error) { + resp, err := client.ContainerExecInspect(ctx, execID) + if err != nil { + // If we can't connect, then the daemon probably died. + if err != apiclient.ErrConnectionFailed { + return false, -1, err + } + return false, -1, nil + } + + return resp.Running, resp.ExitCode, nil +} + +// parseExec parses the specified args for the specified command and generates +// an ExecConfig from it. +func parseExec(opts *execOptions, container string, execCmd []string) (*types.ExecConfig, error) { + execConfig := &types.ExecConfig{ + User: opts.user, + Privileged: opts.privileged, + Tty: opts.tty, + Cmd: execCmd, + Detach: opts.detach, + // container is not used here + } + + // If -d is not set, attach to everything by default + if !opts.detach { + execConfig.AttachStdout = true + execConfig.AttachStderr = true + if opts.interactive { + execConfig.AttachStdin = true + } + } + + return execConfig, nil +} diff --git a/components/cli/command/container/exec_test.go b/components/cli/command/container/exec_test.go new file mode 100644 index 0000000000..2e122e7386 --- /dev/null +++ b/components/cli/command/container/exec_test.go @@ -0,0 +1,117 @@ +package container + +import ( + "testing" + + "github.com/docker/docker/api/types" +) + +type arguments struct { + options execOptions + container string + execCmd []string +} + +func TestParseExec(t *testing.T) { + valids := map[*arguments]*types.ExecConfig{ + &arguments{ + execCmd: []string{"command"}, + }: { + Cmd: []string{"command"}, + AttachStdout: true, + AttachStderr: true, + }, + &arguments{ + execCmd: []string{"command1", "command2"}, + }: { + Cmd: []string{"command1", "command2"}, + AttachStdout: true, + AttachStderr: true, + }, + &arguments{ + options: execOptions{ + interactive: true, + tty: true, + user: "uid", + }, + execCmd: []string{"command"}, + }: { + User: "uid", + AttachStdin: true, + AttachStdout: true, + AttachStderr: true, + Tty: true, + Cmd: []string{"command"}, + }, + &arguments{ + options: execOptions{ + detach: true, + }, + execCmd: []string{"command"}, + }: { + AttachStdin: false, + AttachStdout: false, + AttachStderr: false, + Detach: true, + Cmd: []string{"command"}, + }, + &arguments{ + options: execOptions{ + tty: true, + interactive: true, + detach: true, + }, + execCmd: []string{"command"}, + }: { + AttachStdin: false, + AttachStdout: false, + AttachStderr: false, + Detach: true, + Tty: true, + Cmd: []string{"command"}, + }, + } + + for valid, expectedExecConfig := range valids { + execConfig, err := parseExec(&valid.options, valid.container, valid.execCmd) + if err != nil { + t.Fatal(err) + } + if !compareExecConfig(expectedExecConfig, execConfig) { + t.Fatalf("Expected [%v] for %v, got [%v]", expectedExecConfig, valid, execConfig) + } + } +} + +func compareExecConfig(config1 *types.ExecConfig, config2 *types.ExecConfig) bool { + if config1.AttachStderr != config2.AttachStderr { + return false + } + if config1.AttachStdin != config2.AttachStdin { + return false + } + if config1.AttachStdout != config2.AttachStdout { + return false + } + if config1.Detach != config2.Detach { + return false + } + if config1.Privileged != config2.Privileged { + return false + } + if config1.Tty != config2.Tty { + return false + } + if config1.User != config2.User { + return false + } + if len(config1.Cmd) != len(config2.Cmd) { + return false + } + for index, value := range config1.Cmd { + if value != config2.Cmd[index] { + return false + } + } + return true +} diff --git a/components/cli/command/container/export.go b/components/cli/command/container/export.go new file mode 100644 index 0000000000..8fa2e5d77e --- /dev/null +++ b/components/cli/command/container/export.go @@ -0,0 +1,59 @@ +package container + +import ( + "errors" + "io" + + "golang.org/x/net/context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +type exportOptions struct { + container string + output string +} + +// NewExportCommand creates a new `docker export` command +func NewExportCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts exportOptions + + cmd := &cobra.Command{ + Use: "export [OPTIONS] CONTAINER", + Short: "Export a container's filesystem as a tar archive", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.container = args[0] + return runExport(dockerCli, opts) + }, + } + + flags := cmd.Flags() + + flags.StringVarP(&opts.output, "output", "o", "", "Write to a file, instead of STDOUT") + + return cmd +} + +func runExport(dockerCli *command.DockerCli, opts exportOptions) error { + if opts.output == "" && dockerCli.Out().IsTerminal() { + return errors.New("Cowardly refusing to save to a terminal. Use the -o flag or redirect.") + } + + clnt := dockerCli.Client() + + responseBody, err := clnt.ContainerExport(context.Background(), opts.container) + if err != nil { + return err + } + defer responseBody.Close() + + if opts.output == "" { + _, err := io.Copy(dockerCli.Out(), responseBody) + return err + } + + return command.CopyToFile(opts.output, responseBody) +} diff --git a/components/cli/command/container/hijack.go b/components/cli/command/container/hijack.go new file mode 100644 index 0000000000..855a152904 --- /dev/null +++ b/components/cli/command/container/hijack.go @@ -0,0 +1,121 @@ +package container + +import ( + "io" + "runtime" + "sync" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/stdcopy" + "golang.org/x/net/context" +) + +type streams interface { + In() *command.InStream + Out() *command.OutStream +} + +// holdHijackedConnection handles copying input to and output from streams to the +// connection +func holdHijackedConnection(ctx context.Context, streams streams, tty bool, inputStream io.ReadCloser, outputStream, errorStream io.Writer, resp types.HijackedResponse) error { + var ( + err error + restoreOnce sync.Once + ) + if inputStream != nil && tty { + if err := setRawTerminal(streams); err != nil { + return err + } + defer func() { + restoreOnce.Do(func() { + restoreTerminal(streams, inputStream) + }) + }() + } + + receiveStdout := make(chan error, 1) + if outputStream != nil || errorStream != nil { + go func() { + // When TTY is ON, use regular copy + if tty && outputStream != nil { + _, err = io.Copy(outputStream, resp.Reader) + // we should restore the terminal as soon as possible once connection end + // so any following print messages will be in normal type. + if inputStream != nil { + restoreOnce.Do(func() { + restoreTerminal(streams, inputStream) + }) + } + } else { + _, err = stdcopy.StdCopy(outputStream, errorStream, resp.Reader) + } + + logrus.Debug("[hijack] End of stdout") + receiveStdout <- err + }() + } + + stdinDone := make(chan struct{}) + go func() { + if inputStream != nil { + io.Copy(resp.Conn, inputStream) + // we should restore the terminal as soon as possible once connection end + // so any following print messages will be in normal type. + if tty { + restoreOnce.Do(func() { + restoreTerminal(streams, inputStream) + }) + } + logrus.Debug("[hijack] End of stdin") + } + + if err := resp.CloseWrite(); err != nil { + logrus.Debugf("Couldn't send EOF: %s", err) + } + close(stdinDone) + }() + + select { + case err := <-receiveStdout: + if err != nil { + logrus.Debugf("Error receiveStdout: %s", err) + return err + } + case <-stdinDone: + if outputStream != nil || errorStream != nil { + select { + case err := <-receiveStdout: + if err != nil { + logrus.Debugf("Error receiveStdout: %s", err) + return err + } + case <-ctx.Done(): + } + } + case <-ctx.Done(): + } + + return nil +} + +func setRawTerminal(streams streams) error { + if err := streams.In().SetRawTerminal(); err != nil { + return err + } + return streams.Out().SetRawTerminal() +} + +func restoreTerminal(streams streams, in io.Closer) error { + streams.In().RestoreTerminal() + streams.Out().RestoreTerminal() + // WARNING: DO NOT REMOVE THE OS CHECK !!! + // For some reason this Close call blocks on darwin.. + // As the client exists right after, simply discard the close + // until we find a better solution. + if in != nil && runtime.GOOS != "darwin" { + return in.Close() + } + return nil +} diff --git a/components/cli/command/container/kill.go b/components/cli/command/container/kill.go new file mode 100644 index 0000000000..8d9af6f7a6 --- /dev/null +++ b/components/cli/command/container/kill.go @@ -0,0 +1,53 @@ +package container + +import ( + "fmt" + "strings" + + "golang.org/x/net/context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +type killOptions struct { + signal string + + containers []string +} + +// NewKillCommand creates a new cobra.Command for `docker kill` +func NewKillCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts killOptions + + cmd := &cobra.Command{ + Use: "kill [OPTIONS] CONTAINER [CONTAINER...]", + Short: "Kill one or more running containers", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.containers = args + return runKill(dockerCli, &opts) + }, + } + + flags := cmd.Flags() + flags.StringVarP(&opts.signal, "signal", "s", "KILL", "Signal to send to the container") + return cmd +} + +func runKill(dockerCli *command.DockerCli, opts *killOptions) error { + var errs []string + ctx := context.Background() + for _, name := range opts.containers { + if err := dockerCli.Client().ContainerKill(ctx, name, opts.signal); err != nil { + errs = append(errs, err.Error()) + } else { + fmt.Fprintf(dockerCli.Out(), "%s\n", name) + } + } + if len(errs) > 0 { + return fmt.Errorf("%s", strings.Join(errs, "\n")) + } + return nil +} diff --git a/components/cli/command/container/logs.go b/components/cli/command/container/logs.go new file mode 100644 index 0000000000..3a37cedf43 --- /dev/null +++ b/components/cli/command/container/logs.go @@ -0,0 +1,87 @@ +package container + +import ( + "fmt" + "io" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/pkg/stdcopy" + "github.com/spf13/cobra" +) + +var validDrivers = map[string]bool{ + "json-file": true, + "journald": true, +} + +type logsOptions struct { + follow bool + since string + timestamps bool + details bool + tail string + + container string +} + +// NewLogsCommand creates a new cobra.Command for `docker logs` +func NewLogsCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts logsOptions + + cmd := &cobra.Command{ + Use: "logs [OPTIONS] CONTAINER", + Short: "Fetch the logs of a container", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.container = args[0] + return runLogs(dockerCli, &opts) + }, + } + + flags := cmd.Flags() + flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output") + flags.StringVar(&opts.since, "since", "", "Show logs since timestamp") + flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps") + flags.BoolVar(&opts.details, "details", false, "Show extra details provided to logs") + flags.StringVar(&opts.tail, "tail", "all", "Number of lines to show from the end of the logs") + return cmd +} + +func runLogs(dockerCli *command.DockerCli, opts *logsOptions) error { + ctx := context.Background() + + c, err := dockerCli.Client().ContainerInspect(ctx, opts.container) + if err != nil { + return err + } + + if !validDrivers[c.HostConfig.LogConfig.Type] { + return fmt.Errorf("\"logs\" command is supported only for \"json-file\" and \"journald\" logging drivers (got: %s)", c.HostConfig.LogConfig.Type) + } + + options := types.ContainerLogsOptions{ + ShowStdout: true, + ShowStderr: true, + Since: opts.since, + Timestamps: opts.timestamps, + Follow: opts.follow, + Tail: opts.tail, + Details: opts.details, + } + responseBody, err := dockerCli.Client().ContainerLogs(ctx, opts.container, options) + if err != nil { + return err + } + defer responseBody.Close() + + if c.Config.Tty { + _, err = io.Copy(dockerCli.Out(), responseBody) + } else { + _, err = stdcopy.StdCopy(dockerCli.Out(), dockerCli.Err(), responseBody) + } + return err +} diff --git a/components/cli/command/container/pause.go b/components/cli/command/container/pause.go new file mode 100644 index 0000000000..0cc5b351ba --- /dev/null +++ b/components/cli/command/container/pause.go @@ -0,0 +1,48 @@ +package container + +import ( + "fmt" + "strings" + + "golang.org/x/net/context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +type pauseOptions struct { + containers []string +} + +// NewPauseCommand creates a new cobra.Command for `docker pause` +func NewPauseCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts pauseOptions + + return &cobra.Command{ + Use: "pause CONTAINER [CONTAINER...]", + Short: "Pause all processes within one or more containers", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.containers = args + return runPause(dockerCli, &opts) + }, + } +} + +func runPause(dockerCli *command.DockerCli, opts *pauseOptions) error { + ctx := context.Background() + + var errs []string + for _, container := range opts.containers { + if err := dockerCli.Client().ContainerPause(ctx, container); err != nil { + errs = append(errs, err.Error()) + } else { + fmt.Fprintf(dockerCli.Out(), "%s\n", container) + } + } + if len(errs) > 0 { + return fmt.Errorf("%s", strings.Join(errs, "\n")) + } + return nil +} diff --git a/components/cli/command/container/port.go b/components/cli/command/container/port.go new file mode 100644 index 0000000000..ea15290145 --- /dev/null +++ b/components/cli/command/container/port.go @@ -0,0 +1,78 @@ +package container + +import ( + "fmt" + "strings" + + "golang.org/x/net/context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/go-connections/nat" + "github.com/spf13/cobra" +) + +type portOptions struct { + container string + + port string +} + +// NewPortCommand creates a new cobra.Command for `docker port` +func NewPortCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts portOptions + + cmd := &cobra.Command{ + Use: "port CONTAINER [PRIVATE_PORT[/PROTO]]", + Short: "List port mappings or a specific mapping for the container", + Args: cli.RequiresRangeArgs(1, 2), + RunE: func(cmd *cobra.Command, args []string) error { + opts.container = args[0] + if len(args) > 1 { + opts.port = args[1] + } + return runPort(dockerCli, &opts) + }, + } + return cmd +} + +func runPort(dockerCli *command.DockerCli, opts *portOptions) error { + ctx := context.Background() + + c, err := dockerCli.Client().ContainerInspect(ctx, opts.container) + if err != nil { + return err + } + + if opts.port != "" { + port := opts.port + proto := "tcp" + parts := strings.SplitN(port, "/", 2) + + if len(parts) == 2 && len(parts[1]) != 0 { + port = parts[0] + proto = parts[1] + } + natPort := port + "/" + proto + newP, err := nat.NewPort(proto, port) + if err != nil { + return err + } + if frontends, exists := c.NetworkSettings.Ports[newP]; exists && frontends != nil { + for _, frontend := range frontends { + fmt.Fprintf(dockerCli.Out(), "%s:%s\n", frontend.HostIP, frontend.HostPort) + } + return nil + } + return fmt.Errorf("Error: No public port '%s' published for %s", natPort, opts.container) + } + + for from, frontends := range c.NetworkSettings.Ports { + for _, frontend := range frontends { + fmt.Fprintf(dockerCli.Out(), "%s -> %s:%s\n", from, frontend.HostIP, frontend.HostPort) + } + } + + return nil +} diff --git a/components/cli/command/container/ps.go b/components/cli/command/container/ps.go new file mode 100644 index 0000000000..d7ae675f5b --- /dev/null +++ b/components/cli/command/container/ps.go @@ -0,0 +1,142 @@ +package container + +import ( + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/formatter" + + "io/ioutil" + + "github.com/docker/docker/utils/templates" + "github.com/spf13/cobra" +) + +type psOptions struct { + quiet bool + size bool + all bool + noTrunc bool + nLatest bool + last int + format string + filter []string +} + +// NewPsCommand creates a new cobra.Command for `docker ps` +func NewPsCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts psOptions + + cmd := &cobra.Command{ + Use: "ps [OPTIONS]", + Short: "List containers", + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runPs(dockerCli, &opts) + }, + } + + flags := cmd.Flags() + + flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display numeric IDs") + flags.BoolVarP(&opts.size, "size", "s", false, "Display total file sizes") + flags.BoolVarP(&opts.all, "all", "a", false, "Show all containers (default shows just running)") + flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output") + flags.BoolVarP(&opts.nLatest, "latest", "l", false, "Show the latest created container (includes all states)") + flags.IntVarP(&opts.last, "last", "n", -1, "Show n last created containers (includes all states)") + flags.StringVarP(&opts.format, "format", "", "", "Pretty-print containers using a Go template") + flags.StringSliceVarP(&opts.filter, "filter", "f", []string{}, "Filter output based on conditions provided") + + return cmd +} + +type preProcessor struct { + types.Container + opts *types.ContainerListOptions +} + +// Size sets the size option when called by a template execution. +func (p *preProcessor) Size() bool { + p.opts.Size = true + return true +} + +func buildContainerListOptions(opts *psOptions) (*types.ContainerListOptions, error) { + + options := &types.ContainerListOptions{ + All: opts.all, + Limit: opts.last, + Size: opts.size, + Filter: filters.NewArgs(), + } + + if opts.nLatest && opts.last == -1 { + options.Limit = 1 + } + + for _, f := range opts.filter { + var err error + options.Filter, err = filters.ParseFlag(f, options.Filter) + if err != nil { + return nil, err + } + } + + // Currently only used with Size, so we can determine if the user + // put {{.Size}} in their format. + pre := &preProcessor{opts: options} + tmpl, err := templates.Parse(opts.format) + + if err != nil { + return nil, err + } + + // This shouldn't error out but swallowing the error makes it harder + // to track down if preProcessor issues come up. Ref #24696 + if err := tmpl.Execute(ioutil.Discard, pre); err != nil { + return nil, err + } + + return options, nil +} + +func runPs(dockerCli *command.DockerCli, opts *psOptions) error { + ctx := context.Background() + + listOptions, err := buildContainerListOptions(opts) + if err != nil { + return err + } + + containers, err := dockerCli.Client().ContainerList(ctx, *listOptions) + if err != nil { + return err + } + + f := opts.format + if len(f) == 0 { + if len(dockerCli.ConfigFile().PsFormat) > 0 && !opts.quiet { + f = dockerCli.ConfigFile().PsFormat + } else { + f = "table" + } + } + + psCtx := formatter.ContainerContext{ + Context: formatter.Context{ + Output: dockerCli.Out(), + Format: f, + Quiet: opts.quiet, + Trunc: !opts.noTrunc, + }, + Size: listOptions.Size, + Containers: containers, + } + + psCtx.Write() + + return nil +} diff --git a/components/cli/command/container/ps_test.go b/components/cli/command/container/ps_test.go new file mode 100644 index 0000000000..2af183cce1 --- /dev/null +++ b/components/cli/command/container/ps_test.go @@ -0,0 +1,74 @@ +package container + +import "testing" + +func TestBuildContainerListOptions(t *testing.T) { + + contexts := []struct { + psOpts *psOptions + expectedAll bool + expectedSize bool + expectedLimit int + expectedFilters map[string]string + }{ + { + psOpts: &psOptions{ + all: true, + size: true, + last: 5, + filter: []string{"foo=bar", "baz=foo"}, + }, + expectedAll: true, + expectedSize: true, + expectedLimit: 5, + expectedFilters: map[string]string{ + "foo": "bar", + "baz": "foo", + }, + }, + { + psOpts: &psOptions{ + all: true, + size: true, + last: -1, + nLatest: true, + }, + expectedAll: true, + expectedSize: true, + expectedLimit: 1, + expectedFilters: make(map[string]string), + }, + } + + for _, c := range contexts { + options, err := buildContainerListOptions(c.psOpts) + if err != nil { + t.Fatal(err) + } + + if c.expectedAll != options.All { + t.Fatalf("Expected All to be %t but got %t", c.expectedAll, options.All) + } + + if c.expectedSize != options.Size { + t.Fatalf("Expected Size to be %t but got %t", c.expectedSize, options.Size) + } + + if c.expectedLimit != options.Limit { + t.Fatalf("Expected Limit to be %d but got %d", c.expectedLimit, options.Limit) + } + + f := options.Filter + + if f.Len() != len(c.expectedFilters) { + t.Fatalf("Expected %d filters but got %d", len(c.expectedFilters), f.Len()) + } + + for k, v := range c.expectedFilters { + f := options.Filter + if !f.ExactMatch(k, v) { + t.Fatalf("Expected filter with key %s to be %s but got %s", k, v, f.Get(k)) + } + } + } +} diff --git a/components/cli/command/container/rename.go b/components/cli/command/container/rename.go new file mode 100644 index 0000000000..346fb7b3b9 --- /dev/null +++ b/components/cli/command/container/rename.go @@ -0,0 +1,51 @@ +package container + +import ( + "fmt" + "strings" + + "golang.org/x/net/context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +type renameOptions struct { + oldName string + newName string +} + +// NewRenameCommand creates a new cobra.Command for `docker rename` +func NewRenameCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts renameOptions + + cmd := &cobra.Command{ + Use: "rename CONTAINER NEW_NAME", + Short: "Rename a container", + Args: cli.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + opts.oldName = args[0] + opts.newName = args[1] + return runRename(dockerCli, &opts) + }, + } + return cmd +} + +func runRename(dockerCli *command.DockerCli, opts *renameOptions) error { + ctx := context.Background() + + oldName := strings.TrimSpace(opts.oldName) + newName := strings.TrimSpace(opts.newName) + + if oldName == "" || newName == "" { + return fmt.Errorf("Error: Neither old nor new names may be empty") + } + + if err := dockerCli.Client().ContainerRename(ctx, oldName, newName); err != nil { + fmt.Fprintf(dockerCli.Err(), "%s\n", err) + return fmt.Errorf("Error: failed to rename container named %s", oldName) + } + return nil +} diff --git a/components/cli/command/container/restart.go b/components/cli/command/container/restart.go new file mode 100644 index 0000000000..e370ef4010 --- /dev/null +++ b/components/cli/command/container/restart.go @@ -0,0 +1,55 @@ +package container + +import ( + "fmt" + "strings" + "time" + + "golang.org/x/net/context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +type restartOptions struct { + nSeconds int + + containers []string +} + +// NewRestartCommand creates a new cobra.Command for `docker restart` +func NewRestartCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts restartOptions + + cmd := &cobra.Command{ + Use: "restart [OPTIONS] CONTAINER [CONTAINER...]", + Short: "Restart one or more containers", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.containers = args + return runRestart(dockerCli, &opts) + }, + } + + flags := cmd.Flags() + flags.IntVarP(&opts.nSeconds, "time", "t", 10, "Seconds to wait for stop before killing the container") + return cmd +} + +func runRestart(dockerCli *command.DockerCli, opts *restartOptions) error { + ctx := context.Background() + var errs []string + for _, name := range opts.containers { + timeout := time.Duration(opts.nSeconds) * time.Second + if err := dockerCli.Client().ContainerRestart(ctx, name, &timeout); err != nil { + errs = append(errs, err.Error()) + } else { + fmt.Fprintf(dockerCli.Out(), "%s\n", name) + } + } + if len(errs) > 0 { + return fmt.Errorf("%s", strings.Join(errs, "\n")) + } + return nil +} diff --git a/components/cli/command/container/rm.go b/components/cli/command/container/rm.go new file mode 100644 index 0000000000..622a69b510 --- /dev/null +++ b/components/cli/command/container/rm.go @@ -0,0 +1,76 @@ +package container + +import ( + "fmt" + "strings" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +type rmOptions struct { + rmVolumes bool + rmLink bool + force bool + + containers []string +} + +// NewRmCommand creates a new cobra.Command for `docker rm` +func NewRmCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts rmOptions + + cmd := &cobra.Command{ + Use: "rm [OPTIONS] CONTAINER [CONTAINER...]", + Short: "Remove one or more containers", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.containers = args + return runRm(dockerCli, &opts) + }, + } + + flags := cmd.Flags() + flags.BoolVarP(&opts.rmVolumes, "volumes", "v", false, "Remove the volumes associated with the container") + flags.BoolVarP(&opts.rmLink, "link", "l", false, "Remove the specified link") + flags.BoolVarP(&opts.force, "force", "f", false, "Force the removal of a running container (uses SIGKILL)") + return cmd +} + +func runRm(dockerCli *command.DockerCli, opts *rmOptions) error { + ctx := context.Background() + + var errs []string + for _, name := range opts.containers { + if name == "" { + return fmt.Errorf("Container name cannot be empty") + } + name = strings.Trim(name, "/") + + if err := removeContainer(dockerCli, ctx, name, opts.rmVolumes, opts.rmLink, opts.force); err != nil { + errs = append(errs, err.Error()) + } else { + fmt.Fprintf(dockerCli.Out(), "%s\n", name) + } + } + if len(errs) > 0 { + return fmt.Errorf("%s", strings.Join(errs, "\n")) + } + return nil +} + +func removeContainer(dockerCli *command.DockerCli, ctx context.Context, container string, removeVolumes, removeLinks, force bool) error { + options := types.ContainerRemoveOptions{ + RemoveVolumes: removeVolumes, + RemoveLinks: removeLinks, + Force: force, + } + if err := dockerCli.Client().ContainerRemove(ctx, container, options); err != nil { + return err + } + return nil +} diff --git a/components/cli/command/container/run.go b/components/cli/command/container/run.go new file mode 100644 index 0000000000..d36ab610cf --- /dev/null +++ b/components/cli/command/container/run.go @@ -0,0 +1,288 @@ +package container + +import ( + "fmt" + "io" + "net/http/httputil" + "os" + "runtime" + "strings" + "syscall" + + "golang.org/x/net/context" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + opttypes "github.com/docker/docker/opts" + "github.com/docker/docker/pkg/promise" + "github.com/docker/docker/pkg/signal" + runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/docker/libnetwork/resolvconf/dns" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +type runOptions struct { + detach bool + sigProxy bool + name string + detachKeys string +} + +// NewRunCommand create a new `docker run` command +func NewRunCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts runOptions + var copts *runconfigopts.ContainerOptions + + cmd := &cobra.Command{ + Use: "run [OPTIONS] IMAGE [COMMAND] [ARG...]", + Short: "Run a command in a new container", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + copts.Image = args[0] + if len(args) > 1 { + copts.Args = args[1:] + } + return runRun(dockerCli, cmd.Flags(), &opts, copts) + }, + } + + flags := cmd.Flags() + flags.SetInterspersed(false) + + // These are flags not stored in Config/HostConfig + flags.BoolVarP(&opts.detach, "detach", "d", false, "Run container in background and print container ID") + flags.BoolVar(&opts.sigProxy, "sig-proxy", true, "Proxy received signals to the process") + flags.StringVar(&opts.name, "name", "", "Assign a name to the container") + flags.StringVar(&opts.detachKeys, "detach-keys", "", "Override the key sequence for detaching a container") + + // Add an explicit help that doesn't have a `-h` to prevent the conflict + // with hostname + flags.Bool("help", false, "Print usage") + + command.AddTrustedFlags(flags, true) + copts = runconfigopts.AddFlags(flags) + return cmd +} + +func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions, copts *runconfigopts.ContainerOptions) error { + stdout, stderr, stdin := dockerCli.Out(), dockerCli.Err(), dockerCli.In() + client := dockerCli.Client() + // TODO: pass this as an argument + cmdPath := "run" + + var ( + flAttach *opttypes.ListOpts + ErrConflictAttachDetach = fmt.Errorf("Conflicting options: -a and -d") + ErrConflictRestartPolicyAndAutoRemove = fmt.Errorf("Conflicting options: --restart and --rm") + ) + + config, hostConfig, networkingConfig, err := runconfigopts.Parse(flags, copts) + + // just in case the Parse does not exit + if err != nil { + reportError(stderr, cmdPath, err.Error(), true) + return cli.StatusError{StatusCode: 125} + } + + if hostConfig.AutoRemove && !hostConfig.RestartPolicy.IsNone() { + return ErrConflictRestartPolicyAndAutoRemove + } + if hostConfig.OomKillDisable != nil && *hostConfig.OomKillDisable && hostConfig.Memory == 0 { + fmt.Fprintf(stderr, "WARNING: Disabling the OOM killer on containers without setting a '-m/--memory' limit may be dangerous.\n") + } + + if len(hostConfig.DNS) > 0 { + // check the DNS settings passed via --dns against + // localhost regexp to warn if they are trying to + // set a DNS to a localhost address + for _, dnsIP := range hostConfig.DNS { + if dns.IsLocalhost(dnsIP) { + fmt.Fprintf(stderr, "WARNING: Localhost DNS setting (--dns=%s) may fail in containers.\n", dnsIP) + break + } + } + } + + config.ArgsEscaped = false + + if !opts.detach { + if err := dockerCli.In().CheckTty(config.AttachStdin, config.Tty); err != nil { + return err + } + } else { + if fl := flags.Lookup("attach"); fl != nil { + flAttach = fl.Value.(*opttypes.ListOpts) + if flAttach.Len() != 0 { + return ErrConflictAttachDetach + } + } + + config.AttachStdin = false + config.AttachStdout = false + config.AttachStderr = false + config.StdinOnce = false + } + + // Disable sigProxy when in TTY mode + if config.Tty { + opts.sigProxy = false + } + + // Telling the Windows daemon the initial size of the tty during start makes + // a far better user experience rather than relying on subsequent resizes + // to cause things to catch up. + if runtime.GOOS == "windows" { + hostConfig.ConsoleSize[0], hostConfig.ConsoleSize[1] = dockerCli.Out().GetTtySize() + } + + ctx, cancelFun := context.WithCancel(context.Background()) + + createResponse, err := createContainer(ctx, dockerCli, config, hostConfig, networkingConfig, hostConfig.ContainerIDFile, opts.name) + if err != nil { + reportError(stderr, cmdPath, err.Error(), true) + return runStartContainerErr(err) + } + if opts.sigProxy { + sigc := ForwardAllSignals(ctx, dockerCli, createResponse.ID) + defer signal.StopCatch(sigc) + } + var ( + waitDisplayID chan struct{} + errCh chan error + ) + if !config.AttachStdout && !config.AttachStderr { + // Make this asynchronous to allow the client to write to stdin before having to read the ID + waitDisplayID = make(chan struct{}) + go func() { + defer close(waitDisplayID) + fmt.Fprintf(stdout, "%s\n", createResponse.ID) + }() + } + attach := config.AttachStdin || config.AttachStdout || config.AttachStderr + if attach { + var ( + out, cerr io.Writer + in io.ReadCloser + ) + if config.AttachStdin { + in = stdin + } + if config.AttachStdout { + out = stdout + } + if config.AttachStderr { + if config.Tty { + cerr = stdout + } else { + cerr = stderr + } + } + + if opts.detachKeys != "" { + dockerCli.ConfigFile().DetachKeys = opts.detachKeys + } + + options := types.ContainerAttachOptions{ + Stream: true, + Stdin: config.AttachStdin, + Stdout: config.AttachStdout, + Stderr: config.AttachStderr, + DetachKeys: dockerCli.ConfigFile().DetachKeys, + } + + resp, errAttach := client.ContainerAttach(ctx, createResponse.ID, options) + if errAttach != nil && errAttach != httputil.ErrPersistEOF { + // ContainerAttach returns an ErrPersistEOF (connection closed) + // means server met an error and put it in Hijacked connection + // keep the error and read detailed error message from hijacked connection later + return errAttach + } + defer resp.Close() + + errCh = promise.Go(func() error { + errHijack := holdHijackedConnection(ctx, dockerCli, config.Tty, in, out, cerr, resp) + if errHijack == nil { + return errAttach + } + return errHijack + }) + } + + statusChan, err := waitExitOrRemoved(dockerCli, context.Background(), createResponse.ID, hostConfig.AutoRemove) + if err != nil { + return fmt.Errorf("Error waiting container's exit code: %v", err) + } + + //start the container + if err := client.ContainerStart(ctx, createResponse.ID, types.ContainerStartOptions{}); err != nil { + // If we have holdHijackedConnection, we should notify + // holdHijackedConnection we are going to exit and wait + // to avoid the terminal are not restored. + if attach { + cancelFun() + <-errCh + } + + reportError(stderr, cmdPath, err.Error(), false) + if hostConfig.AutoRemove { + // wait container to be removed + <-statusChan + } + return runStartContainerErr(err) + } + + if (config.AttachStdin || config.AttachStdout || config.AttachStderr) && config.Tty && dockerCli.Out().IsTerminal() { + if err := MonitorTtySize(ctx, dockerCli, createResponse.ID, false); err != nil { + fmt.Fprintf(stderr, "Error monitoring TTY size: %s\n", err) + } + } + + if errCh != nil { + if err := <-errCh; err != nil { + logrus.Debugf("Error hijack: %s", err) + return err + } + } + + // Detached mode: wait for the id to be displayed and return. + if !config.AttachStdout && !config.AttachStderr { + // Detached mode + <-waitDisplayID + return nil + } + + status := <-statusChan + if status != 0 { + return cli.StatusError{StatusCode: status} + } + return nil +} + +// reportError is a utility method that prints a user-friendly message +// containing the error that occurred during parsing and a suggestion to get help +func reportError(stderr io.Writer, name string, str string, withHelp bool) { + if withHelp { + str += ".\nSee '" + os.Args[0] + " " + name + " --help'" + } + fmt.Fprintf(stderr, "%s: %s.\n", os.Args[0], str) +} + +// if container start fails with 'not found'/'no such' error, return 127 +// if container start fails with 'permission denied' error, return 126 +// return 125 for generic docker daemon failures +func runStartContainerErr(err error) error { + trimmedErr := strings.TrimPrefix(err.Error(), "Error response from daemon: ") + statusError := cli.StatusError{StatusCode: 125} + if strings.Contains(trimmedErr, "executable file not found") || + strings.Contains(trimmedErr, "no such file or directory") || + strings.Contains(trimmedErr, "system cannot find the file specified") { + statusError = cli.StatusError{StatusCode: 127} + } else if strings.Contains(trimmedErr, syscall.EACCES.Error()) { + statusError = cli.StatusError{StatusCode: 126} + } + + return statusError +} diff --git a/components/cli/command/container/start.go b/components/cli/command/container/start.go new file mode 100644 index 0000000000..e72369177a --- /dev/null +++ b/components/cli/command/container/start.go @@ -0,0 +1,161 @@ +package container + +import ( + "fmt" + "io" + "net/http/httputil" + "strings" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/pkg/promise" + "github.com/docker/docker/pkg/signal" + "github.com/spf13/cobra" +) + +type startOptions struct { + attach bool + openStdin bool + detachKeys string + + containers []string +} + +// NewStartCommand creates a new cobra.Command for `docker start` +func NewStartCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts startOptions + + cmd := &cobra.Command{ + Use: "start [OPTIONS] CONTAINER [CONTAINER...]", + Short: "Start one or more stopped containers", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.containers = args + return runStart(dockerCli, &opts) + }, + } + + flags := cmd.Flags() + flags.BoolVarP(&opts.attach, "attach", "a", false, "Attach STDOUT/STDERR and forward signals") + flags.BoolVarP(&opts.openStdin, "interactive", "i", false, "Attach container's STDIN") + flags.StringVar(&opts.detachKeys, "detach-keys", "", "Override the key sequence for detaching a container") + return cmd +} + +func runStart(dockerCli *command.DockerCli, opts *startOptions) error { + ctx, cancelFun := context.WithCancel(context.Background()) + + if opts.attach || opts.openStdin { + // We're going to attach to a container. + // 1. Ensure we only have one container. + if len(opts.containers) > 1 { + return fmt.Errorf("You cannot start and attach multiple containers at once.") + } + + // 2. Attach to the container. + container := opts.containers[0] + c, err := dockerCli.Client().ContainerInspect(ctx, container) + if err != nil { + return err + } + + // We always use c.ID instead of container to maintain consistency during `docker start` + if !c.Config.Tty { + sigc := ForwardAllSignals(ctx, dockerCli, c.ID) + defer signal.StopCatch(sigc) + } + + if opts.detachKeys != "" { + dockerCli.ConfigFile().DetachKeys = opts.detachKeys + } + + options := types.ContainerAttachOptions{ + Stream: true, + Stdin: opts.openStdin && c.Config.OpenStdin, + Stdout: true, + Stderr: true, + DetachKeys: dockerCli.ConfigFile().DetachKeys, + } + + var in io.ReadCloser + + if options.Stdin { + in = dockerCli.In() + } + + resp, errAttach := dockerCli.Client().ContainerAttach(ctx, c.ID, options) + if errAttach != nil && errAttach != httputil.ErrPersistEOF { + // ContainerAttach return an ErrPersistEOF (connection closed) + // means server met an error and already put it in Hijacked connection, + // we would keep the error and read the detailed error message from hijacked connection + return errAttach + } + defer resp.Close() + cErr := promise.Go(func() error { + errHijack := holdHijackedConnection(ctx, dockerCli, c.Config.Tty, in, dockerCli.Out(), dockerCli.Err(), resp) + if errHijack == nil { + return errAttach + } + return errHijack + }) + + // 3. We should open a channel for receiving status code of the container + // no matter it's detached, removed on daemon side(--rm) or exit normally. + statusChan, statusErr := waitExitOrRemoved(dockerCli, context.Background(), c.ID, c.HostConfig.AutoRemove) + + // 4. Start the container. + if err := dockerCli.Client().ContainerStart(ctx, c.ID, types.ContainerStartOptions{}); err != nil { + cancelFun() + <-cErr + if c.HostConfig.AutoRemove && statusErr == nil { + // wait container to be removed + <-statusChan + } + return err + } + + // 5. Wait for attachment to break. + if c.Config.Tty && dockerCli.Out().IsTerminal() { + if err := MonitorTtySize(ctx, dockerCli, c.ID, false); err != nil { + fmt.Fprintf(dockerCli.Err(), "Error monitoring TTY size: %s\n", err) + } + } + if attchErr := <-cErr; attchErr != nil { + return attchErr + } + + if statusErr != nil { + return fmt.Errorf("can't get container's exit code: %v", statusErr) + } + + if status := <-statusChan; status != 0 { + return cli.StatusError{StatusCode: status} + } + } else { + // We're not going to attach to anything. + // Start as many containers as we want. + return startContainersWithoutAttachments(dockerCli, ctx, opts.containers) + } + + return nil +} + +func startContainersWithoutAttachments(dockerCli *command.DockerCli, ctx context.Context, containers []string) error { + var failedContainers []string + for _, container := range containers { + if err := dockerCli.Client().ContainerStart(ctx, container, types.ContainerStartOptions{}); err != nil { + fmt.Fprintf(dockerCli.Err(), "%s\n", err) + failedContainers = append(failedContainers, container) + } else { + fmt.Fprintf(dockerCli.Out(), "%s\n", container) + } + } + + if len(failedContainers) > 0 { + return fmt.Errorf("Error: failed to start containers: %v", strings.Join(failedContainers, ", ")) + } + return nil +} diff --git a/components/cli/command/container/stats.go b/components/cli/command/container/stats.go new file mode 100644 index 0000000000..ffd3fcae9f --- /dev/null +++ b/components/cli/command/container/stats.go @@ -0,0 +1,233 @@ +package container + +import ( + "fmt" + "io" + "strings" + "sync" + "text/tabwriter" + "time" + + "golang.org/x/net/context" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/events" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/system" + "github.com/spf13/cobra" +) + +type statsOptions struct { + all bool + noStream bool + + containers []string +} + +// NewStatsCommand creates a new cobra.Command for `docker stats` +func NewStatsCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts statsOptions + + cmd := &cobra.Command{ + Use: "stats [OPTIONS] [CONTAINER...]", + Short: "Display a live stream of container(s) resource usage statistics", + Args: cli.RequiresMinArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + opts.containers = args + return runStats(dockerCli, &opts) + }, + } + + flags := cmd.Flags() + flags.BoolVarP(&opts.all, "all", "a", false, "Show all containers (default shows just running)") + flags.BoolVar(&opts.noStream, "no-stream", false, "Disable streaming stats and only pull the first result") + return cmd +} + +// runStats displays a live stream of resource usage statistics for one or more containers. +// This shows real-time information on CPU usage, memory usage, and network I/O. +func runStats(dockerCli *command.DockerCli, opts *statsOptions) error { + showAll := len(opts.containers) == 0 + closeChan := make(chan error) + + ctx := context.Background() + + // monitorContainerEvents watches for container creation and removal (only + // used when calling `docker stats` without arguments). + monitorContainerEvents := func(started chan<- struct{}, c chan events.Message) { + f := filters.NewArgs() + f.Add("type", "container") + options := types.EventsOptions{ + Filters: f, + } + resBody, err := dockerCli.Client().Events(ctx, options) + // Whether we successfully subscribed to events or not, we can now + // unblock the main goroutine. + close(started) + if err != nil { + closeChan <- err + return + } + defer resBody.Close() + + system.DecodeEvents(resBody, func(event events.Message, err error) error { + if err != nil { + closeChan <- err + return nil + } + c <- event + return nil + }) + } + + // waitFirst is a WaitGroup to wait first stat data's reach for each container + waitFirst := &sync.WaitGroup{} + + cStats := stats{} + // getContainerList simulates creation event for all previously existing + // containers (only used when calling `docker stats` without arguments). + getContainerList := func() { + options := types.ContainerListOptions{ + All: opts.all, + } + cs, err := dockerCli.Client().ContainerList(ctx, options) + if err != nil { + closeChan <- err + } + for _, container := range cs { + s := &containerStats{Name: container.ID[:12]} + if cStats.add(s) { + waitFirst.Add(1) + go s.Collect(ctx, dockerCli.Client(), !opts.noStream, waitFirst) + } + } + } + + if showAll { + // If no names were specified, start a long running goroutine which + // monitors container events. We make sure we're subscribed before + // retrieving the list of running containers to avoid a race where we + // would "miss" a creation. + started := make(chan struct{}) + eh := system.InitEventHandler() + eh.Handle("create", func(e events.Message) { + if opts.all { + s := &containerStats{Name: e.ID[:12]} + if cStats.add(s) { + waitFirst.Add(1) + go s.Collect(ctx, dockerCli.Client(), !opts.noStream, waitFirst) + } + } + }) + + eh.Handle("start", func(e events.Message) { + s := &containerStats{Name: e.ID[:12]} + if cStats.add(s) { + waitFirst.Add(1) + go s.Collect(ctx, dockerCli.Client(), !opts.noStream, waitFirst) + } + }) + + eh.Handle("die", func(e events.Message) { + if !opts.all { + cStats.remove(e.ID[:12]) + } + }) + + eventChan := make(chan events.Message) + go eh.Watch(eventChan) + go monitorContainerEvents(started, eventChan) + defer close(eventChan) + <-started + + // Start a short-lived goroutine to retrieve the initial list of + // containers. + getContainerList() + } else { + // Artificially send creation events for the containers we were asked to + // monitor (same code path than we use when monitoring all containers). + for _, name := range opts.containers { + s := &containerStats{Name: name} + if cStats.add(s) { + waitFirst.Add(1) + go s.Collect(ctx, dockerCli.Client(), !opts.noStream, waitFirst) + } + } + + // We don't expect any asynchronous errors: closeChan can be closed. + close(closeChan) + + // Do a quick pause to detect any error with the provided list of + // container names. + time.Sleep(1500 * time.Millisecond) + var errs []string + cStats.mu.Lock() + for _, c := range cStats.cs { + c.mu.Lock() + if c.err != nil { + errs = append(errs, fmt.Sprintf("%s: %v", c.Name, c.err)) + } + c.mu.Unlock() + } + cStats.mu.Unlock() + if len(errs) > 0 { + return fmt.Errorf("%s", strings.Join(errs, ", ")) + } + } + + // before print to screen, make sure each container get at least one valid stat data + waitFirst.Wait() + + w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0) + printHeader := func() { + if !opts.noStream { + fmt.Fprint(dockerCli.Out(), "\033[2J") + fmt.Fprint(dockerCli.Out(), "\033[H") + } + io.WriteString(w, "CONTAINER\tCPU %\tMEM USAGE / LIMIT\tMEM %\tNET I/O\tBLOCK I/O\tPIDS\n") + } + + for range time.Tick(500 * time.Millisecond) { + printHeader() + toRemove := []string{} + cStats.mu.Lock() + for _, s := range cStats.cs { + if err := s.Display(w); err != nil && !opts.noStream { + logrus.Debugf("stats: got error for %s: %v", s.Name, err) + if err == io.EOF { + toRemove = append(toRemove, s.Name) + } + } + } + cStats.mu.Unlock() + for _, name := range toRemove { + cStats.remove(name) + } + if len(cStats.cs) == 0 && !showAll { + return nil + } + w.Flush() + if opts.noStream { + break + } + select { + case err, ok := <-closeChan: + if ok { + if err != nil { + // this is suppressing "unexpected EOF" in the cli when the + // daemon restarts so it shutdowns cleanly + if err == io.ErrUnexpectedEOF { + return nil + } + return err + } + } + default: + // just skip + } + } + return nil +} diff --git a/components/cli/command/container/stats_helpers.go b/components/cli/command/container/stats_helpers.go new file mode 100644 index 0000000000..b5e8e0472f --- /dev/null +++ b/components/cli/command/container/stats_helpers.go @@ -0,0 +1,238 @@ +package container + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "strings" + "sync" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/types" + "github.com/docker/docker/client" + "github.com/docker/go-units" + "golang.org/x/net/context" +) + +type containerStats struct { + Name string + CPUPercentage float64 + Memory float64 + MemoryLimit float64 + MemoryPercentage float64 + NetworkRx float64 + NetworkTx float64 + BlockRead float64 + BlockWrite float64 + PidsCurrent uint64 + mu sync.Mutex + err error +} + +type stats struct { + mu sync.Mutex + cs []*containerStats +} + +func (s *stats) add(cs *containerStats) bool { + s.mu.Lock() + defer s.mu.Unlock() + if _, exists := s.isKnownContainer(cs.Name); !exists { + s.cs = append(s.cs, cs) + return true + } + return false +} + +func (s *stats) remove(id string) { + s.mu.Lock() + if i, exists := s.isKnownContainer(id); exists { + s.cs = append(s.cs[:i], s.cs[i+1:]...) + } + s.mu.Unlock() +} + +func (s *stats) isKnownContainer(cid string) (int, bool) { + for i, c := range s.cs { + if c.Name == cid { + return i, true + } + } + return -1, false +} + +func (s *containerStats) Collect(ctx context.Context, cli client.APIClient, streamStats bool, waitFirst *sync.WaitGroup) { + logrus.Debugf("collecting stats for %s", s.Name) + var ( + getFirst bool + previousCPU uint64 + previousSystem uint64 + u = make(chan error, 1) + ) + + defer func() { + // if error happens and we get nothing of stats, release wait group whatever + if !getFirst { + getFirst = true + waitFirst.Done() + } + }() + + responseBody, err := cli.ContainerStats(ctx, s.Name, streamStats) + if err != nil { + s.mu.Lock() + s.err = err + s.mu.Unlock() + return + } + defer responseBody.Close() + + dec := json.NewDecoder(responseBody) + go func() { + for { + var v *types.StatsJSON + + if err := dec.Decode(&v); err != nil { + dec = json.NewDecoder(io.MultiReader(dec.Buffered(), responseBody)) + u <- err + if err == io.EOF { + break + } + time.Sleep(100 * time.Millisecond) + continue + } + + var memPercent = 0.0 + var cpuPercent = 0.0 + + // MemoryStats.Limit will never be 0 unless the container is not running and we haven't + // got any data from cgroup + if v.MemoryStats.Limit != 0 { + memPercent = float64(v.MemoryStats.Usage) / float64(v.MemoryStats.Limit) * 100.0 + } + + previousCPU = v.PreCPUStats.CPUUsage.TotalUsage + previousSystem = v.PreCPUStats.SystemUsage + cpuPercent = calculateCPUPercent(previousCPU, previousSystem, v) + blkRead, blkWrite := calculateBlockIO(v.BlkioStats) + s.mu.Lock() + s.CPUPercentage = cpuPercent + s.Memory = float64(v.MemoryStats.Usage) + s.MemoryLimit = float64(v.MemoryStats.Limit) + s.MemoryPercentage = memPercent + s.NetworkRx, s.NetworkTx = calculateNetwork(v.Networks) + s.BlockRead = float64(blkRead) + s.BlockWrite = float64(blkWrite) + s.PidsCurrent = v.PidsStats.Current + s.mu.Unlock() + u <- nil + if !streamStats { + return + } + } + }() + for { + select { + case <-time.After(2 * time.Second): + // zero out the values if we have not received an update within + // the specified duration. + s.mu.Lock() + s.CPUPercentage = 0 + s.Memory = 0 + s.MemoryPercentage = 0 + s.MemoryLimit = 0 + s.NetworkRx = 0 + s.NetworkTx = 0 + s.BlockRead = 0 + s.BlockWrite = 0 + s.PidsCurrent = 0 + s.err = errors.New("timeout waiting for stats") + s.mu.Unlock() + // if this is the first stat you get, release WaitGroup + if !getFirst { + getFirst = true + waitFirst.Done() + } + case err := <-u: + if err != nil { + s.mu.Lock() + s.err = err + s.mu.Unlock() + continue + } + s.err = nil + // if this is the first stat you get, release WaitGroup + if !getFirst { + getFirst = true + waitFirst.Done() + } + } + if !streamStats { + return + } + } +} + +func (s *containerStats) Display(w io.Writer) error { + s.mu.Lock() + defer s.mu.Unlock() + // NOTE: if you change this format, you must also change the err format below! + format := "%s\t%.2f%%\t%s / %s\t%.2f%%\t%s / %s\t%s / %s\t%d\n" + if s.err != nil { + format = "%s\t%s\t%s / %s\t%s\t%s / %s\t%s / %s\t%s\n" + errStr := "--" + fmt.Fprintf(w, format, + s.Name, errStr, errStr, errStr, errStr, errStr, errStr, errStr, errStr, errStr, + ) + err := s.err + return err + } + fmt.Fprintf(w, format, + s.Name, + s.CPUPercentage, + units.BytesSize(s.Memory), units.BytesSize(s.MemoryLimit), + s.MemoryPercentage, + units.HumanSize(s.NetworkRx), units.HumanSize(s.NetworkTx), + units.HumanSize(s.BlockRead), units.HumanSize(s.BlockWrite), + s.PidsCurrent) + return nil +} + +func calculateCPUPercent(previousCPU, previousSystem uint64, v *types.StatsJSON) float64 { + var ( + cpuPercent = 0.0 + // calculate the change for the cpu usage of the container in between readings + cpuDelta = float64(v.CPUStats.CPUUsage.TotalUsage) - float64(previousCPU) + // calculate the change for the entire system between readings + systemDelta = float64(v.CPUStats.SystemUsage) - float64(previousSystem) + ) + + if systemDelta > 0.0 && cpuDelta > 0.0 { + cpuPercent = (cpuDelta / systemDelta) * float64(len(v.CPUStats.CPUUsage.PercpuUsage)) * 100.0 + } + return cpuPercent +} + +func calculateBlockIO(blkio types.BlkioStats) (blkRead uint64, blkWrite uint64) { + for _, bioEntry := range blkio.IoServiceBytesRecursive { + switch strings.ToLower(bioEntry.Op) { + case "read": + blkRead = blkRead + bioEntry.Value + case "write": + blkWrite = blkWrite + bioEntry.Value + } + } + return +} + +func calculateNetwork(network map[string]types.NetworkStats) (float64, float64) { + var rx, tx float64 + + for _, v := range network { + rx += float64(v.RxBytes) + tx += float64(v.TxBytes) + } + return rx, tx +} diff --git a/components/cli/command/container/stats_unit_test.go b/components/cli/command/container/stats_unit_test.go new file mode 100644 index 0000000000..6f6a468068 --- /dev/null +++ b/components/cli/command/container/stats_unit_test.go @@ -0,0 +1,45 @@ +package container + +import ( + "bytes" + "testing" + + "github.com/docker/docker/api/types" +) + +func TestDisplay(t *testing.T) { + c := &containerStats{ + Name: "app", + CPUPercentage: 30.0, + Memory: 100 * 1024 * 1024.0, + MemoryLimit: 2048 * 1024 * 1024.0, + MemoryPercentage: 100.0 / 2048.0 * 100.0, + NetworkRx: 100 * 1024 * 1024, + NetworkTx: 800 * 1024 * 1024, + BlockRead: 100 * 1024 * 1024, + BlockWrite: 800 * 1024 * 1024, + PidsCurrent: 1, + } + var b bytes.Buffer + if err := c.Display(&b); err != nil { + t.Fatalf("c.Display() gave error: %s", err) + } + got := b.String() + want := "app\t30.00%\t100 MiB / 2 GiB\t4.88%\t104.9 MB / 838.9 MB\t104.9 MB / 838.9 MB\t1\n" + if got != want { + t.Fatalf("c.Display() = %q, want %q", got, want) + } +} + +func TestCalculBlockIO(t *testing.T) { + blkio := types.BlkioStats{ + IoServiceBytesRecursive: []types.BlkioStatEntry{{8, 0, "read", 1234}, {8, 1, "read", 4567}, {8, 0, "write", 123}, {8, 1, "write", 456}}, + } + blkRead, blkWrite := calculateBlockIO(blkio) + if blkRead != 5801 { + t.Fatalf("blkRead = %d, want 5801", blkRead) + } + if blkWrite != 579 { + t.Fatalf("blkWrite = %d, want 579", blkWrite) + } +} diff --git a/components/cli/command/container/stop.go b/components/cli/command/container/stop.go new file mode 100644 index 0000000000..dddb7efa22 --- /dev/null +++ b/components/cli/command/container/stop.go @@ -0,0 +1,56 @@ +package container + +import ( + "fmt" + "strings" + "time" + + "golang.org/x/net/context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +type stopOptions struct { + time int + + containers []string +} + +// NewStopCommand creates a new cobra.Command for `docker stop` +func NewStopCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts stopOptions + + cmd := &cobra.Command{ + Use: "stop [OPTIONS] CONTAINER [CONTAINER...]", + Short: "Stop one or more running containers", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.containers = args + return runStop(dockerCli, &opts) + }, + } + + flags := cmd.Flags() + flags.IntVarP(&opts.time, "time", "t", 10, "Seconds to wait for stop before killing it") + return cmd +} + +func runStop(dockerCli *command.DockerCli, opts *stopOptions) error { + ctx := context.Background() + + var errs []string + for _, container := range opts.containers { + timeout := time.Duration(opts.time) * time.Second + if err := dockerCli.Client().ContainerStop(ctx, container, &timeout); err != nil { + errs = append(errs, err.Error()) + } else { + fmt.Fprintf(dockerCli.Out(), "%s\n", container) + } + } + if len(errs) > 0 { + return fmt.Errorf("%s", strings.Join(errs, "\n")) + } + return nil +} diff --git a/components/cli/command/container/top.go b/components/cli/command/container/top.go new file mode 100644 index 0000000000..160153ba7f --- /dev/null +++ b/components/cli/command/container/top.go @@ -0,0 +1,58 @@ +package container + +import ( + "fmt" + "strings" + "text/tabwriter" + + "golang.org/x/net/context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +type topOptions struct { + container string + + args []string +} + +// NewTopCommand creates a new cobra.Command for `docker top` +func NewTopCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts topOptions + + cmd := &cobra.Command{ + Use: "top CONTAINER [ps OPTIONS]", + Short: "Display the running processes of a container", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.container = args[0] + opts.args = args[1:] + return runTop(dockerCli, &opts) + }, + } + + flags := cmd.Flags() + flags.SetInterspersed(false) + + return cmd +} + +func runTop(dockerCli *command.DockerCli, opts *topOptions) error { + ctx := context.Background() + + procList, err := dockerCli.Client().ContainerTop(ctx, opts.container, opts.args) + if err != nil { + return err + } + + w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0) + fmt.Fprintln(w, strings.Join(procList.Titles, "\t")) + + for _, proc := range procList.Processes { + fmt.Fprintln(w, strings.Join(proc, "\t")) + } + w.Flush() + return nil +} diff --git a/components/cli/command/container/tty.go b/components/cli/command/container/tty.go new file mode 100644 index 0000000000..5360c6b040 --- /dev/null +++ b/components/cli/command/container/tty.go @@ -0,0 +1,103 @@ +package container + +import ( + "fmt" + "os" + gosignal "os/signal" + "runtime" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/api/types" + "github.com/docker/docker/client" + "github.com/docker/docker/pkg/signal" + "golang.org/x/net/context" +) + +// resizeTtyTo resizes tty to specific height and width +func resizeTtyTo(ctx context.Context, client client.ContainerAPIClient, id string, height, width int, isExec bool) { + if height == 0 && width == 0 { + return + } + + options := types.ResizeOptions{ + Height: height, + Width: width, + } + + var err error + if isExec { + err = client.ContainerExecResize(ctx, id, options) + } else { + err = client.ContainerResize(ctx, id, options) + } + + if err != nil { + logrus.Debugf("Error resize: %s", err) + } +} + +// MonitorTtySize updates the container tty size when the terminal tty changes size +func MonitorTtySize(ctx context.Context, cli *command.DockerCli, id string, isExec bool) error { + resizeTty := func() { + height, width := cli.Out().GetTtySize() + resizeTtyTo(ctx, cli.Client(), id, height, width, isExec) + } + + resizeTty() + + if runtime.GOOS == "windows" { + go func() { + prevH, prevW := cli.Out().GetTtySize() + for { + time.Sleep(time.Millisecond * 250) + h, w := cli.Out().GetTtySize() + + if prevW != w || prevH != h { + resizeTty() + } + prevH = h + prevW = w + } + }() + } else { + sigchan := make(chan os.Signal, 1) + gosignal.Notify(sigchan, signal.SIGWINCH) + go func() { + for range sigchan { + resizeTty() + } + }() + } + return nil +} + +// ForwardAllSignals forwards signals to the container +func ForwardAllSignals(ctx context.Context, cli *command.DockerCli, cid string) chan os.Signal { + sigc := make(chan os.Signal, 128) + signal.CatchAll(sigc) + go func() { + for s := range sigc { + if s == signal.SIGCHLD || s == signal.SIGPIPE { + continue + } + var sig string + for sigStr, sigN := range signal.SignalMap { + if sigN == s { + sig = sigStr + break + } + } + if sig == "" { + fmt.Fprintf(cli.Err(), "Unsupported signal: %v. Discarding.\n", s) + continue + } + + if err := cli.Client().ContainerKill(ctx, cid, sig); err != nil { + logrus.Debugf("Error sending signal: %s", err) + } + } + }() + return sigc +} diff --git a/components/cli/command/container/unpause.go b/components/cli/command/container/unpause.go new file mode 100644 index 0000000000..c3635db555 --- /dev/null +++ b/components/cli/command/container/unpause.go @@ -0,0 +1,49 @@ +package container + +import ( + "fmt" + "strings" + + "golang.org/x/net/context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +type unpauseOptions struct { + containers []string +} + +// NewUnpauseCommand creates a new cobra.Command for `docker unpause` +func NewUnpauseCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts unpauseOptions + + cmd := &cobra.Command{ + Use: "unpause CONTAINER [CONTAINER...]", + Short: "Unpause all processes within one or more containers", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.containers = args + return runUnpause(dockerCli, &opts) + }, + } + return cmd +} + +func runUnpause(dockerCli *command.DockerCli, opts *unpauseOptions) error { + ctx := context.Background() + + var errs []string + for _, container := range opts.containers { + if err := dockerCli.Client().ContainerUnpause(ctx, container); err != nil { + errs = append(errs, err.Error()) + } else { + fmt.Fprintf(dockerCli.Out(), "%s\n", container) + } + } + if len(errs) > 0 { + return fmt.Errorf("%s", strings.Join(errs, "\n")) + } + return nil +} diff --git a/components/cli/command/container/update.go b/components/cli/command/container/update.go new file mode 100644 index 0000000000..b5770c8997 --- /dev/null +++ b/components/cli/command/container/update.go @@ -0,0 +1,157 @@ +package container + +import ( + "fmt" + "strings" + + "golang.org/x/net/context" + + containertypes "github.com/docker/docker/api/types/container" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/docker/go-units" + "github.com/spf13/cobra" +) + +type updateOptions struct { + blkioWeight uint16 + cpuPeriod int64 + cpuQuota int64 + cpusetCpus string + cpusetMems string + cpuShares int64 + memoryString string + memoryReservation string + memorySwap string + kernelMemory string + restartPolicy string + + nFlag int + + containers []string +} + +// NewUpdateCommand creates a new cobra.Command for `docker update` +func NewUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts updateOptions + + cmd := &cobra.Command{ + Use: "update [OPTIONS] CONTAINER [CONTAINER...]", + Short: "Update configuration of one or more containers", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.containers = args + opts.nFlag = cmd.Flags().NFlag() + return runUpdate(dockerCli, &opts) + }, + } + + flags := cmd.Flags() + flags.Uint16Var(&opts.blkioWeight, "blkio-weight", 0, "Block IO (relative weight), between 10 and 1000") + flags.Int64Var(&opts.cpuPeriod, "cpu-period", 0, "Limit CPU CFS (Completely Fair Scheduler) period") + flags.Int64Var(&opts.cpuQuota, "cpu-quota", 0, "Limit CPU CFS (Completely Fair Scheduler) quota") + flags.StringVar(&opts.cpusetCpus, "cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)") + flags.StringVar(&opts.cpusetMems, "cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)") + flags.Int64VarP(&opts.cpuShares, "cpu-shares", "c", 0, "CPU shares (relative weight)") + flags.StringVarP(&opts.memoryString, "memory", "m", "", "Memory limit") + flags.StringVar(&opts.memoryReservation, "memory-reservation", "", "Memory soft limit") + flags.StringVar(&opts.memorySwap, "memory-swap", "", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap") + flags.StringVar(&opts.kernelMemory, "kernel-memory", "", "Kernel memory limit") + flags.StringVar(&opts.restartPolicy, "restart", "", "Restart policy to apply when a container exits") + + return cmd +} + +func runUpdate(dockerCli *command.DockerCli, opts *updateOptions) error { + var err error + + if opts.nFlag == 0 { + return fmt.Errorf("You must provide one or more flags when using this command.") + } + + var memory int64 + if opts.memoryString != "" { + memory, err = units.RAMInBytes(opts.memoryString) + if err != nil { + return err + } + } + + var memoryReservation int64 + if opts.memoryReservation != "" { + memoryReservation, err = units.RAMInBytes(opts.memoryReservation) + if err != nil { + return err + } + } + + var memorySwap int64 + if opts.memorySwap != "" { + if opts.memorySwap == "-1" { + memorySwap = -1 + } else { + memorySwap, err = units.RAMInBytes(opts.memorySwap) + if err != nil { + return err + } + } + } + + var kernelMemory int64 + if opts.kernelMemory != "" { + kernelMemory, err = units.RAMInBytes(opts.kernelMemory) + if err != nil { + return err + } + } + + var restartPolicy containertypes.RestartPolicy + if opts.restartPolicy != "" { + restartPolicy, err = runconfigopts.ParseRestartPolicy(opts.restartPolicy) + if err != nil { + return err + } + } + + resources := containertypes.Resources{ + BlkioWeight: opts.blkioWeight, + CpusetCpus: opts.cpusetCpus, + CpusetMems: opts.cpusetMems, + CPUShares: opts.cpuShares, + Memory: memory, + MemoryReservation: memoryReservation, + MemorySwap: memorySwap, + KernelMemory: kernelMemory, + CPUPeriod: opts.cpuPeriod, + CPUQuota: opts.cpuQuota, + } + + updateConfig := containertypes.UpdateConfig{ + Resources: resources, + RestartPolicy: restartPolicy, + } + + ctx := context.Background() + + var ( + warns []string + errs []string + ) + for _, container := range opts.containers { + r, err := dockerCli.Client().ContainerUpdate(ctx, container, updateConfig) + if err != nil { + errs = append(errs, err.Error()) + } else { + fmt.Fprintf(dockerCli.Out(), "%s\n", container) + } + warns = append(warns, r.Warnings...) + } + if len(warns) > 0 { + fmt.Fprintf(dockerCli.Out(), "%s", strings.Join(warns, "\n")) + } + if len(errs) > 0 { + return fmt.Errorf("%s", strings.Join(errs, "\n")) + } + return nil +} diff --git a/components/cli/command/container/utils.go b/components/cli/command/container/utils.go new file mode 100644 index 0000000000..8c993dcce5 --- /dev/null +++ b/components/cli/command/container/utils.go @@ -0,0 +1,92 @@ +package container + +import ( + "fmt" + "strconv" + + "golang.org/x/net/context" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/events" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/system" + clientapi "github.com/docker/docker/client" +) + +func waitExitOrRemoved(dockerCli *command.DockerCli, ctx context.Context, containerID string, waitRemove bool) (chan int, error) { + if len(containerID) == 0 { + // containerID can never be empty + panic("Internal Error: waitExitOrRemoved needs a containerID as parameter") + } + + statusChan := make(chan int) + exitCode := 125 + + eventProcessor := func(e events.Message, err error) error { + if err != nil { + statusChan <- exitCode + return fmt.Errorf("failed to decode event: %v", err) + } + + stopProcessing := false + switch e.Status { + case "die": + if v, ok := e.Actor.Attributes["exitCode"]; ok { + code, cerr := strconv.Atoi(v) + if cerr != nil { + logrus.Errorf("failed to convert exitcode '%q' to int: %v", v, cerr) + } else { + exitCode = code + } + } + if !waitRemove { + stopProcessing = true + } + case "detach": + exitCode = 0 + stopProcessing = true + case "destroy": + stopProcessing = true + } + + if stopProcessing { + statusChan <- exitCode + // stop the loop processing + return fmt.Errorf("done") + } + + return nil + } + + // Get events via Events API + f := filters.NewArgs() + f.Add("type", "container") + f.Add("container", containerID) + options := types.EventsOptions{ + Filters: f, + } + resBody, err := dockerCli.Client().Events(ctx, options) + if err != nil { + return nil, fmt.Errorf("can't get events from daemon: %v", err) + } + + go system.DecodeEvents(resBody, eventProcessor) + + return statusChan, nil +} + +// getExitCode performs an inspect on the container. It returns +// the running state and the exit code. +func getExitCode(dockerCli *command.DockerCli, ctx context.Context, containerID string) (bool, int, error) { + c, err := dockerCli.Client().ContainerInspect(ctx, containerID) + if err != nil { + // If we can't connect, then the daemon probably died. + if err != clientapi.ErrConnectionFailed { + return false, -1, err + } + return false, -1, nil + } + return c.State.Running, c.State.ExitCode, nil +} diff --git a/components/cli/command/container/wait.go b/components/cli/command/container/wait.go new file mode 100644 index 0000000000..19ccf7ac25 --- /dev/null +++ b/components/cli/command/container/wait.go @@ -0,0 +1,50 @@ +package container + +import ( + "fmt" + "strings" + + "golang.org/x/net/context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +type waitOptions struct { + containers []string +} + +// NewWaitCommand creates a new cobra.Command for `docker wait` +func NewWaitCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts waitOptions + + cmd := &cobra.Command{ + Use: "wait CONTAINER [CONTAINER...]", + Short: "Block until one or more containers stop, then print their exit codes", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.containers = args + return runWait(dockerCli, &opts) + }, + } + return cmd +} + +func runWait(dockerCli *command.DockerCli, opts *waitOptions) error { + ctx := context.Background() + + var errs []string + for _, container := range opts.containers { + status, err := dockerCli.Client().ContainerWait(ctx, container) + if err != nil { + errs = append(errs, err.Error()) + } else { + fmt.Fprintf(dockerCli.Out(), "%d\n", status) + } + } + if len(errs) > 0 { + return fmt.Errorf("%s", strings.Join(errs, "\n")) + } + return nil +} diff --git a/components/cli/command/credentials.go b/components/cli/command/credentials.go new file mode 100644 index 0000000000..06e9d1de20 --- /dev/null +++ b/components/cli/command/credentials.go @@ -0,0 +1,44 @@ +package command + +import ( + "github.com/docker/docker/api/types" + "github.com/docker/docker/cliconfig/configfile" + "github.com/docker/docker/cliconfig/credentials" +) + +// GetCredentials loads the user credentials from a credentials store. +// The store is determined by the config file settings. +func GetCredentials(c *configfile.ConfigFile, serverAddress string) (types.AuthConfig, error) { + s := LoadCredentialsStore(c) + return s.Get(serverAddress) +} + +// GetAllCredentials loads all credentials from a credentials store. +// The store is determined by the config file settings. +func GetAllCredentials(c *configfile.ConfigFile) (map[string]types.AuthConfig, error) { + s := LoadCredentialsStore(c) + return s.GetAll() +} + +// StoreCredentials saves the user credentials in a credentials store. +// The store is determined by the config file settings. +func StoreCredentials(c *configfile.ConfigFile, auth types.AuthConfig) error { + s := LoadCredentialsStore(c) + return s.Store(auth) +} + +// EraseCredentials removes the user credentials from a credentials store. +// The store is determined by the config file settings. +func EraseCredentials(c *configfile.ConfigFile, serverAddress string) error { + s := LoadCredentialsStore(c) + return s.Erase(serverAddress) +} + +// LoadCredentialsStore initializes a new credentials store based +// in the settings provided in the configuration file. +func LoadCredentialsStore(c *configfile.ConfigFile) credentials.Store { + if c.CredentialsStore != "" { + return credentials.NewNativeStore(c) + } + return credentials.NewFileStore(c) +} diff --git a/components/cli/command/formatter/container.go b/components/cli/command/formatter/container.go new file mode 100644 index 0000000000..f1c985791b --- /dev/null +++ b/components/cli/command/formatter/container.go @@ -0,0 +1,208 @@ +package formatter + +import ( + "bytes" + "fmt" + "strconv" + "strings" + "time" + + "github.com/docker/docker/api" + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/stringutils" + "github.com/docker/go-units" +) + +const ( + defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Ports}}\t{{.Names}}" + + containerIDHeader = "CONTAINER ID" + namesHeader = "NAMES" + commandHeader = "COMMAND" + runningForHeader = "CREATED" + statusHeader = "STATUS" + portsHeader = "PORTS" + mountsHeader = "MOUNTS" +) + +// ContainerContext contains container specific information required by the formater, encapsulate a Context struct. +type ContainerContext struct { + Context + // Size when set to true will display the size of the output. + Size bool + // Containers + Containers []types.Container +} + +func (ctx ContainerContext) Write() { + switch ctx.Format { + case tableFormatKey: + if ctx.Quiet { + ctx.Format = defaultQuietFormat + } else { + ctx.Format = defaultContainerTableFormat + if ctx.Size { + ctx.Format += `\t{{.Size}}` + } + } + case rawFormatKey: + if ctx.Quiet { + ctx.Format = `container_id: {{.ID}}` + } else { + ctx.Format = `container_id: {{.ID}}\nimage: {{.Image}}\ncommand: {{.Command}}\ncreated_at: {{.CreatedAt}}\nstatus: {{.Status}}\nnames: {{.Names}}\nlabels: {{.Labels}}\nports: {{.Ports}}\n` + if ctx.Size { + ctx.Format += `size: {{.Size}}\n` + } + } + } + + ctx.buffer = bytes.NewBufferString("") + ctx.preformat() + + tmpl, err := ctx.parseFormat() + if err != nil { + return + } + + for _, container := range ctx.Containers { + containerCtx := &containerContext{ + trunc: ctx.Trunc, + c: container, + } + err = ctx.contextFormat(tmpl, containerCtx) + if err != nil { + return + } + } + + ctx.postformat(tmpl, &containerContext{}) +} + +type containerContext struct { + baseSubContext + trunc bool + c types.Container +} + +func (c *containerContext) ID() string { + c.addHeader(containerIDHeader) + if c.trunc { + return stringid.TruncateID(c.c.ID) + } + return c.c.ID +} + +func (c *containerContext) Names() string { + c.addHeader(namesHeader) + names := stripNamePrefix(c.c.Names) + if c.trunc { + for _, name := range names { + if len(strings.Split(name, "/")) == 1 { + names = []string{name} + break + } + } + } + return strings.Join(names, ",") +} + +func (c *containerContext) Image() string { + c.addHeader(imageHeader) + if c.c.Image == "" { + return "" + } + if c.trunc { + if trunc := stringid.TruncateID(c.c.ImageID); trunc == stringid.TruncateID(c.c.Image) { + return trunc + } + } + return c.c.Image +} + +func (c *containerContext) Command() string { + c.addHeader(commandHeader) + command := c.c.Command + if c.trunc { + command = stringutils.Ellipsis(command, 20) + } + return strconv.Quote(command) +} + +func (c *containerContext) CreatedAt() string { + c.addHeader(createdAtHeader) + return time.Unix(int64(c.c.Created), 0).String() +} + +func (c *containerContext) RunningFor() string { + c.addHeader(runningForHeader) + createdAt := time.Unix(int64(c.c.Created), 0) + return units.HumanDuration(time.Now().UTC().Sub(createdAt)) +} + +func (c *containerContext) Ports() string { + c.addHeader(portsHeader) + return api.DisplayablePorts(c.c.Ports) +} + +func (c *containerContext) Status() string { + c.addHeader(statusHeader) + return c.c.Status +} + +func (c *containerContext) Size() string { + c.addHeader(sizeHeader) + srw := units.HumanSize(float64(c.c.SizeRw)) + sv := units.HumanSize(float64(c.c.SizeRootFs)) + + sf := srw + if c.c.SizeRootFs > 0 { + sf = fmt.Sprintf("%s (virtual %s)", srw, sv) + } + return sf +} + +func (c *containerContext) Labels() string { + c.addHeader(labelsHeader) + if c.c.Labels == nil { + return "" + } + + var joinLabels []string + for k, v := range c.c.Labels { + joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v)) + } + return strings.Join(joinLabels, ",") +} + +func (c *containerContext) Label(name string) string { + n := strings.Split(name, ".") + r := strings.NewReplacer("-", " ", "_", " ") + h := r.Replace(n[len(n)-1]) + + c.addHeader(h) + + if c.c.Labels == nil { + return "" + } + return c.c.Labels[name] +} + +func (c *containerContext) Mounts() string { + c.addHeader(mountsHeader) + + var name string + var mounts []string + for _, m := range c.c.Mounts { + if m.Name == "" { + name = m.Source + } else { + name = m.Name + } + if c.trunc { + name = stringutils.Ellipsis(name, 15) + } + mounts = append(mounts, name) + } + return strings.Join(mounts, ",") +} diff --git a/components/cli/command/formatter/container_test.go b/components/cli/command/formatter/container_test.go new file mode 100644 index 0000000000..deaa915a89 --- /dev/null +++ b/components/cli/command/formatter/container_test.go @@ -0,0 +1,404 @@ +package formatter + +import ( + "bytes" + "fmt" + "strings" + "testing" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/stringid" +) + +func TestContainerPsContext(t *testing.T) { + containerID := stringid.GenerateRandomID() + unix := time.Now().Add(-65 * time.Second).Unix() + + var ctx containerContext + cases := []struct { + container types.Container + trunc bool + expValue string + expHeader string + call func() string + }{ + {types.Container{ID: containerID}, true, stringid.TruncateID(containerID), containerIDHeader, ctx.ID}, + {types.Container{ID: containerID}, false, containerID, containerIDHeader, ctx.ID}, + {types.Container{Names: []string{"/foobar_baz"}}, true, "foobar_baz", namesHeader, ctx.Names}, + {types.Container{Image: "ubuntu"}, true, "ubuntu", imageHeader, ctx.Image}, + {types.Container{Image: "verylongimagename"}, true, "verylongimagename", imageHeader, ctx.Image}, + {types.Container{Image: "verylongimagename"}, false, "verylongimagename", imageHeader, ctx.Image}, + {types.Container{ + Image: "a5a665ff33eced1e0803148700880edab4", + ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5", + }, + true, + "a5a665ff33ec", + imageHeader, + ctx.Image, + }, + {types.Container{ + Image: "a5a665ff33eced1e0803148700880edab4", + ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5", + }, + false, + "a5a665ff33eced1e0803148700880edab4", + imageHeader, + ctx.Image, + }, + {types.Container{Image: ""}, true, "", imageHeader, ctx.Image}, + {types.Container{Command: "sh -c 'ls -la'"}, true, `"sh -c 'ls -la'"`, commandHeader, ctx.Command}, + {types.Container{Created: unix}, true, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt}, + {types.Container{Ports: []types.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, true, "8080/tcp", portsHeader, ctx.Ports}, + {types.Container{Status: "RUNNING"}, true, "RUNNING", statusHeader, ctx.Status}, + {types.Container{SizeRw: 10}, true, "10 B", sizeHeader, ctx.Size}, + {types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10 B (virtual 20 B)", sizeHeader, ctx.Size}, + {types.Container{}, true, "", labelsHeader, ctx.Labels}, + {types.Container{Labels: map[string]string{"cpu": "6", "storage": "ssd"}}, true, "cpu=6,storage=ssd", labelsHeader, ctx.Labels}, + {types.Container{Created: unix}, true, "About a minute", runningForHeader, ctx.RunningFor}, + {types.Container{ + Mounts: []types.MountPoint{ + { + Name: "this-is-a-long-volume-name-and-will-be-truncated-if-trunc-is-set", + Driver: "local", + Source: "/a/path", + }, + }, + }, true, "this-is-a-lo...", mountsHeader, ctx.Mounts}, + {types.Container{ + Mounts: []types.MountPoint{ + { + Driver: "local", + Source: "/a/path", + }, + }, + }, false, "/a/path", mountsHeader, ctx.Mounts}, + {types.Container{ + Mounts: []types.MountPoint{ + { + Name: "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203", + Driver: "local", + Source: "/a/path", + }, + }, + }, false, "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203", mountsHeader, ctx.Mounts}, + } + + for _, c := range cases { + ctx = containerContext{c: c.container, trunc: c.trunc} + v := c.call() + if strings.Contains(v, ",") { + compareMultipleValues(t, v, c.expValue) + } else if v != c.expValue { + t.Fatalf("Expected %s, was %s\n", c.expValue, v) + } + + h := ctx.fullHeader() + if h != c.expHeader { + t.Fatalf("Expected %s, was %s\n", c.expHeader, h) + } + } + + c1 := types.Container{Labels: map[string]string{"com.docker.swarm.swarm-id": "33", "com.docker.swarm.node_name": "ubuntu"}} + ctx = containerContext{c: c1, trunc: true} + + sid := ctx.Label("com.docker.swarm.swarm-id") + node := ctx.Label("com.docker.swarm.node_name") + if sid != "33" { + t.Fatalf("Expected 33, was %s\n", sid) + } + + if node != "ubuntu" { + t.Fatalf("Expected ubuntu, was %s\n", node) + } + + h := ctx.fullHeader() + if h != "SWARM ID\tNODE NAME" { + t.Fatalf("Expected %s, was %s\n", "SWARM ID\tNODE NAME", h) + + } + + c2 := types.Container{} + ctx = containerContext{c: c2, trunc: true} + + label := ctx.Label("anything.really") + if label != "" { + t.Fatalf("Expected an empty string, was %s", label) + } + + ctx = containerContext{c: c2, trunc: true} + fullHeader := ctx.fullHeader() + if fullHeader != "" { + t.Fatalf("Expected fullHeader to be empty, was %s", fullHeader) + } + +} + +func TestContainerContextWrite(t *testing.T) { + unixTime := time.Now().AddDate(0, 0, -1).Unix() + expectedTime := time.Unix(unixTime, 0).String() + + contexts := []struct { + context ContainerContext + expected string + }{ + // Errors + { + ContainerContext{ + Context: Context{ + Format: "{{InvalidFunction}}", + }, + }, + `Template parsing error: template: :1: function "InvalidFunction" not defined +`, + }, + { + ContainerContext{ + Context: Context{ + Format: "{{nil}}", + }, + }, + `Template parsing error: template: :1:2: executing "" at : nil is not a command +`, + }, + // Table Format + { + ContainerContext{ + Context: Context{ + Format: "table", + }, + Size: true, + }, + `CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES SIZE +containerID1 ubuntu "" 24 hours ago foobar_baz 0 B +containerID2 ubuntu "" 24 hours ago foobar_bar 0 B +`, + }, + { + ContainerContext{ + Context: Context{ + Format: "table", + }, + }, + `CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +containerID1 ubuntu "" 24 hours ago foobar_baz +containerID2 ubuntu "" 24 hours ago foobar_bar +`, + }, + { + ContainerContext{ + Context: Context{ + Format: "table {{.Image}}", + }, + }, + "IMAGE\nubuntu\nubuntu\n", + }, + { + ContainerContext{ + Context: Context{ + Format: "table {{.Image}}", + }, + Size: true, + }, + "IMAGE\nubuntu\nubuntu\n", + }, + { + ContainerContext{ + Context: Context{ + Format: "table {{.Image}}", + Quiet: true, + }, + }, + "IMAGE\nubuntu\nubuntu\n", + }, + { + ContainerContext{ + Context: Context{ + Format: "table", + Quiet: true, + }, + }, + "containerID1\ncontainerID2\n", + }, + // Raw Format + { + ContainerContext{ + Context: Context{ + Format: "raw", + }, + }, + fmt.Sprintf(`container_id: containerID1 +image: ubuntu +command: "" +created_at: %s +status: +names: foobar_baz +labels: +ports: + +container_id: containerID2 +image: ubuntu +command: "" +created_at: %s +status: +names: foobar_bar +labels: +ports: + +`, expectedTime, expectedTime), + }, + { + ContainerContext{ + Context: Context{ + Format: "raw", + }, + Size: true, + }, + fmt.Sprintf(`container_id: containerID1 +image: ubuntu +command: "" +created_at: %s +status: +names: foobar_baz +labels: +ports: +size: 0 B + +container_id: containerID2 +image: ubuntu +command: "" +created_at: %s +status: +names: foobar_bar +labels: +ports: +size: 0 B + +`, expectedTime, expectedTime), + }, + { + ContainerContext{ + Context: Context{ + Format: "raw", + Quiet: true, + }, + }, + "container_id: containerID1\ncontainer_id: containerID2\n", + }, + // Custom Format + { + ContainerContext{ + Context: Context{ + Format: "{{.Image}}", + }, + }, + "ubuntu\nubuntu\n", + }, + { + ContainerContext{ + Context: Context{ + Format: "{{.Image}}", + }, + Size: true, + }, + "ubuntu\nubuntu\n", + }, + } + + for _, context := range contexts { + containers := []types.Container{ + {ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unixTime}, + {ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unixTime}, + } + out := bytes.NewBufferString("") + context.context.Output = out + context.context.Containers = containers + context.context.Write() + actual := out.String() + if actual != context.expected { + t.Fatalf("Expected \n%s, got \n%s", context.expected, actual) + } + // Clean buffer + out.Reset() + } +} + +func TestContainerContextWriteWithNoContainers(t *testing.T) { + out := bytes.NewBufferString("") + containers := []types.Container{} + + contexts := []struct { + context ContainerContext + expected string + }{ + { + ContainerContext{ + Context: Context{ + Format: "{{.Image}}", + Output: out, + }, + }, + "", + }, + { + ContainerContext{ + Context: Context{ + Format: "table {{.Image}}", + Output: out, + }, + }, + "IMAGE\n", + }, + { + ContainerContext{ + Context: Context{ + Format: "{{.Image}}", + Output: out, + }, + Size: true, + }, + "", + }, + { + ContainerContext{ + Context: Context{ + Format: "table {{.Image}}", + Output: out, + }, + Size: true, + }, + "IMAGE\n", + }, + { + ContainerContext{ + Context: Context{ + Format: "table {{.Image}}\t{{.Size}}", + Output: out, + }, + }, + "IMAGE SIZE\n", + }, + { + ContainerContext{ + Context: Context{ + Format: "table {{.Image}}\t{{.Size}}", + Output: out, + }, + Size: true, + }, + "IMAGE SIZE\n", + }, + } + + for _, context := range contexts { + context.context.Containers = containers + context.context.Write() + actual := out.String() + if actual != context.expected { + t.Fatalf("Expected \n%s, got \n%s", context.expected, actual) + } + // Clean buffer + out.Reset() + } +} diff --git a/components/cli/command/formatter/custom.go b/components/cli/command/formatter/custom.go new file mode 100644 index 0000000000..2aa2e7b554 --- /dev/null +++ b/components/cli/command/formatter/custom.go @@ -0,0 +1,50 @@ +package formatter + +import ( + "strings" +) + +const ( + tableKey = "table" + + imageHeader = "IMAGE" + createdSinceHeader = "CREATED" + createdAtHeader = "CREATED AT" + sizeHeader = "SIZE" + labelsHeader = "LABELS" + nameHeader = "NAME" + driverHeader = "DRIVER" + scopeHeader = "SCOPE" +) + +type subContext interface { + fullHeader() string + addHeader(header string) +} + +type baseSubContext struct { + header []string +} + +func (c *baseSubContext) fullHeader() string { + if c.header == nil { + return "" + } + return strings.Join(c.header, "\t") +} + +func (c *baseSubContext) addHeader(header string) { + if c.header == nil { + c.header = []string{} + } + c.header = append(c.header, strings.ToUpper(header)) +} + +func stripNamePrefix(ss []string) []string { + sss := make([]string, len(ss)) + for i, s := range ss { + sss[i] = s[1:] + } + + return sss +} diff --git a/components/cli/command/formatter/custom_test.go b/components/cli/command/formatter/custom_test.go new file mode 100644 index 0000000000..da42039dca --- /dev/null +++ b/components/cli/command/formatter/custom_test.go @@ -0,0 +1,28 @@ +package formatter + +import ( + "reflect" + "strings" + "testing" +) + +func compareMultipleValues(t *testing.T, value, expected string) { + // comma-separated values means probably a map input, which won't + // be guaranteed to have the same order as our expected value + // We'll create maps and use reflect.DeepEquals to check instead: + entriesMap := make(map[string]string) + expMap := make(map[string]string) + entries := strings.Split(value, ",") + expectedEntries := strings.Split(expected, ",") + for _, entry := range entries { + keyval := strings.Split(entry, "=") + entriesMap[keyval[0]] = keyval[1] + } + for _, expected := range expectedEntries { + keyval := strings.Split(expected, "=") + expMap[keyval[0]] = keyval[1] + } + if !reflect.DeepEqual(expMap, entriesMap) { + t.Fatalf("Expected entries: %v, got: %v", expected, value) + } +} diff --git a/components/cli/command/formatter/formatter.go b/components/cli/command/formatter/formatter.go new file mode 100644 index 0000000000..de71c3cdd4 --- /dev/null +++ b/components/cli/command/formatter/formatter.go @@ -0,0 +1,90 @@ +package formatter + +import ( + "bytes" + "fmt" + "io" + "strings" + "text/tabwriter" + "text/template" + + "github.com/docker/docker/utils/templates" +) + +const ( + tableFormatKey = "table" + rawFormatKey = "raw" + + defaultQuietFormat = "{{.ID}}" +) + +// Context contains information required by the formatter to print the output as desired. +type Context struct { + // Output is the output stream to which the formatted string is written. + Output io.Writer + // Format is used to choose raw, table or custom format for the output. + Format string + // Quiet when set to true will simply print minimal information. + Quiet bool + // Trunc when set to true will truncate the output of certain fields such as Container ID. + Trunc bool + + // internal element + table bool + finalFormat string + header string + buffer *bytes.Buffer +} + +func (c *Context) preformat() { + c.finalFormat = c.Format + + if strings.HasPrefix(c.Format, tableKey) { + c.table = true + c.finalFormat = c.finalFormat[len(tableKey):] + } + + c.finalFormat = strings.Trim(c.finalFormat, " ") + r := strings.NewReplacer(`\t`, "\t", `\n`, "\n") + c.finalFormat = r.Replace(c.finalFormat) +} + +func (c *Context) parseFormat() (*template.Template, error) { + tmpl, err := templates.Parse(c.finalFormat) + if err != nil { + c.buffer.WriteString(fmt.Sprintf("Template parsing error: %v\n", err)) + c.buffer.WriteTo(c.Output) + } + return tmpl, err +} + +func (c *Context) postformat(tmpl *template.Template, subContext subContext) { + if c.table { + if len(c.header) == 0 { + // if we still don't have a header, we didn't have any containers so we need to fake it to get the right headers from the template + tmpl.Execute(bytes.NewBufferString(""), subContext) + c.header = subContext.fullHeader() + } + + t := tabwriter.NewWriter(c.Output, 20, 1, 3, ' ', 0) + t.Write([]byte(c.header)) + t.Write([]byte("\n")) + c.buffer.WriteTo(t) + t.Flush() + } else { + c.buffer.WriteTo(c.Output) + } +} + +func (c *Context) contextFormat(tmpl *template.Template, subContext subContext) error { + if err := tmpl.Execute(c.buffer, subContext); err != nil { + c.buffer = bytes.NewBufferString(fmt.Sprintf("Template parsing error: %v\n", err)) + c.buffer.WriteTo(c.Output) + return err + } + if c.table && len(c.header) == 0 { + c.header = subContext.fullHeader() + } + c.buffer.WriteString("\n") + return nil +} diff --git a/components/cli/command/formatter/image.go b/components/cli/command/formatter/image.go new file mode 100644 index 0000000000..0ffcfaf728 --- /dev/null +++ b/components/cli/command/formatter/image.go @@ -0,0 +1,229 @@ +package formatter + +import ( + "bytes" + "strings" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/reference" + "github.com/docker/go-units" +) + +const ( + defaultImageTableFormat = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.Size}}" + defaultImageTableFormatWithDigest = "table {{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.Size}}" + + imageIDHeader = "IMAGE ID" + repositoryHeader = "REPOSITORY" + tagHeader = "TAG" + digestHeader = "DIGEST" +) + +// ImageContext contains image specific information required by the formater, encapsulate a Context struct. +type ImageContext struct { + Context + Digest bool + // Images + Images []types.Image +} + +func isDangling(image types.Image) bool { + return len(image.RepoTags) == 1 && image.RepoTags[0] == ":" && len(image.RepoDigests) == 1 && image.RepoDigests[0] == "@" +} + +func (ctx ImageContext) Write() { + switch ctx.Format { + case tableFormatKey: + ctx.Format = defaultImageTableFormat + if ctx.Digest { + ctx.Format = defaultImageTableFormatWithDigest + } + if ctx.Quiet { + ctx.Format = defaultQuietFormat + } + case rawFormatKey: + if ctx.Quiet { + ctx.Format = `image_id: {{.ID}}` + } else { + if ctx.Digest { + ctx.Format = `repository: {{ .Repository }} +tag: {{.Tag}} +digest: {{.Digest}} +image_id: {{.ID}} +created_at: {{.CreatedAt}} +virtual_size: {{.Size}} +` + } else { + ctx.Format = `repository: {{ .Repository }} +tag: {{.Tag}} +image_id: {{.ID}} +created_at: {{.CreatedAt}} +virtual_size: {{.Size}} +` + } + } + } + + ctx.buffer = bytes.NewBufferString("") + ctx.preformat() + if ctx.table && ctx.Digest && !strings.Contains(ctx.Format, "{{.Digest}}") { + ctx.finalFormat += "\t{{.Digest}}" + } + + tmpl, err := ctx.parseFormat() + if err != nil { + return + } + + for _, image := range ctx.Images { + images := []*imageContext{} + if isDangling(image) { + images = append(images, &imageContext{ + trunc: ctx.Trunc, + i: image, + repo: "", + tag: "", + digest: "", + }) + } else { + repoTags := map[string][]string{} + repoDigests := map[string][]string{} + + for _, refString := range append(image.RepoTags) { + ref, err := reference.ParseNamed(refString) + if err != nil { + continue + } + if nt, ok := ref.(reference.NamedTagged); ok { + repoTags[ref.Name()] = append(repoTags[ref.Name()], nt.Tag()) + } + } + for _, refString := range append(image.RepoDigests) { + ref, err := reference.ParseNamed(refString) + if err != nil { + continue + } + if c, ok := ref.(reference.Canonical); ok { + repoDigests[ref.Name()] = append(repoDigests[ref.Name()], c.Digest().String()) + } + } + + for repo, tags := range repoTags { + digests := repoDigests[repo] + + // Do not display digests as their own row + delete(repoDigests, repo) + + if !ctx.Digest { + // Ignore digest references, just show tag once + digests = nil + } + + for _, tag := range tags { + if len(digests) == 0 { + images = append(images, &imageContext{ + trunc: ctx.Trunc, + i: image, + repo: repo, + tag: tag, + digest: "", + }) + continue + } + // Display the digests for each tag + for _, dgst := range digests { + images = append(images, &imageContext{ + trunc: ctx.Trunc, + i: image, + repo: repo, + tag: tag, + digest: dgst, + }) + } + + } + } + + // Show rows for remaining digest only references + for repo, digests := range repoDigests { + // If digests are displayed, show row per digest + if ctx.Digest { + for _, dgst := range digests { + images = append(images, &imageContext{ + trunc: ctx.Trunc, + i: image, + repo: repo, + tag: "", + digest: dgst, + }) + } + } else { + images = append(images, &imageContext{ + trunc: ctx.Trunc, + i: image, + repo: repo, + tag: "", + }) + } + } + } + for _, imageCtx := range images { + err = ctx.contextFormat(tmpl, imageCtx) + if err != nil { + return + } + } + } + + ctx.postformat(tmpl, &imageContext{}) +} + +type imageContext struct { + baseSubContext + trunc bool + i types.Image + repo string + tag string + digest string +} + +func (c *imageContext) ID() string { + c.addHeader(imageIDHeader) + if c.trunc { + return stringid.TruncateID(c.i.ID) + } + return c.i.ID +} + +func (c *imageContext) Repository() string { + c.addHeader(repositoryHeader) + return c.repo +} + +func (c *imageContext) Tag() string { + c.addHeader(tagHeader) + return c.tag +} + +func (c *imageContext) Digest() string { + c.addHeader(digestHeader) + return c.digest +} + +func (c *imageContext) CreatedSince() string { + c.addHeader(createdSinceHeader) + createdAt := time.Unix(int64(c.i.Created), 0) + return units.HumanDuration(time.Now().UTC().Sub(createdAt)) +} + +func (c *imageContext) CreatedAt() string { + c.addHeader(createdAtHeader) + return time.Unix(int64(c.i.Created), 0).String() +} + +func (c *imageContext) Size() string { + c.addHeader(sizeHeader) + return units.HumanSize(float64(c.i.Size)) +} diff --git a/components/cli/command/formatter/image_test.go b/components/cli/command/formatter/image_test.go new file mode 100644 index 0000000000..7c87f393fc --- /dev/null +++ b/components/cli/command/formatter/image_test.go @@ -0,0 +1,345 @@ +package formatter + +import ( + "bytes" + "fmt" + "strings" + "testing" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/stringid" +) + +func TestImageContext(t *testing.T) { + imageID := stringid.GenerateRandomID() + unix := time.Now().Unix() + + var ctx imageContext + cases := []struct { + imageCtx imageContext + expValue string + expHeader string + call func() string + }{ + {imageContext{ + i: types.Image{ID: imageID}, + trunc: true, + }, stringid.TruncateID(imageID), imageIDHeader, ctx.ID}, + {imageContext{ + i: types.Image{ID: imageID}, + trunc: false, + }, imageID, imageIDHeader, ctx.ID}, + {imageContext{ + i: types.Image{Size: 10}, + trunc: true, + }, "10 B", sizeHeader, ctx.Size}, + {imageContext{ + i: types.Image{Created: unix}, + trunc: true, + }, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt}, + // FIXME + // {imageContext{ + // i: types.Image{Created: unix}, + // trunc: true, + // }, units.HumanDuration(time.Unix(unix, 0)), createdSinceHeader, ctx.CreatedSince}, + {imageContext{ + i: types.Image{}, + repo: "busybox", + }, "busybox", repositoryHeader, ctx.Repository}, + {imageContext{ + i: types.Image{}, + tag: "latest", + }, "latest", tagHeader, ctx.Tag}, + {imageContext{ + i: types.Image{}, + digest: "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", + }, "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", digestHeader, ctx.Digest}, + } + + for _, c := range cases { + ctx = c.imageCtx + v := c.call() + if strings.Contains(v, ",") { + compareMultipleValues(t, v, c.expValue) + } else if v != c.expValue { + t.Fatalf("Expected %s, was %s\n", c.expValue, v) + } + + h := ctx.fullHeader() + if h != c.expHeader { + t.Fatalf("Expected %s, was %s\n", c.expHeader, h) + } + } +} + +func TestImageContextWrite(t *testing.T) { + unixTime := time.Now().AddDate(0, 0, -1).Unix() + expectedTime := time.Unix(unixTime, 0).String() + + contexts := []struct { + context ImageContext + expected string + }{ + // Errors + { + ImageContext{ + Context: Context{ + Format: "{{InvalidFunction}}", + }, + }, + `Template parsing error: template: :1: function "InvalidFunction" not defined +`, + }, + { + ImageContext{ + Context: Context{ + Format: "{{nil}}", + }, + }, + `Template parsing error: template: :1:2: executing "" at : nil is not a command +`, + }, + // Table Format + { + ImageContext{ + Context: Context{ + Format: "table", + }, + }, + `REPOSITORY TAG IMAGE ID CREATED SIZE +image tag1 imageID1 24 hours ago 0 B +image tag2 imageID2 24 hours ago 0 B + imageID3 24 hours ago 0 B +`, + }, + { + ImageContext{ + Context: Context{ + Format: "table {{.Repository}}", + }, + }, + "REPOSITORY\nimage\nimage\n\n", + }, + { + ImageContext{ + Context: Context{ + Format: "table {{.Repository}}", + }, + Digest: true, + }, + `REPOSITORY DIGEST +image sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf +image + +`, + }, + { + ImageContext{ + Context: Context{ + Format: "table {{.Repository}}", + Quiet: true, + }, + }, + "REPOSITORY\nimage\nimage\n\n", + }, + { + ImageContext{ + Context: Context{ + Format: "table", + Quiet: true, + }, + }, + "imageID1\nimageID2\nimageID3\n", + }, + { + ImageContext{ + Context: Context{ + Format: "table", + Quiet: false, + }, + Digest: true, + }, + `REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE +image tag1 sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf imageID1 24 hours ago 0 B +image tag2 imageID2 24 hours ago 0 B + imageID3 24 hours ago 0 B +`, + }, + { + ImageContext{ + Context: Context{ + Format: "table", + Quiet: true, + }, + Digest: true, + }, + "imageID1\nimageID2\nimageID3\n", + }, + // Raw Format + { + ImageContext{ + Context: Context{ + Format: "raw", + }, + }, + fmt.Sprintf(`repository: image +tag: tag1 +image_id: imageID1 +created_at: %s +virtual_size: 0 B + +repository: image +tag: tag2 +image_id: imageID2 +created_at: %s +virtual_size: 0 B + +repository: +tag: +image_id: imageID3 +created_at: %s +virtual_size: 0 B + +`, expectedTime, expectedTime, expectedTime), + }, + { + ImageContext{ + Context: Context{ + Format: "raw", + }, + Digest: true, + }, + fmt.Sprintf(`repository: image +tag: tag1 +digest: sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf +image_id: imageID1 +created_at: %s +virtual_size: 0 B + +repository: image +tag: tag2 +digest: +image_id: imageID2 +created_at: %s +virtual_size: 0 B + +repository: +tag: +digest: +image_id: imageID3 +created_at: %s +virtual_size: 0 B + +`, expectedTime, expectedTime, expectedTime), + }, + { + ImageContext{ + Context: Context{ + Format: "raw", + Quiet: true, + }, + }, + `image_id: imageID1 +image_id: imageID2 +image_id: imageID3 +`, + }, + // Custom Format + { + ImageContext{ + Context: Context{ + Format: "{{.Repository}}", + }, + }, + "image\nimage\n\n", + }, + { + ImageContext{ + Context: Context{ + Format: "{{.Repository}}", + }, + Digest: true, + }, + "image\nimage\n\n", + }, + } + + for _, context := range contexts { + images := []types.Image{ + {ID: "imageID1", RepoTags: []string{"image:tag1"}, RepoDigests: []string{"image@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf"}, Created: unixTime}, + {ID: "imageID2", RepoTags: []string{"image:tag2"}, Created: unixTime}, + {ID: "imageID3", RepoTags: []string{":"}, RepoDigests: []string{"@"}, Created: unixTime}, + } + out := bytes.NewBufferString("") + context.context.Output = out + context.context.Images = images + context.context.Write() + actual := out.String() + if actual != context.expected { + t.Fatalf("Expected \n%s, got \n%s", context.expected, actual) + } + // Clean buffer + out.Reset() + } +} + +func TestImageContextWriteWithNoImage(t *testing.T) { + out := bytes.NewBufferString("") + images := []types.Image{} + + contexts := []struct { + context ImageContext + expected string + }{ + { + ImageContext{ + Context: Context{ + Format: "{{.Repository}}", + Output: out, + }, + }, + "", + }, + { + ImageContext{ + Context: Context{ + Format: "table {{.Repository}}", + Output: out, + }, + }, + "REPOSITORY\n", + }, + { + ImageContext{ + Context: Context{ + Format: "{{.Repository}}", + Output: out, + }, + Digest: true, + }, + "", + }, + { + ImageContext{ + Context: Context{ + Format: "table {{.Repository}}", + Output: out, + }, + Digest: true, + }, + "REPOSITORY DIGEST\n", + }, + } + + for _, context := range contexts { + context.context.Images = images + context.context.Write() + actual := out.String() + if actual != context.expected { + t.Fatalf("Expected \n%s, got \n%s", context.expected, actual) + } + // Clean buffer + out.Reset() + } +} diff --git a/components/cli/command/formatter/network.go b/components/cli/command/formatter/network.go new file mode 100644 index 0000000000..6eb820879e --- /dev/null +++ b/components/cli/command/formatter/network.go @@ -0,0 +1,129 @@ +package formatter + +import ( + "bytes" + "fmt" + "strings" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/stringid" +) + +const ( + defaultNetworkTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Driver}}\t{{.Scope}}" + + networkIDHeader = "NETWORK ID" + ipv6Header = "IPV6" + internalHeader = "INTERNAL" +) + +// NetworkContext contains network specific information required by the formatter, +// encapsulate a Context struct. +type NetworkContext struct { + Context + // Networks + Networks []types.NetworkResource +} + +func (ctx NetworkContext) Write() { + switch ctx.Format { + case tableFormatKey: + if ctx.Quiet { + ctx.Format = defaultQuietFormat + } else { + ctx.Format = defaultNetworkTableFormat + } + case rawFormatKey: + if ctx.Quiet { + ctx.Format = `network_id: {{.ID}}` + } else { + ctx.Format = `network_id: {{.ID}}\nname: {{.Name}}\ndriver: {{.Driver}}\nscope: {{.Scope}}\n` + } + } + + ctx.buffer = bytes.NewBufferString("") + ctx.preformat() + + tmpl, err := ctx.parseFormat() + if err != nil { + return + } + + for _, network := range ctx.Networks { + networkCtx := &networkContext{ + trunc: ctx.Trunc, + n: network, + } + err = ctx.contextFormat(tmpl, networkCtx) + if err != nil { + return + } + } + + ctx.postformat(tmpl, &networkContext{}) +} + +type networkContext struct { + baseSubContext + trunc bool + n types.NetworkResource +} + +func (c *networkContext) ID() string { + c.addHeader(networkIDHeader) + if c.trunc { + return stringid.TruncateID(c.n.ID) + } + return c.n.ID +} + +func (c *networkContext) Name() string { + c.addHeader(nameHeader) + return c.n.Name +} + +func (c *networkContext) Driver() string { + c.addHeader(driverHeader) + return c.n.Driver +} + +func (c *networkContext) Scope() string { + c.addHeader(scopeHeader) + return c.n.Scope +} + +func (c *networkContext) IPv6() string { + c.addHeader(ipv6Header) + return fmt.Sprintf("%v", c.n.EnableIPv6) +} + +func (c *networkContext) Internal() string { + c.addHeader(internalHeader) + return fmt.Sprintf("%v", c.n.Internal) +} + +func (c *networkContext) Labels() string { + c.addHeader(labelsHeader) + if c.n.Labels == nil { + return "" + } + + var joinLabels []string + for k, v := range c.n.Labels { + joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v)) + } + return strings.Join(joinLabels, ",") +} + +func (c *networkContext) Label(name string) string { + n := strings.Split(name, ".") + r := strings.NewReplacer("-", " ", "_", " ") + h := r.Replace(n[len(n)-1]) + + c.addHeader(h) + + if c.n.Labels == nil { + return "" + } + return c.n.Labels[name] +} diff --git a/components/cli/command/formatter/network_test.go b/components/cli/command/formatter/network_test.go new file mode 100644 index 0000000000..b5f826af6d --- /dev/null +++ b/components/cli/command/formatter/network_test.go @@ -0,0 +1,201 @@ +package formatter + +import ( + "bytes" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/stringid" +) + +func TestNetworkContext(t *testing.T) { + networkID := stringid.GenerateRandomID() + + var ctx networkContext + cases := []struct { + networkCtx networkContext + expValue string + expHeader string + call func() string + }{ + {networkContext{ + n: types.NetworkResource{ID: networkID}, + trunc: false, + }, networkID, networkIDHeader, ctx.ID}, + {networkContext{ + n: types.NetworkResource{ID: networkID}, + trunc: true, + }, stringid.TruncateID(networkID), networkIDHeader, ctx.ID}, + {networkContext{ + n: types.NetworkResource{Name: "network_name"}, + }, "network_name", nameHeader, ctx.Name}, + {networkContext{ + n: types.NetworkResource{Driver: "driver_name"}, + }, "driver_name", driverHeader, ctx.Driver}, + {networkContext{ + n: types.NetworkResource{EnableIPv6: true}, + }, "true", ipv6Header, ctx.IPv6}, + {networkContext{ + n: types.NetworkResource{EnableIPv6: false}, + }, "false", ipv6Header, ctx.IPv6}, + {networkContext{ + n: types.NetworkResource{Internal: true}, + }, "true", internalHeader, ctx.Internal}, + {networkContext{ + n: types.NetworkResource{Internal: false}, + }, "false", internalHeader, ctx.Internal}, + {networkContext{ + n: types.NetworkResource{}, + }, "", labelsHeader, ctx.Labels}, + {networkContext{ + n: types.NetworkResource{Labels: map[string]string{"label1": "value1", "label2": "value2"}}, + }, "label1=value1,label2=value2", labelsHeader, ctx.Labels}, + } + + for _, c := range cases { + ctx = c.networkCtx + v := c.call() + if strings.Contains(v, ",") { + compareMultipleValues(t, v, c.expValue) + } else if v != c.expValue { + t.Fatalf("Expected %s, was %s\n", c.expValue, v) + } + + h := ctx.fullHeader() + if h != c.expHeader { + t.Fatalf("Expected %s, was %s\n", c.expHeader, h) + } + } +} + +func TestNetworkContextWrite(t *testing.T) { + contexts := []struct { + context NetworkContext + expected string + }{ + + // Errors + { + NetworkContext{ + Context: Context{ + Format: "{{InvalidFunction}}", + }, + }, + `Template parsing error: template: :1: function "InvalidFunction" not defined +`, + }, + { + NetworkContext{ + Context: Context{ + Format: "{{nil}}", + }, + }, + `Template parsing error: template: :1:2: executing "" at : nil is not a command +`, + }, + // Table format + { + NetworkContext{ + Context: Context{ + Format: "table", + }, + }, + `NETWORK ID NAME DRIVER SCOPE +networkID1 foobar_baz foo local +networkID2 foobar_bar bar local +`, + }, + { + NetworkContext{ + Context: Context{ + Format: "table", + Quiet: true, + }, + }, + `networkID1 +networkID2 +`, + }, + { + NetworkContext{ + Context: Context{ + Format: "table {{.Name}}", + }, + }, + `NAME +foobar_baz +foobar_bar +`, + }, + { + NetworkContext{ + Context: Context{ + Format: "table {{.Name}}", + Quiet: true, + }, + }, + `NAME +foobar_baz +foobar_bar +`, + }, + // Raw Format + { + NetworkContext{ + Context: Context{ + Format: "raw", + }, + }, `network_id: networkID1 +name: foobar_baz +driver: foo +scope: local + +network_id: networkID2 +name: foobar_bar +driver: bar +scope: local + +`, + }, + { + NetworkContext{ + Context: Context{ + Format: "raw", + Quiet: true, + }, + }, + `network_id: networkID1 +network_id: networkID2 +`, + }, + // Custom Format + { + NetworkContext{ + Context: Context{ + Format: "{{.Name}}", + }, + }, + `foobar_baz +foobar_bar +`, + }, + } + + for _, context := range contexts { + networks := []types.NetworkResource{ + {ID: "networkID1", Name: "foobar_baz", Driver: "foo", Scope: "local"}, + {ID: "networkID2", Name: "foobar_bar", Driver: "bar", Scope: "local"}, + } + out := bytes.NewBufferString("") + context.context.Output = out + context.context.Networks = networks + context.context.Write() + actual := out.String() + if actual != context.expected { + t.Fatalf("Expected \n%s, got \n%s", context.expected, actual) + } + // Clean buffer + out.Reset() + } +} diff --git a/components/cli/command/formatter/volume.go b/components/cli/command/formatter/volume.go new file mode 100644 index 0000000000..ba24b06a4f --- /dev/null +++ b/components/cli/command/formatter/volume.go @@ -0,0 +1,114 @@ +package formatter + +import ( + "bytes" + "fmt" + "strings" + + "github.com/docker/docker/api/types" +) + +const ( + defaultVolumeQuietFormat = "{{.Name}}" + defaultVolumeTableFormat = "table {{.Driver}}\t{{.Name}}" + + mountpointHeader = "MOUNTPOINT" + // Status header ? +) + +// VolumeContext contains volume specific information required by the formatter, +// encapsulate a Context struct. +type VolumeContext struct { + Context + // Volumes + Volumes []*types.Volume +} + +func (ctx VolumeContext) Write() { + switch ctx.Format { + case tableFormatKey: + if ctx.Quiet { + ctx.Format = defaultVolumeQuietFormat + } else { + ctx.Format = defaultVolumeTableFormat + } + case rawFormatKey: + if ctx.Quiet { + ctx.Format = `name: {{.Name}}` + } else { + ctx.Format = `name: {{.Name}}\ndriver: {{.Driver}}\n` + } + } + + ctx.buffer = bytes.NewBufferString("") + ctx.preformat() + + tmpl, err := ctx.parseFormat() + if err != nil { + return + } + + for _, volume := range ctx.Volumes { + volumeCtx := &volumeContext{ + v: volume, + } + err = ctx.contextFormat(tmpl, volumeCtx) + if err != nil { + return + } + } + + ctx.postformat(tmpl, &networkContext{}) +} + +type volumeContext struct { + baseSubContext + v *types.Volume +} + +func (c *volumeContext) Name() string { + c.addHeader(nameHeader) + return c.v.Name +} + +func (c *volumeContext) Driver() string { + c.addHeader(driverHeader) + return c.v.Driver +} + +func (c *volumeContext) Scope() string { + c.addHeader(scopeHeader) + return c.v.Scope +} + +func (c *volumeContext) Mountpoint() string { + c.addHeader(mountpointHeader) + return c.v.Mountpoint +} + +func (c *volumeContext) Labels() string { + c.addHeader(labelsHeader) + if c.v.Labels == nil { + return "" + } + + var joinLabels []string + for k, v := range c.v.Labels { + joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v)) + } + return strings.Join(joinLabels, ",") +} + +func (c *volumeContext) Label(name string) string { + + n := strings.Split(name, ".") + r := strings.NewReplacer("-", " ", "_", " ") + h := r.Replace(n[len(n)-1]) + + c.addHeader(h) + + if c.v.Labels == nil { + return "" + } + return c.v.Labels[name] +} diff --git a/components/cli/command/formatter/volume_test.go b/components/cli/command/formatter/volume_test.go new file mode 100644 index 0000000000..2295eff3ef --- /dev/null +++ b/components/cli/command/formatter/volume_test.go @@ -0,0 +1,183 @@ +package formatter + +import ( + "bytes" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/stringid" +) + +func TestVolumeContext(t *testing.T) { + volumeName := stringid.GenerateRandomID() + + var ctx volumeContext + cases := []struct { + volumeCtx volumeContext + expValue string + expHeader string + call func() string + }{ + {volumeContext{ + v: &types.Volume{Name: volumeName}, + }, volumeName, nameHeader, ctx.Name}, + {volumeContext{ + v: &types.Volume{Driver: "driver_name"}, + }, "driver_name", driverHeader, ctx.Driver}, + {volumeContext{ + v: &types.Volume{Scope: "local"}, + }, "local", scopeHeader, ctx.Scope}, + {volumeContext{ + v: &types.Volume{Mountpoint: "mountpoint"}, + }, "mountpoint", mountpointHeader, ctx.Mountpoint}, + {volumeContext{ + v: &types.Volume{}, + }, "", labelsHeader, ctx.Labels}, + {volumeContext{ + v: &types.Volume{Labels: map[string]string{"label1": "value1", "label2": "value2"}}, + }, "label1=value1,label2=value2", labelsHeader, ctx.Labels}, + } + + for _, c := range cases { + ctx = c.volumeCtx + v := c.call() + if strings.Contains(v, ",") { + compareMultipleValues(t, v, c.expValue) + } else if v != c.expValue { + t.Fatalf("Expected %s, was %s\n", c.expValue, v) + } + + h := ctx.fullHeader() + if h != c.expHeader { + t.Fatalf("Expected %s, was %s\n", c.expHeader, h) + } + } +} + +func TestVolumeContextWrite(t *testing.T) { + contexts := []struct { + context VolumeContext + expected string + }{ + + // Errors + { + VolumeContext{ + Context: Context{ + Format: "{{InvalidFunction}}", + }, + }, + `Template parsing error: template: :1: function "InvalidFunction" not defined +`, + }, + { + VolumeContext{ + Context: Context{ + Format: "{{nil}}", + }, + }, + `Template parsing error: template: :1:2: executing "" at : nil is not a command +`, + }, + // Table format + { + VolumeContext{ + Context: Context{ + Format: "table", + }, + }, + `DRIVER NAME +foo foobar_baz +bar foobar_bar +`, + }, + { + VolumeContext{ + Context: Context{ + Format: "table", + Quiet: true, + }, + }, + `foobar_baz +foobar_bar +`, + }, + { + VolumeContext{ + Context: Context{ + Format: "table {{.Name}}", + }, + }, + `NAME +foobar_baz +foobar_bar +`, + }, + { + VolumeContext{ + Context: Context{ + Format: "table {{.Name}}", + Quiet: true, + }, + }, + `NAME +foobar_baz +foobar_bar +`, + }, + // Raw Format + { + VolumeContext{ + Context: Context{ + Format: "raw", + }, + }, `name: foobar_baz +driver: foo + +name: foobar_bar +driver: bar + +`, + }, + { + VolumeContext{ + Context: Context{ + Format: "raw", + Quiet: true, + }, + }, + `name: foobar_baz +name: foobar_bar +`, + }, + // Custom Format + { + VolumeContext{ + Context: Context{ + Format: "{{.Name}}", + }, + }, + `foobar_baz +foobar_bar +`, + }, + } + + for _, context := range contexts { + volumes := []*types.Volume{ + {Name: "foobar_baz", Driver: "foo"}, + {Name: "foobar_bar", Driver: "bar"}, + } + out := bytes.NewBufferString("") + context.context.Output = out + context.context.Volumes = volumes + context.context.Write() + actual := out.String() + if actual != context.expected { + t.Fatalf("Expected \n%s, got \n%s", context.expected, actual) + } + // Clean buffer + out.Reset() + } +} diff --git a/components/cli/command/idresolver/idresolver.go b/components/cli/command/idresolver/idresolver.go new file mode 100644 index 0000000000..ad0d96735d --- /dev/null +++ b/components/cli/command/idresolver/idresolver.go @@ -0,0 +1,70 @@ +package idresolver + +import ( + "fmt" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/client" +) + +// IDResolver provides ID to Name resolution. +type IDResolver struct { + client client.APIClient + noResolve bool + cache map[string]string +} + +// New creates a new IDResolver. +func New(client client.APIClient, noResolve bool) *IDResolver { + return &IDResolver{ + client: client, + noResolve: noResolve, + cache: make(map[string]string), + } +} + +func (r *IDResolver) get(ctx context.Context, t interface{}, id string) (string, error) { + switch t.(type) { + case swarm.Node: + node, _, err := r.client.NodeInspectWithRaw(ctx, id) + if err != nil { + return id, nil + } + if node.Spec.Annotations.Name != "" { + return node.Spec.Annotations.Name, nil + } + if node.Description.Hostname != "" { + return node.Description.Hostname, nil + } + return id, nil + case swarm.Service: + service, _, err := r.client.ServiceInspectWithRaw(ctx, id) + if err != nil { + return id, nil + } + return service.Spec.Annotations.Name, nil + default: + return "", fmt.Errorf("unsupported type") + } + +} + +// Resolve will attempt to resolve an ID to a Name by querying the manager. +// Results are stored into a cache. +// If the `-n` flag is used in the command-line, resolution is disabled. +func (r *IDResolver) Resolve(ctx context.Context, t interface{}, id string) (string, error) { + if r.noResolve { + return id, nil + } + if name, ok := r.cache[id]; ok { + return name, nil + } + name, err := r.get(ctx, t, id) + if err != nil { + return "", err + } + r.cache[id] = name + return name, nil +} diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go new file mode 100644 index 0000000000..10ad413f25 --- /dev/null +++ b/components/cli/command/image/build.go @@ -0,0 +1,452 @@ +package image + +import ( + "archive/tar" + "bufio" + "bytes" + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "runtime" + + "golang.org/x/net/context" + + "github.com/docker/docker/api" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/builder" + "github.com/docker/docker/builder/dockerignore" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/opts" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/fileutils" + "github.com/docker/docker/pkg/jsonmessage" + "github.com/docker/docker/pkg/progress" + "github.com/docker/docker/pkg/streamformatter" + "github.com/docker/docker/pkg/urlutil" + "github.com/docker/docker/reference" + runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/docker/go-units" + "github.com/spf13/cobra" +) + +type buildOptions struct { + context string + dockerfileName string + tags opts.ListOpts + labels []string + buildArgs opts.ListOpts + ulimits *runconfigopts.UlimitOpt + memory string + memorySwap string + shmSize string + cpuShares int64 + cpuPeriod int64 + cpuQuota int64 + cpuSetCpus string + cpuSetMems string + cgroupParent string + isolation string + quiet bool + noCache bool + rm bool + forceRm bool + pull bool +} + +// NewBuildCommand creates a new `docker build` command +func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command { + ulimits := make(map[string]*units.Ulimit) + options := buildOptions{ + tags: opts.NewListOpts(validateTag), + buildArgs: opts.NewListOpts(runconfigopts.ValidateEnv), + ulimits: runconfigopts.NewUlimitOpt(&ulimits), + } + + cmd := &cobra.Command{ + Use: "build [OPTIONS] PATH | URL | -", + Short: "Build an image from a Dockerfile", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + options.context = args[0] + return runBuild(dockerCli, options) + }, + } + + flags := cmd.Flags() + + flags.VarP(&options.tags, "tag", "t", "Name and optionally a tag in the 'name:tag' format") + flags.Var(&options.buildArgs, "build-arg", "Set build-time variables") + flags.Var(options.ulimits, "ulimit", "Ulimit options") + flags.StringVarP(&options.dockerfileName, "file", "f", "", "Name of the Dockerfile (Default is 'PATH/Dockerfile')") + flags.StringVarP(&options.memory, "memory", "m", "", "Memory limit") + flags.StringVar(&options.memorySwap, "memory-swap", "", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap") + flags.StringVar(&options.shmSize, "shm-size", "", "Size of /dev/shm, default value is 64MB") + flags.Int64VarP(&options.cpuShares, "cpu-shares", "c", 0, "CPU shares (relative weight)") + flags.Int64Var(&options.cpuPeriod, "cpu-period", 0, "Limit the CPU CFS (Completely Fair Scheduler) period") + flags.Int64Var(&options.cpuQuota, "cpu-quota", 0, "Limit the CPU CFS (Completely Fair Scheduler) quota") + flags.StringVar(&options.cpuSetCpus, "cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)") + flags.StringVar(&options.cpuSetMems, "cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)") + flags.StringVar(&options.cgroupParent, "cgroup-parent", "", "Optional parent cgroup for the container") + flags.StringVar(&options.isolation, "isolation", "", "Container isolation technology") + flags.StringSliceVar(&options.labels, "label", []string{}, "Set metadata for an image") + flags.BoolVar(&options.noCache, "no-cache", false, "Do not use cache when building the image") + flags.BoolVar(&options.rm, "rm", true, "Remove intermediate containers after a successful build") + flags.BoolVar(&options.forceRm, "force-rm", false, "Always remove intermediate containers") + flags.BoolVarP(&options.quiet, "quiet", "q", false, "Suppress the build output and print image ID on success") + flags.BoolVar(&options.pull, "pull", false, "Always attempt to pull a newer version of the image") + + command.AddTrustedFlags(flags, true) + + return cmd +} + +// lastProgressOutput is the same as progress.Output except +// that it only output with the last update. It is used in +// non terminal scenarios to depresss verbose messages +type lastProgressOutput struct { + output progress.Output +} + +// WriteProgress formats progress information from a ProgressReader. +func (out *lastProgressOutput) WriteProgress(prog progress.Progress) error { + if !prog.LastUpdate { + return nil + } + + return out.output.WriteProgress(prog) +} + +func runBuild(dockerCli *command.DockerCli, options buildOptions) error { + + var ( + buildCtx io.ReadCloser + err error + ) + + specifiedContext := options.context + + var ( + contextDir string + tempDir string + relDockerfile string + progBuff io.Writer + buildBuff io.Writer + ) + + progBuff = dockerCli.Out() + buildBuff = dockerCli.Out() + if options.quiet { + progBuff = bytes.NewBuffer(nil) + buildBuff = bytes.NewBuffer(nil) + } + + switch { + case specifiedContext == "-": + buildCtx, relDockerfile, err = builder.GetContextFromReader(dockerCli.In(), options.dockerfileName) + case urlutil.IsGitURL(specifiedContext): + tempDir, relDockerfile, err = builder.GetContextFromGitURL(specifiedContext, options.dockerfileName) + case urlutil.IsURL(specifiedContext): + buildCtx, relDockerfile, err = builder.GetContextFromURL(progBuff, specifiedContext, options.dockerfileName) + default: + contextDir, relDockerfile, err = builder.GetContextFromLocalDir(specifiedContext, options.dockerfileName) + } + + if err != nil { + if options.quiet && urlutil.IsURL(specifiedContext) { + fmt.Fprintln(dockerCli.Err(), progBuff) + } + return fmt.Errorf("unable to prepare context: %s", err) + } + + if tempDir != "" { + defer os.RemoveAll(tempDir) + contextDir = tempDir + } + + if buildCtx == nil { + // And canonicalize dockerfile name to a platform-independent one + relDockerfile, err = archive.CanonicalTarNameForPath(relDockerfile) + if err != nil { + return fmt.Errorf("cannot canonicalize dockerfile path %s: %v", relDockerfile, err) + } + + f, err := os.Open(filepath.Join(contextDir, ".dockerignore")) + if err != nil && !os.IsNotExist(err) { + return err + } + defer f.Close() + + var excludes []string + if err == nil { + excludes, err = dockerignore.ReadAll(f) + if err != nil { + return err + } + } + + if err := builder.ValidateContextDirectory(contextDir, excludes); err != nil { + return fmt.Errorf("Error checking context: '%s'.", err) + } + + // If .dockerignore mentions .dockerignore or the Dockerfile + // then make sure we send both files over to the daemon + // because Dockerfile is, obviously, needed no matter what, and + // .dockerignore is needed to know if either one needs to be + // removed. The daemon will remove them for us, if needed, after it + // parses the Dockerfile. Ignore errors here, as they will have been + // caught by validateContextDirectory above. + var includes = []string{"."} + keepThem1, _ := fileutils.Matches(".dockerignore", excludes) + keepThem2, _ := fileutils.Matches(relDockerfile, excludes) + if keepThem1 || keepThem2 { + includes = append(includes, ".dockerignore", relDockerfile) + } + + buildCtx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{ + Compression: archive.Uncompressed, + ExcludePatterns: excludes, + IncludeFiles: includes, + }) + if err != nil { + return err + } + } + + ctx := context.Background() + + var resolvedTags []*resolvedTag + if command.IsTrusted() { + // Wrap the tar archive to replace the Dockerfile entry with the rewritten + // Dockerfile which uses trusted pulls. + buildCtx = replaceDockerfileTarWrapper(ctx, buildCtx, relDockerfile, dockerCli.TrustedReference, &resolvedTags) + } + + // Setup an upload progress bar + progressOutput := streamformatter.NewStreamFormatter().NewProgressOutput(progBuff, true) + if !dockerCli.Out().IsTerminal() { + progressOutput = &lastProgressOutput{output: progressOutput} + } + + var body io.Reader = progress.NewProgressReader(buildCtx, progressOutput, 0, "", "Sending build context to Docker daemon") + + var memory int64 + if options.memory != "" { + parsedMemory, err := units.RAMInBytes(options.memory) + if err != nil { + return err + } + memory = parsedMemory + } + + var memorySwap int64 + if options.memorySwap != "" { + if options.memorySwap == "-1" { + memorySwap = -1 + } else { + parsedMemorySwap, err := units.RAMInBytes(options.memorySwap) + if err != nil { + return err + } + memorySwap = parsedMemorySwap + } + } + + var shmSize int64 + if options.shmSize != "" { + shmSize, err = units.RAMInBytes(options.shmSize) + if err != nil { + return err + } + } + + buildOptions := types.ImageBuildOptions{ + Memory: memory, + MemorySwap: memorySwap, + Tags: options.tags.GetAll(), + SuppressOutput: options.quiet, + NoCache: options.noCache, + Remove: options.rm, + ForceRemove: options.forceRm, + PullParent: options.pull, + Isolation: container.Isolation(options.isolation), + CPUSetCPUs: options.cpuSetCpus, + CPUSetMems: options.cpuSetMems, + CPUShares: options.cpuShares, + CPUQuota: options.cpuQuota, + CPUPeriod: options.cpuPeriod, + CgroupParent: options.cgroupParent, + Dockerfile: relDockerfile, + ShmSize: shmSize, + Ulimits: options.ulimits.GetList(), + BuildArgs: runconfigopts.ConvertKVStringsToMap(options.buildArgs.GetAll()), + AuthConfigs: dockerCli.RetrieveAuthConfigs(), + Labels: runconfigopts.ConvertKVStringsToMap(options.labels), + } + + response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions) + if err != nil { + return err + } + defer response.Body.Close() + + err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, dockerCli.Out().FD(), dockerCli.Out().IsTerminal(), nil) + if err != nil { + if jerr, ok := err.(*jsonmessage.JSONError); ok { + // If no error code is set, default to 1 + if jerr.Code == 0 { + jerr.Code = 1 + } + if options.quiet { + fmt.Fprintf(dockerCli.Err(), "%s%s", progBuff, buildBuff) + } + return cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code} + } + } + + // Windows: show error message about modified file permissions if the + // daemon isn't running Windows. + if response.OSType != "windows" && runtime.GOOS == "windows" && !options.quiet { + fmt.Fprintln(dockerCli.Err(), `SECURITY WARNING: You are building a Docker image from Windows against a non-Windows Docker host. All files and directories added to build context will have '-rwxr-xr-x' permissions. It is recommended to double check and reset permissions for sensitive files and directories.`) + } + + // Everything worked so if -q was provided the output from the daemon + // should be just the image ID and we'll print that to stdout. + if options.quiet { + fmt.Fprintf(dockerCli.Out(), "%s", buildBuff) + } + + if command.IsTrusted() { + // Since the build was successful, now we must tag any of the resolved + // images from the above Dockerfile rewrite. + for _, resolved := range resolvedTags { + if err := dockerCli.TagTrusted(ctx, resolved.digestRef, resolved.tagRef); err != nil { + return err + } + } + } + + return nil +} + +type translatorFunc func(context.Context, reference.NamedTagged) (reference.Canonical, error) + +// validateTag checks if the given image name can be resolved. +func validateTag(rawRepo string) (string, error) { + _, err := reference.ParseNamed(rawRepo) + if err != nil { + return "", err + } + + return rawRepo, nil +} + +var dockerfileFromLinePattern = regexp.MustCompile(`(?i)^[\s]*FROM[ \f\r\t\v]+(?P[^ \f\r\t\v\n#]+)`) + +// resolvedTag records the repository, tag, and resolved digest reference +// from a Dockerfile rewrite. +type resolvedTag struct { + digestRef reference.Canonical + tagRef reference.NamedTagged +} + +// rewriteDockerfileFrom rewrites the given Dockerfile by resolving images in +// "FROM " instructions to a digest reference. `translator` is a +// function that takes a repository name and tag reference and returns a +// trusted digest reference. +func rewriteDockerfileFrom(ctx context.Context, dockerfile io.Reader, translator translatorFunc) (newDockerfile []byte, resolvedTags []*resolvedTag, err error) { + scanner := bufio.NewScanner(dockerfile) + buf := bytes.NewBuffer(nil) + + // Scan the lines of the Dockerfile, looking for a "FROM" line. + for scanner.Scan() { + line := scanner.Text() + + matches := dockerfileFromLinePattern.FindStringSubmatch(line) + if matches != nil && matches[1] != api.NoBaseImageSpecifier { + // Replace the line with a resolved "FROM repo@digest" + ref, err := reference.ParseNamed(matches[1]) + if err != nil { + return nil, nil, err + } + ref = reference.WithDefaultTag(ref) + if ref, ok := ref.(reference.NamedTagged); ok && command.IsTrusted() { + trustedRef, err := translator(ctx, ref) + if err != nil { + return nil, nil, err + } + + line = dockerfileFromLinePattern.ReplaceAllLiteralString(line, fmt.Sprintf("FROM %s", trustedRef.String())) + resolvedTags = append(resolvedTags, &resolvedTag{ + digestRef: trustedRef, + tagRef: ref, + }) + } + } + + _, err := fmt.Fprintln(buf, line) + if err != nil { + return nil, nil, err + } + } + + return buf.Bytes(), resolvedTags, scanner.Err() +} + +// replaceDockerfileTarWrapper wraps the given input tar archive stream and +// replaces the entry with the given Dockerfile name with the contents of the +// new Dockerfile. Returns a new tar archive stream with the replaced +// Dockerfile. +func replaceDockerfileTarWrapper(ctx context.Context, inputTarStream io.ReadCloser, dockerfileName string, translator translatorFunc, resolvedTags *[]*resolvedTag) io.ReadCloser { + pipeReader, pipeWriter := io.Pipe() + go func() { + tarReader := tar.NewReader(inputTarStream) + tarWriter := tar.NewWriter(pipeWriter) + + defer inputTarStream.Close() + + for { + hdr, err := tarReader.Next() + if err == io.EOF { + // Signals end of archive. + tarWriter.Close() + pipeWriter.Close() + return + } + if err != nil { + pipeWriter.CloseWithError(err) + return + } + + content := io.Reader(tarReader) + if hdr.Name == dockerfileName { + // This entry is the Dockerfile. Since the tar archive was + // generated from a directory on the local filesystem, the + // Dockerfile will only appear once in the archive. + var newDockerfile []byte + newDockerfile, *resolvedTags, err = rewriteDockerfileFrom(ctx, content, translator) + if err != nil { + pipeWriter.CloseWithError(err) + return + } + hdr.Size = int64(len(newDockerfile)) + content = bytes.NewBuffer(newDockerfile) + } + + if err := tarWriter.WriteHeader(hdr); err != nil { + pipeWriter.CloseWithError(err) + return + } + + if _, err := io.Copy(tarWriter, content); err != nil { + pipeWriter.CloseWithError(err) + return + } + } + }() + + return pipeReader +} diff --git a/components/cli/command/image/history.go b/components/cli/command/image/history.go new file mode 100644 index 0000000000..a75403a45f --- /dev/null +++ b/components/cli/command/image/history.go @@ -0,0 +1,99 @@ +package image + +import ( + "fmt" + "strconv" + "strings" + "text/tabwriter" + "time" + + "golang.org/x/net/context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/stringutils" + "github.com/docker/go-units" + "github.com/spf13/cobra" +) + +type historyOptions struct { + image string + + human bool + quiet bool + noTrunc bool +} + +// NewHistoryCommand creates a new `docker history` command +func NewHistoryCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts historyOptions + + cmd := &cobra.Command{ + Use: "history [OPTIONS] IMAGE", + Short: "Show the history of an image", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.image = args[0] + return runHistory(dockerCli, opts) + }, + } + + flags := cmd.Flags() + + flags.BoolVarP(&opts.human, "human", "H", true, "Print sizes and dates in human readable format") + flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only show numeric IDs") + flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output") + + return cmd +} + +func runHistory(dockerCli *command.DockerCli, opts historyOptions) error { + ctx := context.Background() + + history, err := dockerCli.Client().ImageHistory(ctx, opts.image) + if err != nil { + return err + } + + w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0) + + if opts.quiet { + for _, entry := range history { + if opts.noTrunc { + fmt.Fprintf(w, "%s\n", entry.ID) + } else { + fmt.Fprintf(w, "%s\n", stringid.TruncateID(entry.ID)) + } + } + w.Flush() + return nil + } + + var imageID string + var createdBy string + var created string + var size string + + fmt.Fprintln(w, "IMAGE\tCREATED\tCREATED BY\tSIZE\tCOMMENT") + for _, entry := range history { + imageID = entry.ID + createdBy = strings.Replace(entry.CreatedBy, "\t", " ", -1) + if !opts.noTrunc { + createdBy = stringutils.Ellipsis(createdBy, 45) + imageID = stringid.TruncateID(entry.ID) + } + + if opts.human { + created = units.HumanDuration(time.Now().UTC().Sub(time.Unix(entry.Created, 0))) + " ago" + size = units.HumanSize(float64(entry.Size)) + } else { + created = time.Unix(entry.Created, 0).Format(time.RFC3339) + size = strconv.FormatInt(entry.Size, 10) + } + + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", imageID, created, createdBy, size, entry.Comment) + } + w.Flush() + return nil +} diff --git a/components/cli/command/image/images.go b/components/cli/command/image/images.go new file mode 100644 index 0000000000..f00fecf672 --- /dev/null +++ b/components/cli/command/image/images.go @@ -0,0 +1,103 @@ +package image + +import ( + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/formatter" + "github.com/spf13/cobra" +) + +type imagesOptions struct { + matchName string + + quiet bool + all bool + noTrunc bool + showDigests bool + format string + filter []string +} + +// NewImagesCommand creates a new `docker images` command +func NewImagesCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts imagesOptions + + cmd := &cobra.Command{ + Use: "images [OPTIONS] [REPOSITORY[:TAG]]", + Short: "List images", + Args: cli.RequiresMaxArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + opts.matchName = args[0] + } + return runImages(dockerCli, opts) + }, + } + + flags := cmd.Flags() + + flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only show numeric IDs") + flags.BoolVarP(&opts.all, "all", "a", false, "Show all images (default hides intermediate images)") + flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output") + flags.BoolVar(&opts.showDigests, "digests", false, "Show digests") + flags.StringVar(&opts.format, "format", "", "Pretty-print images using a Go template") + flags.StringSliceVarP(&opts.filter, "filter", "f", []string{}, "Filter output based on conditions provided") + + return cmd +} + +func runImages(dockerCli *command.DockerCli, opts imagesOptions) error { + ctx := context.Background() + + // Consolidate all filter flags, and sanity check them early. + // They'll get process in the daemon/server. + imageFilterArgs := filters.NewArgs() + for _, f := range opts.filter { + var err error + imageFilterArgs, err = filters.ParseFlag(f, imageFilterArgs) + if err != nil { + return err + } + } + + matchName := opts.matchName + + options := types.ImageListOptions{ + MatchName: matchName, + All: opts.all, + Filters: imageFilterArgs, + } + + images, err := dockerCli.Client().ImageList(ctx, options) + if err != nil { + return err + } + + f := opts.format + if len(f) == 0 { + if len(dockerCli.ConfigFile().ImagesFormat) > 0 && !opts.quiet { + f = dockerCli.ConfigFile().ImagesFormat + } else { + f = "table" + } + } + + imagesCtx := formatter.ImageContext{ + Context: formatter.Context{ + Output: dockerCli.Out(), + Format: f, + Quiet: opts.quiet, + Trunc: !opts.noTrunc, + }, + Digest: opts.showDigests, + Images: images, + } + + imagesCtx.Write() + + return nil +} diff --git a/components/cli/command/image/import.go b/components/cli/command/image/import.go new file mode 100644 index 0000000000..60024fb53c --- /dev/null +++ b/components/cli/command/image/import.go @@ -0,0 +1,88 @@ +package image + +import ( + "io" + "os" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + dockeropts "github.com/docker/docker/opts" + "github.com/docker/docker/pkg/jsonmessage" + "github.com/docker/docker/pkg/urlutil" + "github.com/spf13/cobra" +) + +type importOptions struct { + source string + reference string + changes dockeropts.ListOpts + message string +} + +// NewImportCommand creates a new `docker import` command +func NewImportCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts importOptions + + cmd := &cobra.Command{ + Use: "import [OPTIONS] file|URL|- [REPOSITORY[:TAG]]", + Short: "Import the contents from a tarball to create a filesystem image", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.source = args[0] + if len(args) > 1 { + opts.reference = args[1] + } + return runImport(dockerCli, opts) + }, + } + + flags := cmd.Flags() + + opts.changes = dockeropts.NewListOpts(nil) + flags.VarP(&opts.changes, "change", "c", "Apply Dockerfile instruction to the created image") + flags.StringVarP(&opts.message, "message", "m", "", "Set commit message for imported image") + + return cmd +} + +func runImport(dockerCli *command.DockerCli, opts importOptions) error { + var ( + in io.Reader + srcName = opts.source + ) + + if opts.source == "-" { + in = dockerCli.In() + } else if !urlutil.IsURL(opts.source) { + srcName = "-" + file, err := os.Open(opts.source) + if err != nil { + return err + } + defer file.Close() + in = file + } + + source := types.ImageImportSource{ + Source: in, + SourceName: srcName, + } + + options := types.ImageImportOptions{ + Message: opts.message, + Changes: opts.changes.GetAll(), + } + + clnt := dockerCli.Client() + + responseBody, err := clnt.ImageImport(context.Background(), source, opts.reference, options) + if err != nil { + return err + } + defer responseBody.Close() + + return jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil) +} diff --git a/components/cli/command/image/load.go b/components/cli/command/image/load.go new file mode 100644 index 0000000000..56145a8a34 --- /dev/null +++ b/components/cli/command/image/load.go @@ -0,0 +1,67 @@ +package image + +import ( + "io" + "os" + + "golang.org/x/net/context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/pkg/jsonmessage" + "github.com/spf13/cobra" +) + +type loadOptions struct { + input string + quiet bool +} + +// NewLoadCommand creates a new `docker load` command +func NewLoadCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts loadOptions + + cmd := &cobra.Command{ + Use: "load [OPTIONS]", + Short: "Load an image from a tar archive or STDIN", + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runLoad(dockerCli, opts) + }, + } + + flags := cmd.Flags() + + flags.StringVarP(&opts.input, "input", "i", "", "Read from tar archive file, instead of STDIN") + flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress the load output") + + return cmd +} + +func runLoad(dockerCli *command.DockerCli, opts loadOptions) error { + + var input io.Reader = dockerCli.In() + if opts.input != "" { + file, err := os.Open(opts.input) + if err != nil { + return err + } + defer file.Close() + input = file + } + if !dockerCli.Out().IsTerminal() { + opts.quiet = true + } + response, err := dockerCli.Client().ImageLoad(context.Background(), input, opts.quiet) + if err != nil { + return err + } + defer response.Body.Close() + + if response.Body != nil && response.JSON { + return jsonmessage.DisplayJSONMessagesToStream(response.Body, dockerCli.Out(), nil) + } + + _, err = io.Copy(dockerCli.Out(), response.Body) + return err +} diff --git a/components/cli/command/image/pull.go b/components/cli/command/image/pull.go new file mode 100644 index 0000000000..88ccb47342 --- /dev/null +++ b/components/cli/command/image/pull.go @@ -0,0 +1,93 @@ +package image + +import ( + "errors" + "fmt" + "strings" + + "golang.org/x/net/context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/reference" + "github.com/docker/docker/registry" + "github.com/spf13/cobra" +) + +type pullOptions struct { + remote string + all bool +} + +// NewPullCommand creates a new `docker pull` command +func NewPullCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts pullOptions + + cmd := &cobra.Command{ + Use: "pull [OPTIONS] NAME[:TAG|@DIGEST]", + Short: "Pull an image or a repository from a registry", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.remote = args[0] + return runPull(dockerCli, opts) + }, + } + + flags := cmd.Flags() + + flags.BoolVarP(&opts.all, "all-tags", "a", false, "Download all tagged images in the repository") + command.AddTrustedFlags(flags, true) + + return cmd +} + +func runPull(dockerCli *command.DockerCli, opts pullOptions) error { + distributionRef, err := reference.ParseNamed(opts.remote) + if err != nil { + return err + } + if opts.all && !reference.IsNameOnly(distributionRef) { + return errors.New("tag can't be used with --all-tags/-a") + } + + if !opts.all && reference.IsNameOnly(distributionRef) { + distributionRef = reference.WithDefaultTag(distributionRef) + fmt.Fprintf(dockerCli.Out(), "Using default tag: %s\n", reference.DefaultTag) + } + + var tag string + switch x := distributionRef.(type) { + case reference.Canonical: + tag = x.Digest().String() + case reference.NamedTagged: + tag = x.Tag() + } + + registryRef := registry.ParseReference(tag) + + // Resolve the Repository name from fqn to RepositoryInfo + repoInfo, err := registry.ParseRepositoryInfo(distributionRef) + if err != nil { + return err + } + + ctx := context.Background() + + authConfig := dockerCli.ResolveAuthConfig(ctx, repoInfo.Index) + requestPrivilege := dockerCli.RegistryAuthenticationPrivilegedFunc(repoInfo.Index, "pull") + + if command.IsTrusted() && !registryRef.HasDigest() { + // Check if tag is digest + err = dockerCli.TrustedPull(ctx, repoInfo, registryRef, authConfig, requestPrivilege) + } else { + err = dockerCli.ImagePullPrivileged(ctx, authConfig, distributionRef.String(), requestPrivilege, opts.all) + } + if err != nil { + if strings.Contains(err.Error(), "target is a plugin") { + return errors.New(err.Error() + " - Use `docker plugin install`") + } + return err + } + + return nil +} diff --git a/components/cli/command/image/push.go b/components/cli/command/image/push.go new file mode 100644 index 0000000000..62b637f6ee --- /dev/null +++ b/components/cli/command/image/push.go @@ -0,0 +1,61 @@ +package image + +import ( + "golang.org/x/net/context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/pkg/jsonmessage" + "github.com/docker/docker/reference" + "github.com/docker/docker/registry" + "github.com/spf13/cobra" +) + +// NewPushCommand creates a new `docker push` command +func NewPushCommand(dockerCli *command.DockerCli) *cobra.Command { + cmd := &cobra.Command{ + Use: "push [OPTIONS] NAME[:TAG]", + Short: "Push an image or a repository to a registry", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runPush(dockerCli, args[0]) + }, + } + + flags := cmd.Flags() + + command.AddTrustedFlags(flags, true) + + return cmd +} + +func runPush(dockerCli *command.DockerCli, remote string) error { + ref, err := reference.ParseNamed(remote) + if err != nil { + return err + } + + // Resolve the Repository name from fqn to RepositoryInfo + repoInfo, err := registry.ParseRepositoryInfo(ref) + if err != nil { + return err + } + + ctx := context.Background() + + // Resolve the Auth config relevant for this server + authConfig := dockerCli.ResolveAuthConfig(ctx, repoInfo.Index) + requestPrivilege := dockerCli.RegistryAuthenticationPrivilegedFunc(repoInfo.Index, "push") + + if command.IsTrusted() { + return dockerCli.TrustedPush(ctx, repoInfo, ref, authConfig, requestPrivilege) + } + + responseBody, err := dockerCli.ImagePushPrivileged(ctx, authConfig, ref.String(), requestPrivilege) + if err != nil { + return err + } + + defer responseBody.Close() + return jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil) +} diff --git a/components/cli/command/image/remove.go b/components/cli/command/image/remove.go new file mode 100644 index 0000000000..51a7b21642 --- /dev/null +++ b/components/cli/command/image/remove.go @@ -0,0 +1,70 @@ +package image + +import ( + "fmt" + "strings" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +type removeOptions struct { + force bool + noPrune bool +} + +// NewRemoveCommand creates a new `docker remove` command +func NewRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts removeOptions + + cmd := &cobra.Command{ + Use: "rmi [OPTIONS] IMAGE [IMAGE...]", + Short: "Remove one or more images", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runRemove(dockerCli, opts, args) + }, + } + + flags := cmd.Flags() + + flags.BoolVarP(&opts.force, "force", "f", false, "Force removal of the image") + flags.BoolVar(&opts.noPrune, "no-prune", false, "Do not delete untagged parents") + + return cmd +} + +func runRemove(dockerCli *command.DockerCli, opts removeOptions, images []string) error { + client := dockerCli.Client() + ctx := context.Background() + + options := types.ImageRemoveOptions{ + Force: opts.force, + PruneChildren: !opts.noPrune, + } + + var errs []string + for _, image := range images { + dels, err := client.ImageRemove(ctx, image, options) + if err != nil { + errs = append(errs, err.Error()) + } else { + for _, del := range dels { + if del.Deleted != "" { + fmt.Fprintf(dockerCli.Out(), "Deleted: %s\n", del.Deleted) + } else { + fmt.Fprintf(dockerCli.Out(), "Untagged: %s\n", del.Untagged) + } + } + } + } + + if len(errs) > 0 { + return fmt.Errorf("%s", strings.Join(errs, "\n")) + } + return nil +} diff --git a/components/cli/command/image/save.go b/components/cli/command/image/save.go new file mode 100644 index 0000000000..bbe82d2a05 --- /dev/null +++ b/components/cli/command/image/save.go @@ -0,0 +1,57 @@ +package image + +import ( + "errors" + "io" + + "golang.org/x/net/context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +type saveOptions struct { + images []string + output string +} + +// NewSaveCommand creates a new `docker save` command +func NewSaveCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts saveOptions + + cmd := &cobra.Command{ + Use: "save [OPTIONS] IMAGE [IMAGE...]", + Short: "Save one or more images to a tar archive (streamed to STDOUT by default)", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.images = args + return runSave(dockerCli, opts) + }, + } + + flags := cmd.Flags() + + flags.StringVarP(&opts.output, "output", "o", "", "Write to a file, instead of STDOUT") + + return cmd +} + +func runSave(dockerCli *command.DockerCli, opts saveOptions) error { + if opts.output == "" && dockerCli.Out().IsTerminal() { + return errors.New("Cowardly refusing to save to a terminal. Use the -o flag or redirect.") + } + + responseBody, err := dockerCli.Client().ImageSave(context.Background(), opts.images) + if err != nil { + return err + } + defer responseBody.Close() + + if opts.output == "" { + _, err := io.Copy(dockerCli.Out(), responseBody) + return err + } + + return command.CopyToFile(opts.output, responseBody) +} diff --git a/components/cli/command/image/search.go b/components/cli/command/image/search.go new file mode 100644 index 0000000000..7c4ad03b96 --- /dev/null +++ b/components/cli/command/image/search.go @@ -0,0 +1,135 @@ +package image + +import ( + "fmt" + "sort" + "strings" + "text/tabwriter" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + registrytypes "github.com/docker/docker/api/types/registry" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/pkg/stringutils" + "github.com/docker/docker/registry" + "github.com/spf13/cobra" +) + +type searchOptions struct { + term string + noTrunc bool + limit int + filter []string + + // Deprecated + stars uint + automated bool +} + +// NewSearchCommand creates a new `docker search` command +func NewSearchCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts searchOptions + + cmd := &cobra.Command{ + Use: "search [OPTIONS] TERM", + Short: "Search the Docker Hub for images", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.term = args[0] + return runSearch(dockerCli, opts) + }, + } + + flags := cmd.Flags() + + flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output") + flags.StringSliceVarP(&opts.filter, "filter", "f", []string{}, "Filter output based on conditions provided") + flags.IntVar(&opts.limit, "limit", registry.DefaultSearchLimit, "Max number of search results") + + flags.BoolVar(&opts.automated, "automated", false, "Only show automated builds") + flags.UintVarP(&opts.stars, "stars", "s", 0, "Only displays with at least x stars") + + flags.MarkDeprecated("automated", "use --filter=automated=true instead") + flags.MarkDeprecated("stars", "use --filter=stars=3 instead") + + return cmd +} + +func runSearch(dockerCli *command.DockerCli, opts searchOptions) error { + indexInfo, err := registry.ParseSearchIndexInfo(opts.term) + if err != nil { + return err + } + + ctx := context.Background() + + authConfig := dockerCli.ResolveAuthConfig(ctx, indexInfo) + requestPrivilege := dockerCli.RegistryAuthenticationPrivilegedFunc(indexInfo, "search") + + encodedAuth, err := command.EncodeAuthToBase64(authConfig) + if err != nil { + return err + } + + searchFilters := filters.NewArgs() + for _, f := range opts.filter { + var err error + searchFilters, err = filters.ParseFlag(f, searchFilters) + if err != nil { + return err + } + } + + options := types.ImageSearchOptions{ + RegistryAuth: encodedAuth, + PrivilegeFunc: requestPrivilege, + Filters: searchFilters, + Limit: opts.limit, + } + + clnt := dockerCli.Client() + + unorderedResults, err := clnt.ImageSearch(ctx, opts.term, options) + if err != nil { + return err + } + + results := searchResultsByStars(unorderedResults) + sort.Sort(results) + + w := tabwriter.NewWriter(dockerCli.Out(), 10, 1, 3, ' ', 0) + fmt.Fprintf(w, "NAME\tDESCRIPTION\tSTARS\tOFFICIAL\tAUTOMATED\n") + for _, res := range results { + // --automated and -s, --stars are deprecated since Docker 1.12 + if (opts.automated && !res.IsAutomated) || (int(opts.stars) > res.StarCount) { + continue + } + desc := strings.Replace(res.Description, "\n", " ", -1) + desc = strings.Replace(desc, "\r", " ", -1) + if !opts.noTrunc { + desc = stringutils.Ellipsis(desc, 45) + } + fmt.Fprintf(w, "%s\t%s\t%d\t", res.Name, desc, res.StarCount) + if res.IsOfficial { + fmt.Fprint(w, "[OK]") + + } + fmt.Fprint(w, "\t") + if res.IsAutomated { + fmt.Fprint(w, "[OK]") + } + fmt.Fprint(w, "\n") + } + w.Flush() + return nil +} + +// SearchResultsByStars sorts search results in descending order by number of stars. +type searchResultsByStars []registrytypes.SearchResult + +func (r searchResultsByStars) Len() int { return len(r) } +func (r searchResultsByStars) Swap(i, j int) { r[i], r[j] = r[j], r[i] } +func (r searchResultsByStars) Less(i, j int) bool { return r[j].StarCount < r[i].StarCount } diff --git a/components/cli/command/image/tag.go b/components/cli/command/image/tag.go new file mode 100644 index 0000000000..b88789b0f8 --- /dev/null +++ b/components/cli/command/image/tag.go @@ -0,0 +1,41 @@ +package image + +import ( + "golang.org/x/net/context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +type tagOptions struct { + image string + name string +} + +// NewTagCommand creates a new `docker tag` command +func NewTagCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts tagOptions + + cmd := &cobra.Command{ + Use: "tag IMAGE[:TAG] IMAGE[:TAG]", + Short: "Tag an image into a repository", + Args: cli.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + opts.image = args[0] + opts.name = args[1] + return runTag(dockerCli, opts) + }, + } + + flags := cmd.Flags() + flags.SetInterspersed(false) + + return cmd +} + +func runTag(dockerCli *command.DockerCli, opts tagOptions) error { + ctx := context.Background() + + return dockerCli.Client().ImageTag(ctx, opts.image, opts.name) +} diff --git a/components/cli/command/in.go b/components/cli/command/in.go new file mode 100644 index 0000000000..c3ed70dc12 --- /dev/null +++ b/components/cli/command/in.go @@ -0,0 +1,75 @@ +package command + +import ( + "errors" + "io" + "os" + "runtime" + + "github.com/docker/docker/pkg/term" +) + +// InStream is an input stream used by the DockerCli to read user input +type InStream struct { + in io.ReadCloser + fd uintptr + isTerminal bool + state *term.State +} + +func (i *InStream) Read(p []byte) (int, error) { + return i.in.Read(p) +} + +// Close implements the Closer interface +func (i *InStream) Close() error { + return i.in.Close() +} + +// FD returns the file descriptor number for this stream +func (i *InStream) FD() uintptr { + return i.fd +} + +// IsTerminal returns true if this stream is connected to a terminal +func (i *InStream) IsTerminal() bool { + return i.isTerminal +} + +// SetRawTerminal sets raw mode on the input terminal +func (i *InStream) SetRawTerminal() (err error) { + if os.Getenv("NORAW") != "" || !i.isTerminal { + return nil + } + i.state, err = term.SetRawTerminal(i.fd) + return err +} + +// RestoreTerminal restores normal mode to the terminal +func (i *InStream) RestoreTerminal() { + if i.state != nil { + term.RestoreTerminal(i.fd, i.state) + } +} + +// CheckTty checks if we are trying to attach to a container tty +// from a non-tty client input stream, and if so, returns an error. +func (i *InStream) CheckTty(attachStdin, ttyMode bool) error { + // In order to attach to a container tty, input stream for the client must + // be a tty itself: redirecting or piping the client standard input is + // incompatible with `docker run -t`, `docker exec -t` or `docker attach`. + if ttyMode && attachStdin && !i.isTerminal { + eText := "the input device is not a TTY" + if runtime.GOOS == "windows" { + return errors.New(eText + ". If you are using mintty, try prefixing the command with 'winpty'") + } + return errors.New(eText) + } + return nil +} + +// NewInStream returns a new OutStream object from a Writer +func NewInStream(in io.ReadCloser) *InStream { + fd, isTerminal := term.GetFdInfo(in) + return &InStream{in: in, fd: fd, isTerminal: isTerminal} +} diff --git a/components/cli/command/inspect/inspector.go b/components/cli/command/inspect/inspector.go new file mode 100644 index 0000000000..b0537e8464 --- /dev/null +++ b/components/cli/command/inspect/inspector.go @@ -0,0 +1,195 @@ +package inspect + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "text/template" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/cli" + "github.com/docker/docker/utils/templates" +) + +// Inspector defines an interface to implement to process elements +type Inspector interface { + Inspect(typedElement interface{}, rawElement []byte) error + Flush() error +} + +// TemplateInspector uses a text template to inspect elements. +type TemplateInspector struct { + outputStream io.Writer + buffer *bytes.Buffer + tmpl *template.Template +} + +// NewTemplateInspector creates a new inspector with a template. +func NewTemplateInspector(outputStream io.Writer, tmpl *template.Template) Inspector { + return &TemplateInspector{ + outputStream: outputStream, + buffer: new(bytes.Buffer), + tmpl: tmpl, + } +} + +// NewTemplateInspectorFromString creates a new TemplateInspector from a string +// which is compiled into a template. +func NewTemplateInspectorFromString(out io.Writer, tmplStr string) (Inspector, error) { + if tmplStr == "" { + return NewIndentedInspector(out), nil + } + + tmpl, err := templates.Parse(tmplStr) + if err != nil { + return nil, fmt.Errorf("Template parsing error: %s", err) + } + return NewTemplateInspector(out, tmpl), nil +} + +// GetRefFunc is a function which used by Inspect to fetch an object from a +// reference +type GetRefFunc func(ref string) (interface{}, []byte, error) + +// Inspect fetches objects by reference using GetRefFunc and writes the json +// representation to the output writer. +func Inspect(out io.Writer, references []string, tmplStr string, getRef GetRefFunc) error { + inspector, err := NewTemplateInspectorFromString(out, tmplStr) + if err != nil { + return cli.StatusError{StatusCode: 64, Status: err.Error()} + } + + var inspectErr error + for _, ref := range references { + element, raw, err := getRef(ref) + if err != nil { + inspectErr = err + break + } + + if err := inspector.Inspect(element, raw); err != nil { + inspectErr = err + break + } + } + + if err := inspector.Flush(); err != nil { + logrus.Errorf("%s\n", err) + } + + if inspectErr != nil { + return cli.StatusError{StatusCode: 1, Status: inspectErr.Error()} + } + return nil +} + +// Inspect executes the inspect template. +// It decodes the raw element into a map if the initial execution fails. +// This allows docker cli to parse inspect structs injected with Swarm fields. +func (i *TemplateInspector) Inspect(typedElement interface{}, rawElement []byte) error { + buffer := new(bytes.Buffer) + if err := i.tmpl.Execute(buffer, typedElement); err != nil { + if rawElement == nil { + return fmt.Errorf("Template parsing error: %v", err) + } + return i.tryRawInspectFallback(rawElement) + } + i.buffer.Write(buffer.Bytes()) + i.buffer.WriteByte('\n') + return nil +} + +// tryRawInspectFallback executes the inspect template with a raw interface. +// This allows docker cli to parse inspect structs injected with Swarm fields. +func (i *TemplateInspector) tryRawInspectFallback(rawElement []byte) error { + var raw interface{} + buffer := new(bytes.Buffer) + rdr := bytes.NewReader(rawElement) + dec := json.NewDecoder(rdr) + + if rawErr := dec.Decode(&raw); rawErr != nil { + return fmt.Errorf("unable to read inspect data: %v", rawErr) + } + + tmplMissingKey := i.tmpl.Option("missingkey=error") + if rawErr := tmplMissingKey.Execute(buffer, raw); rawErr != nil { + return fmt.Errorf("Template parsing error: %v", rawErr) + } + + i.buffer.Write(buffer.Bytes()) + i.buffer.WriteByte('\n') + return nil +} + +// Flush write the result of inspecting all elements into the output stream. +func (i *TemplateInspector) Flush() error { + if i.buffer.Len() == 0 { + _, err := io.WriteString(i.outputStream, "\n") + return err + } + _, err := io.Copy(i.outputStream, i.buffer) + return err +} + +// IndentedInspector uses a buffer to stop the indented representation of an element. +type IndentedInspector struct { + outputStream io.Writer + elements []interface{} + rawElements [][]byte +} + +// NewIndentedInspector generates a new IndentedInspector. +func NewIndentedInspector(outputStream io.Writer) Inspector { + return &IndentedInspector{ + outputStream: outputStream, + } +} + +// Inspect writes the raw element with an indented json format. +func (i *IndentedInspector) Inspect(typedElement interface{}, rawElement []byte) error { + if rawElement != nil { + i.rawElements = append(i.rawElements, rawElement) + } else { + i.elements = append(i.elements, typedElement) + } + return nil +} + +// Flush write the result of inspecting all elements into the output stream. +func (i *IndentedInspector) Flush() error { + if len(i.elements) == 0 && len(i.rawElements) == 0 { + _, err := io.WriteString(i.outputStream, "[]\n") + return err + } + + var buffer io.Reader + if len(i.rawElements) > 0 { + bytesBuffer := new(bytes.Buffer) + bytesBuffer.WriteString("[") + for idx, r := range i.rawElements { + bytesBuffer.Write(r) + if idx < len(i.rawElements)-1 { + bytesBuffer.WriteString(",") + } + } + bytesBuffer.WriteString("]") + indented := new(bytes.Buffer) + if err := json.Indent(indented, bytesBuffer.Bytes(), "", " "); err != nil { + return err + } + buffer = indented + } else { + b, err := json.MarshalIndent(i.elements, "", " ") + if err != nil { + return err + } + buffer = bytes.NewReader(b) + } + + if _, err := io.Copy(i.outputStream, buffer); err != nil { + return err + } + _, err := io.WriteString(i.outputStream, "\n") + return err +} diff --git a/components/cli/command/inspect/inspector_test.go b/components/cli/command/inspect/inspector_test.go new file mode 100644 index 0000000000..1ce1593ab7 --- /dev/null +++ b/components/cli/command/inspect/inspector_test.go @@ -0,0 +1,221 @@ +package inspect + +import ( + "bytes" + "strings" + "testing" + + "github.com/docker/docker/utils/templates" +) + +type testElement struct { + DNS string `json:"Dns"` +} + +func TestTemplateInspectorDefault(t *testing.T) { + b := new(bytes.Buffer) + tmpl, err := templates.Parse("{{.DNS}}") + if err != nil { + t.Fatal(err) + } + i := NewTemplateInspector(b, tmpl) + if err := i.Inspect(testElement{"0.0.0.0"}, nil); err != nil { + t.Fatal(err) + } + + if err := i.Flush(); err != nil { + t.Fatal(err) + } + if b.String() != "0.0.0.0\n" { + t.Fatalf("Expected `0.0.0.0\\n`, got `%s`", b.String()) + } +} + +func TestTemplateInspectorEmpty(t *testing.T) { + b := new(bytes.Buffer) + tmpl, err := templates.Parse("{{.DNS}}") + if err != nil { + t.Fatal(err) + } + i := NewTemplateInspector(b, tmpl) + + if err := i.Flush(); err != nil { + t.Fatal(err) + } + if b.String() != "\n" { + t.Fatalf("Expected `\\n`, got `%s`", b.String()) + } +} + +func TestTemplateInspectorTemplateError(t *testing.T) { + b := new(bytes.Buffer) + tmpl, err := templates.Parse("{{.Foo}}") + if err != nil { + t.Fatal(err) + } + i := NewTemplateInspector(b, tmpl) + + err = i.Inspect(testElement{"0.0.0.0"}, nil) + if err == nil { + t.Fatal("Expected error got nil") + } + + if !strings.HasPrefix(err.Error(), "Template parsing error") { + t.Fatalf("Expected template error, got %v", err) + } +} + +func TestTemplateInspectorRawFallback(t *testing.T) { + b := new(bytes.Buffer) + tmpl, err := templates.Parse("{{.Dns}}") + if err != nil { + t.Fatal(err) + } + i := NewTemplateInspector(b, tmpl) + if err := i.Inspect(testElement{"0.0.0.0"}, []byte(`{"Dns": "0.0.0.0"}`)); err != nil { + t.Fatal(err) + } + + if err := i.Flush(); err != nil { + t.Fatal(err) + } + if b.String() != "0.0.0.0\n" { + t.Fatalf("Expected `0.0.0.0\\n`, got `%s`", b.String()) + } +} + +func TestTemplateInspectorRawFallbackError(t *testing.T) { + b := new(bytes.Buffer) + tmpl, err := templates.Parse("{{.Dns}}") + if err != nil { + t.Fatal(err) + } + i := NewTemplateInspector(b, tmpl) + err = i.Inspect(testElement{"0.0.0.0"}, []byte(`{"Foo": "0.0.0.0"}`)) + if err == nil { + t.Fatal("Expected error got nil") + } + + if !strings.HasPrefix(err.Error(), "Template parsing error") { + t.Fatalf("Expected template error, got %v", err) + } +} + +func TestTemplateInspectorMultiple(t *testing.T) { + b := new(bytes.Buffer) + tmpl, err := templates.Parse("{{.DNS}}") + if err != nil { + t.Fatal(err) + } + i := NewTemplateInspector(b, tmpl) + + if err := i.Inspect(testElement{"0.0.0.0"}, nil); err != nil { + t.Fatal(err) + } + if err := i.Inspect(testElement{"1.1.1.1"}, nil); err != nil { + t.Fatal(err) + } + + if err := i.Flush(); err != nil { + t.Fatal(err) + } + if b.String() != "0.0.0.0\n1.1.1.1\n" { + t.Fatalf("Expected `0.0.0.0\\n1.1.1.1\\n`, got `%s`", b.String()) + } +} + +func TestIndentedInspectorDefault(t *testing.T) { + b := new(bytes.Buffer) + i := NewIndentedInspector(b) + if err := i.Inspect(testElement{"0.0.0.0"}, nil); err != nil { + t.Fatal(err) + } + + if err := i.Flush(); err != nil { + t.Fatal(err) + } + + expected := `[ + { + "Dns": "0.0.0.0" + } +] +` + if b.String() != expected { + t.Fatalf("Expected `%s`, got `%s`", expected, b.String()) + } +} + +func TestIndentedInspectorMultiple(t *testing.T) { + b := new(bytes.Buffer) + i := NewIndentedInspector(b) + if err := i.Inspect(testElement{"0.0.0.0"}, nil); err != nil { + t.Fatal(err) + } + + if err := i.Inspect(testElement{"1.1.1.1"}, nil); err != nil { + t.Fatal(err) + } + + if err := i.Flush(); err != nil { + t.Fatal(err) + } + + expected := `[ + { + "Dns": "0.0.0.0" + }, + { + "Dns": "1.1.1.1" + } +] +` + if b.String() != expected { + t.Fatalf("Expected `%s`, got `%s`", expected, b.String()) + } +} + +func TestIndentedInspectorEmpty(t *testing.T) { + b := new(bytes.Buffer) + i := NewIndentedInspector(b) + + if err := i.Flush(); err != nil { + t.Fatal(err) + } + + expected := "[]\n" + if b.String() != expected { + t.Fatalf("Expected `%s`, got `%s`", expected, b.String()) + } +} + +func TestIndentedInspectorRawElements(t *testing.T) { + b := new(bytes.Buffer) + i := NewIndentedInspector(b) + if err := i.Inspect(testElement{"0.0.0.0"}, []byte(`{"Dns": "0.0.0.0", "Node": "0"}`)); err != nil { + t.Fatal(err) + } + + if err := i.Inspect(testElement{"1.1.1.1"}, []byte(`{"Dns": "1.1.1.1", "Node": "1"}`)); err != nil { + t.Fatal(err) + } + + if err := i.Flush(); err != nil { + t.Fatal(err) + } + + expected := `[ + { + "Dns": "0.0.0.0", + "Node": "0" + }, + { + "Dns": "1.1.1.1", + "Node": "1" + } +] +` + if b.String() != expected { + t.Fatalf("Expected `%s`, got `%s`", expected, b.String()) + } +} diff --git a/components/cli/command/network/cmd.go b/components/cli/command/network/cmd.go new file mode 100644 index 0000000000..a7c9b3fce3 --- /dev/null +++ b/components/cli/command/network/cmd.go @@ -0,0 +1,31 @@ +package network + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" +) + +// NewNetworkCommand returns a cobra command for `network` subcommands +func NewNetworkCommand(dockerCli *command.DockerCli) *cobra.Command { + cmd := &cobra.Command{ + Use: "network", + Short: "Manage Docker networks", + Args: cli.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + }, + } + cmd.AddCommand( + newConnectCommand(dockerCli), + newCreateCommand(dockerCli), + newDisconnectCommand(dockerCli), + newInspectCommand(dockerCli), + newListCommand(dockerCli), + newRemoveCommand(dockerCli), + ) + return cmd +} diff --git a/components/cli/command/network/connect.go b/components/cli/command/network/connect.go new file mode 100644 index 0000000000..c4b676e5f1 --- /dev/null +++ b/components/cli/command/network/connect.go @@ -0,0 +1,64 @@ +package network + +import ( + "golang.org/x/net/context" + + "github.com/docker/docker/api/types/network" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/opts" + runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/spf13/cobra" +) + +type connectOptions struct { + network string + container string + ipaddress string + ipv6address string + links opts.ListOpts + aliases []string + linklocalips []string +} + +func newConnectCommand(dockerCli *command.DockerCli) *cobra.Command { + opts := connectOptions{ + links: opts.NewListOpts(runconfigopts.ValidateLink), + } + + cmd := &cobra.Command{ + Use: "connect [OPTIONS] NETWORK CONTAINER", + Short: "Connect a container to a network", + Args: cli.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + opts.network = args[0] + opts.container = args[1] + return runConnect(dockerCli, opts) + }, + } + + flags := cmd.Flags() + flags.StringVar(&opts.ipaddress, "ip", "", "IP Address") + flags.StringVar(&opts.ipv6address, "ip6", "", "IPv6 Address") + flags.Var(&opts.links, "link", "Add link to another container") + flags.StringSliceVar(&opts.aliases, "alias", []string{}, "Add network-scoped alias for the container") + flags.StringSliceVar(&opts.linklocalips, "link-local-ip", []string{}, "Add a link-local address for the container") + + return cmd +} + +func runConnect(dockerCli *command.DockerCli, opts connectOptions) error { + client := dockerCli.Client() + + epConfig := &network.EndpointSettings{ + IPAMConfig: &network.EndpointIPAMConfig{ + IPv4Address: opts.ipaddress, + IPv6Address: opts.ipv6address, + LinkLocalIPs: opts.linklocalips, + }, + Links: opts.links.GetAll(), + Aliases: opts.aliases, + } + + return client.NetworkConnect(context.Background(), opts.network, opts.container, epConfig) +} diff --git a/components/cli/command/network/create.go b/components/cli/command/network/create.go new file mode 100644 index 0000000000..2ffd80548b --- /dev/null +++ b/components/cli/command/network/create.go @@ -0,0 +1,225 @@ +package network + +import ( + "fmt" + "net" + "strings" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/network" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/opts" + runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/spf13/cobra" +) + +type createOptions struct { + name string + driver string + driverOpts opts.MapOpts + labels []string + internal bool + ipv6 bool + attachable bool + + ipamDriver string + ipamSubnet []string + ipamIPRange []string + ipamGateway []string + ipamAux opts.MapOpts + ipamOpt opts.MapOpts +} + +func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { + opts := createOptions{ + driverOpts: *opts.NewMapOpts(nil, nil), + ipamAux: *opts.NewMapOpts(nil, nil), + ipamOpt: *opts.NewMapOpts(nil, nil), + } + + cmd := &cobra.Command{ + Use: "create [OPTIONS] NETWORK", + Short: "Create a network", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.name = args[0] + return runCreate(dockerCli, opts) + }, + } + + flags := cmd.Flags() + flags.StringVarP(&opts.driver, "driver", "d", "bridge", "Driver to manage the Network") + flags.VarP(&opts.driverOpts, "opt", "o", "Set driver specific options") + flags.StringSliceVar(&opts.labels, "label", []string{}, "Set metadata on a network") + flags.BoolVar(&opts.internal, "internal", false, "Restrict external access to the network") + flags.BoolVar(&opts.ipv6, "ipv6", false, "Enable IPv6 networking") + flags.BoolVar(&opts.attachable, "attachable", false, "Enable manual container attachment") + + flags.StringVar(&opts.ipamDriver, "ipam-driver", "default", "IP Address Management Driver") + flags.StringSliceVar(&opts.ipamSubnet, "subnet", []string{}, "Subnet in CIDR format that represents a network segment") + flags.StringSliceVar(&opts.ipamIPRange, "ip-range", []string{}, "Allocate container ip from a sub-range") + flags.StringSliceVar(&opts.ipamGateway, "gateway", []string{}, "IPv4 or IPv6 Gateway for the master subnet") + + flags.Var(&opts.ipamAux, "aux-address", "Auxiliary IPv4 or IPv6 addresses used by Network driver") + flags.Var(&opts.ipamOpt, "ipam-opt", "Set IPAM driver specific options") + + return cmd +} + +func runCreate(dockerCli *command.DockerCli, opts createOptions) error { + client := dockerCli.Client() + + ipamCfg, err := consolidateIpam(opts.ipamSubnet, opts.ipamIPRange, opts.ipamGateway, opts.ipamAux.GetAll()) + if err != nil { + return err + } + + // Construct network create request body + nc := types.NetworkCreate{ + Driver: opts.driver, + Options: opts.driverOpts.GetAll(), + IPAM: &network.IPAM{ + Driver: opts.ipamDriver, + Config: ipamCfg, + Options: opts.ipamOpt.GetAll(), + }, + CheckDuplicate: true, + Internal: opts.internal, + EnableIPv6: opts.ipv6, + Attachable: opts.attachable, + Labels: runconfigopts.ConvertKVStringsToMap(opts.labels), + } + + resp, err := client.NetworkCreate(context.Background(), opts.name, nc) + if err != nil { + return err + } + fmt.Fprintf(dockerCli.Out(), "%s\n", resp.ID) + return nil +} + +// Consolidates the ipam configuration as a group from different related configurations +// user can configure network with multiple non-overlapping subnets and hence it is +// possible to correlate the various related parameters and consolidate them. +// consoidateIpam consolidates subnets, ip-ranges, gateways and auxiliary addresses into +// structured ipam data. +func consolidateIpam(subnets, ranges, gateways []string, auxaddrs map[string]string) ([]network.IPAMConfig, error) { + if len(subnets) < len(ranges) || len(subnets) < len(gateways) { + return nil, fmt.Errorf("every ip-range or gateway must have a corresponding subnet") + } + iData := map[string]*network.IPAMConfig{} + + // Populate non-overlapping subnets into consolidation map + for _, s := range subnets { + for k := range iData { + ok1, err := subnetMatches(s, k) + if err != nil { + return nil, err + } + ok2, err := subnetMatches(k, s) + if err != nil { + return nil, err + } + if ok1 || ok2 { + return nil, fmt.Errorf("multiple overlapping subnet configuration is not supported") + } + } + iData[s] = &network.IPAMConfig{Subnet: s, AuxAddress: map[string]string{}} + } + + // Validate and add valid ip ranges + for _, r := range ranges { + match := false + for _, s := range subnets { + ok, err := subnetMatches(s, r) + if err != nil { + return nil, err + } + if !ok { + continue + } + if iData[s].IPRange != "" { + return nil, fmt.Errorf("cannot configure multiple ranges (%s, %s) on the same subnet (%s)", r, iData[s].IPRange, s) + } + d := iData[s] + d.IPRange = r + match = true + } + if !match { + return nil, fmt.Errorf("no matching subnet for range %s", r) + } + } + + // Validate and add valid gateways + for _, g := range gateways { + match := false + for _, s := range subnets { + ok, err := subnetMatches(s, g) + if err != nil { + return nil, err + } + if !ok { + continue + } + if iData[s].Gateway != "" { + return nil, fmt.Errorf("cannot configure multiple gateways (%s, %s) for the same subnet (%s)", g, iData[s].Gateway, s) + } + d := iData[s] + d.Gateway = g + match = true + } + if !match { + return nil, fmt.Errorf("no matching subnet for gateway %s", g) + } + } + + // Validate and add aux-addresses + for key, aa := range auxaddrs { + match := false + for _, s := range subnets { + ok, err := subnetMatches(s, aa) + if err != nil { + return nil, err + } + if !ok { + continue + } + iData[s].AuxAddress[key] = aa + match = true + } + if !match { + return nil, fmt.Errorf("no matching subnet for aux-address %s", aa) + } + } + + idl := []network.IPAMConfig{} + for _, v := range iData { + idl = append(idl, *v) + } + return idl, nil +} + +func subnetMatches(subnet, data string) (bool, error) { + var ( + ip net.IP + ) + + _, s, err := net.ParseCIDR(subnet) + if err != nil { + return false, fmt.Errorf("Invalid subnet %s : %v", s, err) + } + + if strings.Contains(data, "/") { + ip, _, err = net.ParseCIDR(data) + if err != nil { + return false, fmt.Errorf("Invalid cidr %s : %v", data, err) + } + } else { + ip = net.ParseIP(data) + } + + return s.Contains(ip), nil +} diff --git a/components/cli/command/network/disconnect.go b/components/cli/command/network/disconnect.go new file mode 100644 index 0000000000..c9d9c14a13 --- /dev/null +++ b/components/cli/command/network/disconnect.go @@ -0,0 +1,41 @@ +package network + +import ( + "golang.org/x/net/context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +type disconnectOptions struct { + network string + container string + force bool +} + +func newDisconnectCommand(dockerCli *command.DockerCli) *cobra.Command { + opts := disconnectOptions{} + + cmd := &cobra.Command{ + Use: "disconnect [OPTIONS] NETWORK CONTAINER", + Short: "Disconnect a container from a network", + Args: cli.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + opts.network = args[0] + opts.container = args[1] + return runDisconnect(dockerCli, opts) + }, + } + + flags := cmd.Flags() + flags.BoolVarP(&opts.force, "force", "f", false, "Force the container to disconnect from a network") + + return cmd +} + +func runDisconnect(dockerCli *command.DockerCli, opts disconnectOptions) error { + client := dockerCli.Client() + + return client.NetworkDisconnect(context.Background(), opts.network, opts.container, opts.force) +} diff --git a/components/cli/command/network/inspect.go b/components/cli/command/network/inspect.go new file mode 100644 index 0000000000..f1f677db9c --- /dev/null +++ b/components/cli/command/network/inspect.go @@ -0,0 +1,45 @@ +package network + +import ( + "golang.org/x/net/context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/inspect" + "github.com/spf13/cobra" +) + +type inspectOptions struct { + format string + names []string +} + +func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts inspectOptions + + cmd := &cobra.Command{ + Use: "inspect [OPTIONS] NETWORK [NETWORK...]", + Short: "Display detailed information on one or more networks", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.names = args + return runInspect(dockerCli, opts) + }, + } + + cmd.Flags().StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template") + + return cmd +} + +func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { + client := dockerCli.Client() + + ctx := context.Background() + + getNetFunc := func(name string) (interface{}, []byte, error) { + return client.NetworkInspectWithRaw(ctx, name) + } + + return inspect.Inspect(dockerCli.Out(), opts.names, opts.format, getNetFunc) +} diff --git a/components/cli/command/network/list.go b/components/cli/command/network/list.go new file mode 100644 index 0000000000..19013a3b8e --- /dev/null +++ b/components/cli/command/network/list.go @@ -0,0 +1,96 @@ +package network + +import ( + "sort" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/formatter" + "github.com/spf13/cobra" +) + +type byNetworkName []types.NetworkResource + +func (r byNetworkName) Len() int { return len(r) } +func (r byNetworkName) Swap(i, j int) { r[i], r[j] = r[j], r[i] } +func (r byNetworkName) Less(i, j int) bool { return r[i].Name < r[j].Name } + +type listOptions struct { + quiet bool + noTrunc bool + format string + filter []string +} + +func newListCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts listOptions + + cmd := &cobra.Command{ + Use: "ls [OPTIONS]", + Aliases: []string{"list"}, + Short: "List networks", + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runList(dockerCli, opts) + }, + } + + flags := cmd.Flags() + flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display network IDs") + flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate the output") + flags.StringVar(&opts.format, "format", "", "Pretty-print networks using a Go template") + flags.StringSliceVarP(&opts.filter, "filter", "f", []string{}, "Provide filter values (i.e. 'dangling=true')") + + return cmd +} + +func runList(dockerCli *command.DockerCli, opts listOptions) error { + client := dockerCli.Client() + + netFilterArgs := filters.NewArgs() + for _, f := range opts.filter { + var err error + netFilterArgs, err = filters.ParseFlag(f, netFilterArgs) + if err != nil { + return err + } + } + + options := types.NetworkListOptions{ + Filters: netFilterArgs, + } + + networkResources, err := client.NetworkList(context.Background(), options) + if err != nil { + return err + } + + f := opts.format + if len(f) == 0 { + if len(dockerCli.ConfigFile().NetworksFormat) > 0 && !opts.quiet { + f = dockerCli.ConfigFile().NetworksFormat + } else { + f = "table" + } + } + + sort.Sort(byNetworkName(networkResources)) + + networksCtx := formatter.NetworkContext{ + Context: formatter.Context{ + Output: dockerCli.Out(), + Format: f, + Quiet: opts.quiet, + Trunc: !opts.noTrunc, + }, + Networks: networkResources, + } + + networksCtx.Write() + + return nil +} diff --git a/components/cli/command/network/remove.go b/components/cli/command/network/remove.go new file mode 100644 index 0000000000..2034b8709e --- /dev/null +++ b/components/cli/command/network/remove.go @@ -0,0 +1,43 @@ +package network + +import ( + "fmt" + + "golang.org/x/net/context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { + return &cobra.Command{ + Use: "rm NETWORK [NETWORK...]", + Aliases: []string{"remove"}, + Short: "Remove one or more networks", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runRemove(dockerCli, args) + }, + } +} + +func runRemove(dockerCli *command.DockerCli, networks []string) error { + client := dockerCli.Client() + ctx := context.Background() + status := 0 + + for _, name := range networks { + if err := client.NetworkRemove(ctx, name); err != nil { + fmt.Fprintf(dockerCli.Err(), "%s\n", err) + status = 1 + continue + } + fmt.Fprintf(dockerCli.Out(), "%s\n", name) + } + + if status != 0 { + return cli.StatusError{StatusCode: status} + } + return nil +} diff --git a/components/cli/command/node/cmd.go b/components/cli/command/node/cmd.go new file mode 100644 index 0000000000..6aa4dfcb18 --- /dev/null +++ b/components/cli/command/node/cmd.go @@ -0,0 +1,47 @@ +package node + +import ( + "fmt" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + apiclient "github.com/docker/docker/client" + "github.com/spf13/cobra" + "golang.org/x/net/context" +) + +// NewNodeCommand returns a cobra command for `node` subcommands +func NewNodeCommand(dockerCli *command.DockerCli) *cobra.Command { + cmd := &cobra.Command{ + Use: "node", + Short: "Manage Docker Swarm nodes", + Args: cli.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + }, + } + cmd.AddCommand( + newDemoteCommand(dockerCli), + newInspectCommand(dockerCli), + newListCommand(dockerCli), + newPromoteCommand(dockerCli), + newRemoveCommand(dockerCli), + newPsCommand(dockerCli), + newUpdateCommand(dockerCli), + ) + return cmd +} + +// Reference returns the reference of a node. The special value "self" for a node +// reference is mapped to the current node, hence the node ID is retrieved using +// the `/info` endpoint. +func Reference(ctx context.Context, client apiclient.APIClient, ref string) (string, error) { + if ref == "self" { + info, err := client.Info(ctx) + if err != nil { + return "", err + } + return info.Swarm.NodeID, nil + } + return ref, nil +} diff --git a/components/cli/command/node/demote.go b/components/cli/command/node/demote.go new file mode 100644 index 0000000000..33f86c6499 --- /dev/null +++ b/components/cli/command/node/demote.go @@ -0,0 +1,36 @@ +package node + +import ( + "fmt" + + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +func newDemoteCommand(dockerCli *command.DockerCli) *cobra.Command { + return &cobra.Command{ + Use: "demote NODE [NODE...]", + Short: "Demote one or more nodes from manager in the swarm", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runDemote(dockerCli, args) + }, + } +} + +func runDemote(dockerCli *command.DockerCli, nodes []string) error { + demote := func(node *swarm.Node) error { + if node.Spec.Role == swarm.NodeRoleWorker { + fmt.Fprintf(dockerCli.Out(), "Node %s is already a worker.\n", node.ID) + return errNoRoleChange + } + node.Spec.Role = swarm.NodeRoleWorker + return nil + } + success := func(nodeID string) { + fmt.Fprintf(dockerCli.Out(), "Manager %s demoted in the swarm.\n", nodeID) + } + return updateNodes(dockerCli, nodes, demote, success) +} diff --git a/components/cli/command/node/inspect.go b/components/cli/command/node/inspect.go new file mode 100644 index 0000000000..c73b83a87c --- /dev/null +++ b/components/cli/command/node/inspect.go @@ -0,0 +1,144 @@ +package node + +import ( + "fmt" + "io" + "sort" + "strings" + + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/inspect" + "github.com/docker/docker/pkg/ioutils" + "github.com/docker/go-units" + "github.com/spf13/cobra" + "golang.org/x/net/context" +) + +type inspectOptions struct { + nodeIds []string + format string + pretty bool +} + +func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts inspectOptions + + cmd := &cobra.Command{ + Use: "inspect [OPTIONS] self|NODE [NODE...]", + Short: "Display detailed information on one or more nodes", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.nodeIds = args + return runInspect(dockerCli, opts) + }, + } + + flags := cmd.Flags() + flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template") + flags.BoolVar(&opts.pretty, "pretty", false, "Print the information in a human friendly format.") + return cmd +} + +func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { + client := dockerCli.Client() + ctx := context.Background() + getRef := func(ref string) (interface{}, []byte, error) { + nodeRef, err := Reference(ctx, client, ref) + if err != nil { + return nil, nil, err + } + node, _, err := client.NodeInspectWithRaw(ctx, nodeRef) + return node, nil, err + } + + if !opts.pretty { + return inspect.Inspect(dockerCli.Out(), opts.nodeIds, opts.format, getRef) + } + return printHumanFriendly(dockerCli.Out(), opts.nodeIds, getRef) +} + +func printHumanFriendly(out io.Writer, refs []string, getRef inspect.GetRefFunc) error { + for idx, ref := range refs { + obj, _, err := getRef(ref) + if err != nil { + return err + } + printNode(out, obj.(swarm.Node)) + + // TODO: better way to do this? + // print extra space between objects, but not after the last one + if idx+1 != len(refs) { + fmt.Fprintf(out, "\n\n") + } else { + fmt.Fprintf(out, "\n") + } + } + return nil +} + +// TODO: use a template +func printNode(out io.Writer, node swarm.Node) { + fmt.Fprintf(out, "ID:\t\t\t%s\n", node.ID) + ioutils.FprintfIfNotEmpty(out, "Name:\t\t\t%s\n", node.Spec.Name) + if node.Spec.Labels != nil { + fmt.Fprintln(out, "Labels:") + for k, v := range node.Spec.Labels { + fmt.Fprintf(out, " - %s = %s\n", k, v) + } + } + + ioutils.FprintfIfNotEmpty(out, "Hostname:\t\t%s\n", node.Description.Hostname) + fmt.Fprintf(out, "Joined at:\t\t%s\n", command.PrettyPrint(node.CreatedAt)) + fmt.Fprintln(out, "Status:") + fmt.Fprintf(out, " State:\t\t\t%s\n", command.PrettyPrint(node.Status.State)) + ioutils.FprintfIfNotEmpty(out, " Message:\t\t%s\n", command.PrettyPrint(node.Status.Message)) + fmt.Fprintf(out, " Availability:\t\t%s\n", command.PrettyPrint(node.Spec.Availability)) + + if node.ManagerStatus != nil { + fmt.Fprintln(out, "Manager Status:") + fmt.Fprintf(out, " Address:\t\t%s\n", node.ManagerStatus.Addr) + fmt.Fprintf(out, " Raft Status:\t\t%s\n", command.PrettyPrint(node.ManagerStatus.Reachability)) + leader := "No" + if node.ManagerStatus.Leader { + leader = "Yes" + } + fmt.Fprintf(out, " Leader:\t\t%s\n", leader) + } + + fmt.Fprintln(out, "Platform:") + fmt.Fprintf(out, " Operating System:\t%s\n", node.Description.Platform.OS) + fmt.Fprintf(out, " Architecture:\t\t%s\n", node.Description.Platform.Architecture) + + fmt.Fprintln(out, "Resources:") + fmt.Fprintf(out, " CPUs:\t\t\t%d\n", node.Description.Resources.NanoCPUs/1e9) + fmt.Fprintf(out, " Memory:\t\t%s\n", units.BytesSize(float64(node.Description.Resources.MemoryBytes))) + + var pluginTypes []string + pluginNamesByType := map[string][]string{} + for _, p := range node.Description.Engine.Plugins { + // append to pluginTypes only if not done previously + if _, ok := pluginNamesByType[p.Type]; !ok { + pluginTypes = append(pluginTypes, p.Type) + } + pluginNamesByType[p.Type] = append(pluginNamesByType[p.Type], p.Name) + } + + if len(pluginTypes) > 0 { + fmt.Fprintln(out, "Plugins:") + sort.Strings(pluginTypes) // ensure stable output + for _, pluginType := range pluginTypes { + fmt.Fprintf(out, " %s:\t\t%s\n", pluginType, strings.Join(pluginNamesByType[pluginType], ", ")) + } + } + fmt.Fprintf(out, "Engine Version:\t\t%s\n", node.Description.Engine.EngineVersion) + + if len(node.Description.Engine.Labels) != 0 { + fmt.Fprintln(out, "Engine Labels:") + for k, v := range node.Description.Engine.Labels { + fmt.Fprintf(out, " - %s = %s", k, v) + } + } + +} diff --git a/components/cli/command/node/list.go b/components/cli/command/node/list.go new file mode 100644 index 0000000000..bed4bc4965 --- /dev/null +++ b/components/cli/command/node/list.go @@ -0,0 +1,111 @@ +package node + +import ( + "fmt" + "io" + "text/tabwriter" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/opts" + "github.com/spf13/cobra" +) + +const ( + listItemFmt = "%s\t%s\t%s\t%s\t%s\n" +) + +type listOptions struct { + quiet bool + filter opts.FilterOpt +} + +func newListCommand(dockerCli *command.DockerCli) *cobra.Command { + opts := listOptions{filter: opts.NewFilterOpt()} + + cmd := &cobra.Command{ + Use: "ls [OPTIONS]", + Aliases: []string{"list"}, + Short: "List nodes in the swarm", + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runList(dockerCli, opts) + }, + } + flags := cmd.Flags() + flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs") + flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") + + return cmd +} + +func runList(dockerCli *command.DockerCli, opts listOptions) error { + client := dockerCli.Client() + ctx := context.Background() + + nodes, err := client.NodeList( + ctx, + types.NodeListOptions{Filter: opts.filter.Value()}) + if err != nil { + return err + } + + info, err := client.Info(ctx) + if err != nil { + return err + } + + out := dockerCli.Out() + if opts.quiet { + printQuiet(out, nodes) + } else { + printTable(out, nodes, info) + } + return nil +} + +func printTable(out io.Writer, nodes []swarm.Node, info types.Info) { + writer := tabwriter.NewWriter(out, 0, 4, 2, ' ', 0) + + // Ignore flushing errors + defer writer.Flush() + + fmt.Fprintf(writer, listItemFmt, "ID", "HOSTNAME", "STATUS", "AVAILABILITY", "MANAGER STATUS") + for _, node := range nodes { + name := node.Description.Hostname + availability := string(node.Spec.Availability) + + reachability := "" + if node.ManagerStatus != nil { + if node.ManagerStatus.Leader { + reachability = "Leader" + } else { + reachability = string(node.ManagerStatus.Reachability) + } + } + + ID := node.ID + if node.ID == info.Swarm.NodeID { + ID = ID + " *" + } + + fmt.Fprintf( + writer, + listItemFmt, + ID, + name, + command.PrettyPrint(string(node.Status.State)), + command.PrettyPrint(availability), + command.PrettyPrint(reachability)) + } +} + +func printQuiet(out io.Writer, nodes []swarm.Node) { + for _, node := range nodes { + fmt.Fprintln(out, node.ID) + } +} diff --git a/components/cli/command/node/opts.go b/components/cli/command/node/opts.go new file mode 100644 index 0000000000..7e6c55d487 --- /dev/null +++ b/components/cli/command/node/opts.go @@ -0,0 +1,60 @@ +package node + +import ( + "fmt" + "strings" + + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/opts" + runconfigopts "github.com/docker/docker/runconfig/opts" +) + +type nodeOptions struct { + annotations + role string + availability string +} + +type annotations struct { + name string + labels opts.ListOpts +} + +func newNodeOptions() *nodeOptions { + return &nodeOptions{ + annotations: annotations{ + labels: opts.NewListOpts(nil), + }, + } +} + +func (opts *nodeOptions) ToNodeSpec() (swarm.NodeSpec, error) { + var spec swarm.NodeSpec + + spec.Annotations.Name = opts.annotations.name + spec.Annotations.Labels = runconfigopts.ConvertKVStringsToMap(opts.annotations.labels.GetAll()) + + switch swarm.NodeRole(strings.ToLower(opts.role)) { + case swarm.NodeRoleWorker: + spec.Role = swarm.NodeRoleWorker + case swarm.NodeRoleManager: + spec.Role = swarm.NodeRoleManager + case "": + default: + return swarm.NodeSpec{}, fmt.Errorf("invalid role %q, only worker and manager are supported", opts.role) + } + + switch swarm.NodeAvailability(strings.ToLower(opts.availability)) { + case swarm.NodeAvailabilityActive: + spec.Availability = swarm.NodeAvailabilityActive + case swarm.NodeAvailabilityPause: + spec.Availability = swarm.NodeAvailabilityPause + case swarm.NodeAvailabilityDrain: + spec.Availability = swarm.NodeAvailabilityDrain + case "": + default: + return swarm.NodeSpec{}, fmt.Errorf("invalid availability %q, only active, pause and drain are supported", opts.availability) + } + + return spec, nil +} diff --git a/components/cli/command/node/promote.go b/components/cli/command/node/promote.go new file mode 100644 index 0000000000..f47d783f4c --- /dev/null +++ b/components/cli/command/node/promote.go @@ -0,0 +1,36 @@ +package node + +import ( + "fmt" + + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +func newPromoteCommand(dockerCli *command.DockerCli) *cobra.Command { + return &cobra.Command{ + Use: "promote NODE [NODE...]", + Short: "Promote one or more nodes to manager in the swarm", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runPromote(dockerCli, args) + }, + } +} + +func runPromote(dockerCli *command.DockerCli, nodes []string) error { + promote := func(node *swarm.Node) error { + if node.Spec.Role == swarm.NodeRoleManager { + fmt.Fprintf(dockerCli.Out(), "Node %s is already a manager.\n", node.ID) + return errNoRoleChange + } + node.Spec.Role = swarm.NodeRoleManager + return nil + } + success := func(nodeID string) { + fmt.Fprintf(dockerCli.Out(), "Node %s promoted to a manager in the swarm.\n", nodeID) + } + return updateNodes(dockerCli, nodes, promote, success) +} diff --git a/components/cli/command/node/ps.go b/components/cli/command/node/ps.go new file mode 100644 index 0000000000..84d4b375ac --- /dev/null +++ b/components/cli/command/node/ps.go @@ -0,0 +1,69 @@ +package node + +import ( + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/idresolver" + "github.com/docker/docker/cli/command/task" + "github.com/docker/docker/opts" + "github.com/spf13/cobra" + "golang.org/x/net/context" +) + +type psOptions struct { + nodeID string + noResolve bool + noTrunc bool + filter opts.FilterOpt +} + +func newPsCommand(dockerCli *command.DockerCli) *cobra.Command { + opts := psOptions{filter: opts.NewFilterOpt()} + + cmd := &cobra.Command{ + Use: "ps [OPTIONS] [NODE]", + Short: "List tasks running on a node, defaults to current node", + Args: cli.RequiresRangeArgs(0, 1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.nodeID = "self" + + if len(args) != 0 { + opts.nodeID = args[0] + } + + return runPs(dockerCli, opts) + }, + } + flags := cmd.Flags() + flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output") + flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names") + flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") + + return cmd +} + +func runPs(dockerCli *command.DockerCli, opts psOptions) error { + client := dockerCli.Client() + ctx := context.Background() + + nodeRef, err := Reference(ctx, client, opts.nodeID) + if err != nil { + return nil + } + node, _, err := client.NodeInspectWithRaw(ctx, nodeRef) + if err != nil { + return err + } + + filter := opts.filter.Value() + filter.Add("node", node.ID) + tasks, err := client.TaskList( + ctx, + types.TaskListOptions{Filter: filter}) + if err != nil { + return err + } + + return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), opts.noTrunc) +} diff --git a/components/cli/command/node/remove.go b/components/cli/command/node/remove.go new file mode 100644 index 0000000000..696cd58716 --- /dev/null +++ b/components/cli/command/node/remove.go @@ -0,0 +1,46 @@ +package node + +import ( + "fmt" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +type removeOptions struct { + force bool +} + +func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { + opts := removeOptions{} + + cmd := &cobra.Command{ + Use: "rm [OPTIONS] NODE [NODE...]", + Aliases: []string{"remove"}, + Short: "Remove one or more nodes from the swarm", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runRemove(dockerCli, args, opts) + }, + } + flags := cmd.Flags() + flags.BoolVar(&opts.force, "force", false, "Force remove an active node") + return cmd +} + +func runRemove(dockerCli *command.DockerCli, args []string, opts removeOptions) error { + client := dockerCli.Client() + ctx := context.Background() + for _, nodeID := range args { + err := client.NodeRemove(ctx, nodeID, types.NodeRemoveOptions{Force: opts.force}) + if err != nil { + return err + } + fmt.Fprintf(dockerCli.Out(), "%s\n", nodeID) + } + return nil +} diff --git a/components/cli/command/node/update.go b/components/cli/command/node/update.go new file mode 100644 index 0000000000..65339e138b --- /dev/null +++ b/components/cli/command/node/update.go @@ -0,0 +1,121 @@ +package node + +import ( + "errors" + "fmt" + + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/opts" + runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "golang.org/x/net/context" +) + +var ( + errNoRoleChange = errors.New("role was already set to the requested value") +) + +func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { + nodeOpts := newNodeOptions() + + cmd := &cobra.Command{ + Use: "update [OPTIONS] NODE", + Short: "Update a node", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runUpdate(dockerCli, cmd.Flags(), args[0]) + }, + } + + flags := cmd.Flags() + flags.StringVar(&nodeOpts.role, flagRole, "", "Role of the node (worker/manager)") + flags.StringVar(&nodeOpts.availability, flagAvailability, "", "Availability of the node (active/pause/drain)") + flags.Var(&nodeOpts.annotations.labels, flagLabelAdd, "Add or update a node label (key=value)") + labelKeys := opts.NewListOpts(nil) + flags.Var(&labelKeys, flagLabelRemove, "Remove a node label if exists") + return cmd +} + +func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, nodeID string) error { + success := func(_ string) { + fmt.Fprintln(dockerCli.Out(), nodeID) + } + return updateNodes(dockerCli, []string{nodeID}, mergeNodeUpdate(flags), success) +} + +func updateNodes(dockerCli *command.DockerCli, nodes []string, mergeNode func(node *swarm.Node) error, success func(nodeID string)) error { + client := dockerCli.Client() + ctx := context.Background() + + for _, nodeID := range nodes { + node, _, err := client.NodeInspectWithRaw(ctx, nodeID) + if err != nil { + return err + } + + err = mergeNode(&node) + if err != nil { + if err == errNoRoleChange { + continue + } + return err + } + err = client.NodeUpdate(ctx, node.ID, node.Version, node.Spec) + if err != nil { + return err + } + success(nodeID) + } + return nil +} + +func mergeNodeUpdate(flags *pflag.FlagSet) func(*swarm.Node) error { + return func(node *swarm.Node) error { + spec := &node.Spec + + if flags.Changed(flagRole) { + str, err := flags.GetString(flagRole) + if err != nil { + return err + } + spec.Role = swarm.NodeRole(str) + } + if flags.Changed(flagAvailability) { + str, err := flags.GetString(flagAvailability) + if err != nil { + return err + } + spec.Availability = swarm.NodeAvailability(str) + } + if spec.Annotations.Labels == nil { + spec.Annotations.Labels = make(map[string]string) + } + if flags.Changed(flagLabelAdd) { + labels := flags.Lookup(flagLabelAdd).Value.(*opts.ListOpts).GetAll() + for k, v := range runconfigopts.ConvertKVStringsToMap(labels) { + spec.Annotations.Labels[k] = v + } + } + if flags.Changed(flagLabelRemove) { + keys := flags.Lookup(flagLabelRemove).Value.(*opts.ListOpts).GetAll() + for _, k := range keys { + // if a key doesn't exist, fail the command explicitly + if _, exists := spec.Annotations.Labels[k]; !exists { + return fmt.Errorf("key %s doesn't exist in node's labels", k) + } + delete(spec.Annotations.Labels, k) + } + } + return nil + } +} + +const ( + flagRole = "role" + flagAvailability = "availability" + flagLabelAdd = "label-add" + flagLabelRemove = "label-rm" +) diff --git a/components/cli/command/out.go b/components/cli/command/out.go new file mode 100644 index 0000000000..09375d07d7 --- /dev/null +++ b/components/cli/command/out.go @@ -0,0 +1,69 @@ +package command + +import ( + "io" + "os" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/pkg/term" +) + +// OutStream is an output stream used by the DockerCli to write normal program +// output. +type OutStream struct { + out io.Writer + fd uintptr + isTerminal bool + state *term.State +} + +func (o *OutStream) Write(p []byte) (int, error) { + return o.out.Write(p) +} + +// FD returns the file descriptor number for this stream +func (o *OutStream) FD() uintptr { + return o.fd +} + +// IsTerminal returns true if this stream is connected to a terminal +func (o *OutStream) IsTerminal() bool { + return o.isTerminal +} + +// SetRawTerminal sets raw mode on the output terminal +func (o *OutStream) SetRawTerminal() (err error) { + if os.Getenv("NORAW") != "" || !o.isTerminal { + return nil + } + o.state, err = term.SetRawTerminalOutput(o.fd) + return err +} + +// RestoreTerminal restores normal mode to the terminal +func (o *OutStream) RestoreTerminal() { + if o.state != nil { + term.RestoreTerminal(o.fd, o.state) + } +} + +// GetTtySize returns the height and width in characters of the tty +func (o *OutStream) GetTtySize() (int, int) { + if !o.isTerminal { + return 0, 0 + } + ws, err := term.GetWinsize(o.fd) + if err != nil { + logrus.Debugf("Error getting size: %s", err) + if ws == nil { + return 0, 0 + } + } + return int(ws.Height), int(ws.Width) +} + +// NewOutStream returns a new OutStream object from a Writer +func NewOutStream(out io.Writer) *OutStream { + fd, isTerminal := term.GetFdInfo(out) + return &OutStream{out: out, fd: fd, isTerminal: isTerminal} +} diff --git a/components/cli/command/plugin/cmd.go b/components/cli/command/plugin/cmd.go new file mode 100644 index 0000000000..67d0d5031c --- /dev/null +++ b/components/cli/command/plugin/cmd.go @@ -0,0 +1,12 @@ +// +build !experimental + +package plugin + +import ( + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +// NewPluginCommand returns a cobra command for `plugin` subcommands +func NewPluginCommand(cmd *cobra.Command, dockerCli *command.DockerCli) { +} diff --git a/components/cli/command/plugin/cmd_experimental.go b/components/cli/command/plugin/cmd_experimental.go new file mode 100644 index 0000000000..6c991937fe --- /dev/null +++ b/components/cli/command/plugin/cmd_experimental.go @@ -0,0 +1,36 @@ +// +build experimental + +package plugin + +import ( + "fmt" + + "github.com/docker/docker/api/client" + "github.com/docker/docker/cli" + "github.com/spf13/cobra" +) + +// NewPluginCommand returns a cobra command for `plugin` subcommands +func NewPluginCommand(rootCmd *cobra.Command, dockerCli *client.DockerCli) { + cmd := &cobra.Command{ + Use: "plugin", + Short: "Manage Docker plugins", + Args: cli.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + }, + } + + cmd.AddCommand( + newDisableCommand(dockerCli), + newEnableCommand(dockerCli), + newInspectCommand(dockerCli), + newInstallCommand(dockerCli), + newListCommand(dockerCli), + newRemoveCommand(dockerCli), + newSetCommand(dockerCli), + newPushCommand(dockerCli), + ) + + rootCmd.AddCommand(cmd) +} diff --git a/components/cli/command/plugin/disable.go b/components/cli/command/plugin/disable.go new file mode 100644 index 0000000000..704eb75286 --- /dev/null +++ b/components/cli/command/plugin/disable.go @@ -0,0 +1,45 @@ +// +build experimental + +package plugin + +import ( + "fmt" + + "github.com/docker/docker/api/client" + "github.com/docker/docker/cli" + "github.com/docker/docker/reference" + "github.com/spf13/cobra" + "golang.org/x/net/context" +) + +func newDisableCommand(dockerCli *client.DockerCli) *cobra.Command { + cmd := &cobra.Command{ + Use: "disable PLUGIN", + Short: "Disable a plugin", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runDisable(dockerCli, args[0]) + }, + } + + return cmd +} + +func runDisable(dockerCli *client.DockerCli, name string) error { + named, err := reference.ParseNamed(name) // FIXME: validate + if err != nil { + return err + } + if reference.IsNameOnly(named) { + named = reference.WithDefaultTag(named) + } + ref, ok := named.(reference.NamedTagged) + if !ok { + return fmt.Errorf("invalid name: %s", named.String()) + } + if err := dockerCli.Client().PluginDisable(context.Background(), ref.String()); err != nil { + return err + } + fmt.Fprintln(dockerCli.Out(), name) + return nil +} diff --git a/components/cli/command/plugin/enable.go b/components/cli/command/plugin/enable.go new file mode 100644 index 0000000000..c31258bbb6 --- /dev/null +++ b/components/cli/command/plugin/enable.go @@ -0,0 +1,45 @@ +// +build experimental + +package plugin + +import ( + "fmt" + + "github.com/docker/docker/api/client" + "github.com/docker/docker/cli" + "github.com/docker/docker/reference" + "github.com/spf13/cobra" + "golang.org/x/net/context" +) + +func newEnableCommand(dockerCli *client.DockerCli) *cobra.Command { + cmd := &cobra.Command{ + Use: "enable PLUGIN", + Short: "Enable a plugin", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runEnable(dockerCli, args[0]) + }, + } + + return cmd +} + +func runEnable(dockerCli *client.DockerCli, name string) error { + named, err := reference.ParseNamed(name) // FIXME: validate + if err != nil { + return err + } + if reference.IsNameOnly(named) { + named = reference.WithDefaultTag(named) + } + ref, ok := named.(reference.NamedTagged) + if !ok { + return fmt.Errorf("invalid name: %s", named.String()) + } + if err := dockerCli.Client().PluginEnable(context.Background(), ref.String()); err != nil { + return err + } + fmt.Fprintln(dockerCli.Out(), name) + return nil +} diff --git a/components/cli/command/plugin/inspect.go b/components/cli/command/plugin/inspect.go new file mode 100644 index 0000000000..b43e3e9453 --- /dev/null +++ b/components/cli/command/plugin/inspect.go @@ -0,0 +1,59 @@ +// +build experimental + +package plugin + +import ( + "fmt" + + "github.com/docker/docker/api/client" + "github.com/docker/docker/api/client/inspect" + "github.com/docker/docker/cli" + "github.com/docker/docker/reference" + "github.com/spf13/cobra" + "golang.org/x/net/context" +) + +type inspectOptions struct { + pluginNames []string + format string +} + +func newInspectCommand(dockerCli *client.DockerCli) *cobra.Command { + var opts inspectOptions + + cmd := &cobra.Command{ + Use: "inspect [OPTIONS] PLUGIN [PLUGIN...]", + Short: "Display detailed information on one or more plugins", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.pluginNames = args + return runInspect(dockerCli, opts) + }, + } + + flags := cmd.Flags() + flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template") + return cmd +} + +func runInspect(dockerCli *client.DockerCli, opts inspectOptions) error { + client := dockerCli.Client() + ctx := context.Background() + getRef := func(name string) (interface{}, []byte, error) { + named, err := reference.ParseNamed(name) // FIXME: validate + if err != nil { + return nil, nil, err + } + if reference.IsNameOnly(named) { + named = reference.WithDefaultTag(named) + } + ref, ok := named.(reference.NamedTagged) + if !ok { + return nil, nil, fmt.Errorf("invalid name: %s", named.String()) + } + + return client.PluginInspectWithRaw(ctx, ref.String()) + } + + return inspect.Inspect(dockerCli.Out(), opts.pluginNames, opts.format, getRef) +} diff --git a/components/cli/command/plugin/install.go b/components/cli/command/plugin/install.go new file mode 100644 index 0000000000..05dc8e8268 --- /dev/null +++ b/components/cli/command/plugin/install.go @@ -0,0 +1,103 @@ +// +build experimental + +package plugin + +import ( + "bufio" + "fmt" + "strings" + + "github.com/docker/docker/api/client" + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli" + "github.com/docker/docker/reference" + "github.com/docker/docker/registry" + "github.com/spf13/cobra" + "golang.org/x/net/context" +) + +type pluginOptions struct { + name string + grantPerms bool + disable bool +} + +func newInstallCommand(dockerCli *client.DockerCli) *cobra.Command { + var options pluginOptions + cmd := &cobra.Command{ + Use: "install [OPTIONS] PLUGIN", + Short: "Install a plugin", + Args: cli.ExactArgs(1), // TODO: allow for set args + RunE: func(cmd *cobra.Command, args []string) error { + options.name = args[0] + return runInstall(dockerCli, options) + }, + } + + flags := cmd.Flags() + flags.BoolVar(&options.grantPerms, "grant-all-permissions", false, "Grant all permissions necessary to run the plugin") + flags.BoolVar(&options.disable, "disable", false, "Do not enable the plugin on install") + + return cmd +} + +func runInstall(dockerCli *client.DockerCli, opts pluginOptions) error { + named, err := reference.ParseNamed(opts.name) // FIXME: validate + if err != nil { + return err + } + if reference.IsNameOnly(named) { + named = reference.WithDefaultTag(named) + } + ref, ok := named.(reference.NamedTagged) + if !ok { + return fmt.Errorf("invalid name: %s", named.String()) + } + + ctx := context.Background() + + repoInfo, err := registry.ParseRepositoryInfo(named) + if err != nil { + return err + } + + authConfig := dockerCli.ResolveAuthConfig(ctx, repoInfo.Index) + + encodedAuth, err := client.EncodeAuthToBase64(authConfig) + if err != nil { + return err + } + + registryAuthFunc := dockerCli.RegistryAuthenticationPrivilegedFunc(repoInfo.Index, "plugin install") + + options := types.PluginInstallOptions{ + RegistryAuth: encodedAuth, + Disabled: opts.disable, + AcceptAllPermissions: opts.grantPerms, + AcceptPermissionsFunc: acceptPrivileges(dockerCli, opts.name), + // TODO: Rename PrivilegeFunc, it has nothing to do with privileges + PrivilegeFunc: registryAuthFunc, + } + if err := dockerCli.Client().PluginInstall(ctx, ref.String(), options); err != nil { + return err + } + fmt.Fprintln(dockerCli.Out(), opts.name) + return nil +} + +func acceptPrivileges(dockerCli *client.DockerCli, name string) func(privileges types.PluginPrivileges) (bool, error) { + return func(privileges types.PluginPrivileges) (bool, error) { + fmt.Fprintf(dockerCli.Out(), "Plugin %q is requesting the following privileges:\n", name) + for _, privilege := range privileges { + fmt.Fprintf(dockerCli.Out(), " - %s: %v\n", privilege.Name, privilege.Value) + } + + fmt.Fprint(dockerCli.Out(), "Do you grant the above permissions? [y/N] ") + reader := bufio.NewReader(dockerCli.In()) + line, _, err := reader.ReadLine() + if err != nil { + return false, err + } + return strings.ToLower(string(line)) == "y", nil + } +} diff --git a/components/cli/command/plugin/list.go b/components/cli/command/plugin/list.go new file mode 100644 index 0000000000..b50b2066a7 --- /dev/null +++ b/components/cli/command/plugin/list.go @@ -0,0 +1,62 @@ +// +build experimental + +package plugin + +import ( + "fmt" + "strings" + "text/tabwriter" + + "github.com/docker/docker/api/client" + "github.com/docker/docker/cli" + "github.com/docker/docker/pkg/stringutils" + "github.com/spf13/cobra" + "golang.org/x/net/context" +) + +type listOptions struct { + noTrunc bool +} + +func newListCommand(dockerCli *client.DockerCli) *cobra.Command { + var opts listOptions + + cmd := &cobra.Command{ + Use: "ls [OPTIONS]", + Short: "List plugins", + Aliases: []string{"list"}, + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runList(dockerCli, opts) + }, + } + + flags := cmd.Flags() + + flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output") + + return cmd +} + +func runList(dockerCli *client.DockerCli, opts listOptions) error { + plugins, err := dockerCli.Client().PluginList(context.Background()) + if err != nil { + return err + } + + w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0) + fmt.Fprintf(w, "NAME \tTAG \tDESCRIPTION\tENABLED") + fmt.Fprintf(w, "\n") + + for _, p := range plugins { + desc := strings.Replace(p.Manifest.Description, "\n", " ", -1) + desc = strings.Replace(desc, "\r", " ", -1) + if !opts.noTrunc { + desc = stringutils.Ellipsis(desc, 45) + } + + fmt.Fprintf(w, "%s\t%s\t%s\t%v\n", p.Name, p.Tag, desc, p.Enabled) + } + w.Flush() + return nil +} diff --git a/components/cli/command/plugin/push.go b/components/cli/command/plugin/push.go new file mode 100644 index 0000000000..9ef4907961 --- /dev/null +++ b/components/cli/command/plugin/push.go @@ -0,0 +1,55 @@ +// +build experimental + +package plugin + +import ( + "fmt" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/client" + "github.com/docker/docker/cli" + "github.com/docker/docker/reference" + "github.com/docker/docker/registry" + "github.com/spf13/cobra" +) + +func newPushCommand(dockerCli *client.DockerCli) *cobra.Command { + cmd := &cobra.Command{ + Use: "push PLUGIN", + Short: "Push a plugin", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runPush(dockerCli, args[0]) + }, + } + return cmd +} + +func runPush(dockerCli *client.DockerCli, name string) error { + named, err := reference.ParseNamed(name) // FIXME: validate + if err != nil { + return err + } + if reference.IsNameOnly(named) { + named = reference.WithDefaultTag(named) + } + ref, ok := named.(reference.NamedTagged) + if !ok { + return fmt.Errorf("invalid name: %s", named.String()) + } + + ctx := context.Background() + + repoInfo, err := registry.ParseRepositoryInfo(named) + if err != nil { + return err + } + authConfig := dockerCli.ResolveAuthConfig(ctx, repoInfo.Index) + + encodedAuth, err := client.EncodeAuthToBase64(authConfig) + if err != nil { + return err + } + return dockerCli.Client().PluginPush(ctx, ref.String(), encodedAuth) +} diff --git a/components/cli/command/plugin/remove.go b/components/cli/command/plugin/remove.go new file mode 100644 index 0000000000..3b61374009 --- /dev/null +++ b/components/cli/command/plugin/remove.go @@ -0,0 +1,69 @@ +// +build experimental + +package plugin + +import ( + "fmt" + + "github.com/docker/docker/api/client" + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli" + "github.com/docker/docker/reference" + "github.com/spf13/cobra" + "golang.org/x/net/context" +) + +type rmOptions struct { + force bool + + plugins []string +} + +func newRemoveCommand(dockerCli *client.DockerCli) *cobra.Command { + var opts rmOptions + + cmd := &cobra.Command{ + Use: "rm [OPTIONS] PLUGIN [PLUGIN...]", + Short: "Remove one or more plugins", + Aliases: []string{"remove"}, + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.plugins = args + return runRemove(dockerCli, &opts) + }, + } + + flags := cmd.Flags() + flags.BoolVarP(&opts.force, "force", "f", false, "Force the removal of an active plugin") + return cmd +} + +func runRemove(dockerCli *client.DockerCli, opts *rmOptions) error { + ctx := context.Background() + + var errs cli.Errors + for _, name := range opts.plugins { + named, err := reference.ParseNamed(name) // FIXME: validate + if err != nil { + return err + } + if reference.IsNameOnly(named) { + named = reference.WithDefaultTag(named) + } + ref, ok := named.(reference.NamedTagged) + if !ok { + return fmt.Errorf("invalid name: %s", named.String()) + } + // TODO: pass names to api instead of making multiple api calls + if err := dockerCli.Client().PluginRemove(ctx, ref.String(), types.PluginRemoveOptions{Force: opts.force}); err != nil { + errs = append(errs, err) + continue + } + fmt.Fprintln(dockerCli.Out(), name) + } + // Do not simplify to `return errs` because even if errs == nil, it is not a nil-error interface value. + if errs != nil { + return errs + } + return nil +} diff --git a/components/cli/command/plugin/set.go b/components/cli/command/plugin/set.go new file mode 100644 index 0000000000..188bd63cc4 --- /dev/null +++ b/components/cli/command/plugin/set.go @@ -0,0 +1,42 @@ +// +build experimental + +package plugin + +import ( + "fmt" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/client" + "github.com/docker/docker/cli" + "github.com/docker/docker/reference" + "github.com/spf13/cobra" +) + +func newSetCommand(dockerCli *client.DockerCli) *cobra.Command { + cmd := &cobra.Command{ + Use: "set PLUGIN key1=value1 [key2=value2...]", + Short: "Change settings for a plugin", + Args: cli.RequiresMinArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + return runSet(dockerCli, args[0], args[1:]) + }, + } + + return cmd +} + +func runSet(dockerCli *client.DockerCli, name string, args []string) error { + named, err := reference.ParseNamed(name) // FIXME: validate + if err != nil { + return err + } + if reference.IsNameOnly(named) { + named = reference.WithDefaultTag(named) + } + ref, ok := named.(reference.NamedTagged) + if !ok { + return fmt.Errorf("invalid name: %s", named.String()) + } + return dockerCli.Client().PluginSet(context.Background(), ref.String(), args) +} diff --git a/components/cli/command/registry.go b/components/cli/command/registry.go new file mode 100644 index 0000000000..4f72afa4a1 --- /dev/null +++ b/components/cli/command/registry.go @@ -0,0 +1,193 @@ +package command + +import ( + "bufio" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "os" + "runtime" + "strings" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + registrytypes "github.com/docker/docker/api/types/registry" + "github.com/docker/docker/pkg/term" + "github.com/docker/docker/reference" + "github.com/docker/docker/registry" +) + +// ElectAuthServer returns the default registry to use (by asking the daemon) +func (cli *DockerCli) ElectAuthServer(ctx context.Context) string { + // The daemon `/info` endpoint informs us of the default registry being + // used. This is essential in cross-platforms environment, where for + // example a Linux client might be interacting with a Windows daemon, hence + // the default registry URL might be Windows specific. + serverAddress := registry.IndexServer + if info, err := cli.client.Info(ctx); err != nil { + fmt.Fprintf(cli.out, "Warning: failed to get default registry endpoint from daemon (%v). Using system default: %s\n", err, serverAddress) + } else { + serverAddress = info.IndexServerAddress + } + return serverAddress +} + +// EncodeAuthToBase64 serializes the auth configuration as JSON base64 payload +func EncodeAuthToBase64(authConfig types.AuthConfig) (string, error) { + buf, err := json.Marshal(authConfig) + if err != nil { + return "", err + } + return base64.URLEncoding.EncodeToString(buf), nil +} + +// RegistryAuthenticationPrivilegedFunc returns a RequestPrivilegeFunc from the specified registry index info +// for the given command. +func (cli *DockerCli) RegistryAuthenticationPrivilegedFunc(index *registrytypes.IndexInfo, cmdName string) types.RequestPrivilegeFunc { + return func() (string, error) { + fmt.Fprintf(cli.out, "\nPlease login prior to %s:\n", cmdName) + indexServer := registry.GetAuthConfigKey(index) + isDefaultRegistry := indexServer == cli.ElectAuthServer(context.Background()) + authConfig, err := cli.ConfigureAuth("", "", indexServer, isDefaultRegistry) + if err != nil { + return "", err + } + return EncodeAuthToBase64(authConfig) + } +} + +func (cli *DockerCli) promptWithDefault(prompt string, configDefault string) { + if configDefault == "" { + fmt.Fprintf(cli.out, "%s: ", prompt) + } else { + fmt.Fprintf(cli.out, "%s (%s): ", prompt, configDefault) + } +} + +// ResolveAuthConfig is like registry.ResolveAuthConfig, but if using the +// default index, it uses the default index name for the daemon's platform, +// not the client's platform. +func (cli *DockerCli) ResolveAuthConfig(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig { + configKey := index.Name + if index.Official { + configKey = cli.ElectAuthServer(ctx) + } + + a, _ := GetCredentials(cli.configFile, configKey) + return a +} + +// RetrieveAuthConfigs return all credentials. +func (cli *DockerCli) RetrieveAuthConfigs() map[string]types.AuthConfig { + acs, _ := GetAllCredentials(cli.configFile) + return acs +} + +// ConfigureAuth returns an AuthConfig from the specified user, password and server. +func (cli *DockerCli) ConfigureAuth(flUser, flPassword, serverAddress string, isDefaultRegistry bool) (types.AuthConfig, error) { + // On Windows, force the use of the regular OS stdin stream. Fixes #14336/#14210 + if runtime.GOOS == "windows" { + cli.in = NewInStream(os.Stdin) + } + + if !isDefaultRegistry { + serverAddress = registry.ConvertToHostname(serverAddress) + } + + authconfig, err := GetCredentials(cli.configFile, serverAddress) + if err != nil { + return authconfig, err + } + + // Some links documenting this: + // - https://code.google.com/archive/p/mintty/issues/56 + // - https://github.com/docker/docker/issues/15272 + // - https://mintty.github.io/ (compatibility) + // Linux will hit this if you attempt `cat | docker login`, and Windows + // will hit this if you attempt docker login from mintty where stdin + // is a pipe, not a character based console. + if flPassword == "" && !cli.In().IsTerminal() { + return authconfig, fmt.Errorf("Error: Cannot perform an interactive login from a non TTY device") + } + + authconfig.Username = strings.TrimSpace(authconfig.Username) + + if flUser = strings.TrimSpace(flUser); flUser == "" { + if isDefaultRegistry { + // if this is a default registry (docker hub), then display the following message. + fmt.Fprintln(cli.out, "Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.") + } + cli.promptWithDefault("Username", authconfig.Username) + flUser = readInput(cli.in, cli.out) + flUser = strings.TrimSpace(flUser) + if flUser == "" { + flUser = authconfig.Username + } + } + if flUser == "" { + return authconfig, fmt.Errorf("Error: Non-null Username Required") + } + if flPassword == "" { + oldState, err := term.SaveState(cli.In().FD()) + if err != nil { + return authconfig, err + } + fmt.Fprintf(cli.out, "Password: ") + term.DisableEcho(cli.In().FD(), oldState) + + flPassword = readInput(cli.in, cli.out) + fmt.Fprint(cli.out, "\n") + + term.RestoreTerminal(cli.In().FD(), oldState) + if flPassword == "" { + return authconfig, fmt.Errorf("Error: Password Required") + } + } + + authconfig.Username = flUser + authconfig.Password = flPassword + authconfig.ServerAddress = serverAddress + authconfig.IdentityToken = "" + + return authconfig, nil +} + +// resolveAuthConfigFromImage retrieves that AuthConfig using the image string +func (cli *DockerCli) resolveAuthConfigFromImage(ctx context.Context, image string) (types.AuthConfig, error) { + registryRef, err := reference.ParseNamed(image) + if err != nil { + return types.AuthConfig{}, err + } + repoInfo, err := registry.ParseRepositoryInfo(registryRef) + if err != nil { + return types.AuthConfig{}, err + } + authConfig := cli.ResolveAuthConfig(ctx, repoInfo.Index) + return authConfig, nil +} + +// RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete image +func (cli *DockerCli) RetrieveAuthTokenFromImage(ctx context.Context, image string) (string, error) { + // Retrieve encoded auth token from the image reference + authConfig, err := cli.resolveAuthConfigFromImage(ctx, image) + if err != nil { + return "", err + } + encodedAuth, err := EncodeAuthToBase64(authConfig) + if err != nil { + return "", err + } + return encodedAuth, nil +} + +func readInput(in io.Reader, out io.Writer) string { + reader := bufio.NewReader(in) + line, _, err := reader.ReadLine() + if err != nil { + fmt.Fprintln(out, err.Error()) + os.Exit(1) + } + return string(line) +} diff --git a/components/cli/command/registry/login.go b/components/cli/command/registry/login.go new file mode 100644 index 0000000000..dccf538474 --- /dev/null +++ b/components/cli/command/registry/login.go @@ -0,0 +1,85 @@ +package registry + +import ( + "fmt" + + "golang.org/x/net/context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +type loginOptions struct { + serverAddress string + user string + password string + email string +} + +// NewLoginCommand creates a new `docker login` command +func NewLoginCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts loginOptions + + cmd := &cobra.Command{ + Use: "login [OPTIONS] [SERVER]", + Short: "Log in to a Docker registry.", + Long: "Log in to a Docker registry.\nIf no server is specified, the default is defined by the daemon.", + Args: cli.RequiresMaxArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + opts.serverAddress = args[0] + } + return runLogin(dockerCli, opts) + }, + } + + flags := cmd.Flags() + + flags.StringVarP(&opts.user, "username", "u", "", "Username") + flags.StringVarP(&opts.password, "password", "p", "", "Password") + + // Deprecated in 1.11: Should be removed in docker 1.13 + flags.StringVarP(&opts.email, "email", "e", "", "Email") + flags.MarkDeprecated("email", "will be removed in 1.13.") + + return cmd +} + +func runLogin(dockerCli *command.DockerCli, opts loginOptions) error { + ctx := context.Background() + clnt := dockerCli.Client() + + var ( + serverAddress string + authServer = dockerCli.ElectAuthServer(ctx) + ) + if opts.serverAddress != "" { + serverAddress = opts.serverAddress + } else { + serverAddress = authServer + } + + isDefaultRegistry := serverAddress == authServer + + authConfig, err := dockerCli.ConfigureAuth(opts.user, opts.password, serverAddress, isDefaultRegistry) + if err != nil { + return err + } + response, err := clnt.RegistryLogin(ctx, authConfig) + if err != nil { + return err + } + if response.IdentityToken != "" { + authConfig.Password = "" + authConfig.IdentityToken = response.IdentityToken + } + if err := command.StoreCredentials(dockerCli.ConfigFile(), authConfig); err != nil { + return fmt.Errorf("Error saving credentials: %v", err) + } + + if response.Status != "" { + fmt.Fprintln(dockerCli.Out(), response.Status) + } + return nil +} diff --git a/components/cli/command/registry/logout.go b/components/cli/command/registry/logout.go new file mode 100644 index 0000000000..1e0c5170a6 --- /dev/null +++ b/components/cli/command/registry/logout.go @@ -0,0 +1,77 @@ +package registry + +import ( + "fmt" + + "golang.org/x/net/context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/registry" + "github.com/spf13/cobra" +) + +// NewLogoutCommand creates a new `docker login` command +func NewLogoutCommand(dockerCli *command.DockerCli) *cobra.Command { + cmd := &cobra.Command{ + Use: "logout [SERVER]", + Short: "Log out from a Docker registry.", + Long: "Log out from a Docker registry.\nIf no server is specified, the default is defined by the daemon.", + Args: cli.RequiresMaxArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + var serverAddress string + if len(args) > 0 { + serverAddress = args[0] + } + return runLogout(dockerCli, serverAddress) + }, + } + + return cmd +} + +func runLogout(dockerCli *command.DockerCli, serverAddress string) error { + ctx := context.Background() + var isDefaultRegistry bool + + if serverAddress == "" { + serverAddress = dockerCli.ElectAuthServer(ctx) + isDefaultRegistry = true + } + + var ( + loggedIn bool + regsToLogout []string + hostnameAddress = serverAddress + regsToTry = []string{serverAddress} + ) + if !isDefaultRegistry { + hostnameAddress = registry.ConvertToHostname(serverAddress) + // the tries below are kept for backward compatibily where a user could have + // saved the registry in one of the following format. + regsToTry = append(regsToTry, hostnameAddress, "http://"+hostnameAddress, "https://"+hostnameAddress) + } + + // check if we're logged in based on the records in the config file + // which means it couldn't have user/pass cause they may be in the creds store + for _, s := range regsToTry { + if _, ok := dockerCli.ConfigFile().AuthConfigs[s]; ok { + loggedIn = true + regsToLogout = append(regsToLogout, s) + } + } + + if !loggedIn { + fmt.Fprintf(dockerCli.Out(), "Not logged in to %s\n", hostnameAddress) + return nil + } + + fmt.Fprintf(dockerCli.Out(), "Removing login credentials for %s\n", hostnameAddress) + for _, r := range regsToLogout { + if err := command.EraseCredentials(dockerCli.ConfigFile(), r); err != nil { + fmt.Fprintf(dockerCli.Err(), "WARNING: could not erase credentials: %v\n", err) + } + } + + return nil +} diff --git a/components/cli/command/service/cmd.go b/components/cli/command/service/cmd.go new file mode 100644 index 0000000000..282ce2b4b9 --- /dev/null +++ b/components/cli/command/service/cmd.go @@ -0,0 +1,32 @@ +package service + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" +) + +// NewServiceCommand returns a cobra command for `service` subcommands +func NewServiceCommand(dockerCli *command.DockerCli) *cobra.Command { + cmd := &cobra.Command{ + Use: "service", + Short: "Manage Docker services", + Args: cli.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + }, + } + cmd.AddCommand( + newCreateCommand(dockerCli), + newInspectCommand(dockerCli), + newPsCommand(dockerCli), + newListCommand(dockerCli), + newRemoveCommand(dockerCli), + newScaleCommand(dockerCli), + newUpdateCommand(dockerCli), + ) + return cmd +} diff --git a/components/cli/command/service/create.go b/components/cli/command/service/create.go new file mode 100644 index 0000000000..4ec8835b37 --- /dev/null +++ b/components/cli/command/service/create.go @@ -0,0 +1,72 @@ +package service + +import ( + "fmt" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" + "golang.org/x/net/context" +) + +func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { + opts := newServiceOptions() + + cmd := &cobra.Command{ + Use: "create [OPTIONS] IMAGE [COMMAND] [ARG...]", + Short: "Create a new service", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.image = args[0] + if len(args) > 1 { + opts.args = args[1:] + } + return runCreate(dockerCli, opts) + }, + } + flags := cmd.Flags() + flags.StringVar(&opts.mode, flagMode, "replicated", "Service mode (replicated or global)") + addServiceFlags(cmd, opts) + + flags.VarP(&opts.labels, flagLabel, "l", "Service labels") + flags.Var(&opts.containerLabels, flagContainerLabel, "Container labels") + flags.VarP(&opts.env, flagEnv, "e", "Set environment variables") + flags.Var(&opts.mounts, flagMount, "Attach a mount to the service") + flags.StringSliceVar(&opts.constraints, flagConstraint, []string{}, "Placement constraints") + flags.StringSliceVar(&opts.networks, flagNetwork, []string{}, "Network attachments") + flags.VarP(&opts.endpoint.ports, flagPublish, "p", "Publish a port as a node port") + + flags.SetInterspersed(false) + return cmd +} + +func runCreate(dockerCli *command.DockerCli, opts *serviceOptions) error { + apiClient := dockerCli.Client() + createOpts := types.ServiceCreateOptions{} + + service, err := opts.ToService() + if err != nil { + return err + } + + ctx := context.Background() + + // only send auth if flag was set + if opts.registryAuth { + // Retrieve encoded auth token from the image reference + encodedAuth, err := dockerCli.RetrieveAuthTokenFromImage(ctx, opts.image) + if err != nil { + return err + } + createOpts.EncodedRegistryAuth = encodedAuth + } + + response, err := apiClient.ServiceCreate(ctx, service, createOpts) + if err != nil { + return err + } + + fmt.Fprintf(dockerCli.Out(), "%s\n", response.ID) + return nil +} diff --git a/components/cli/command/service/inspect.go b/components/cli/command/service/inspect.go new file mode 100644 index 0000000000..8facb1f28b --- /dev/null +++ b/components/cli/command/service/inspect.go @@ -0,0 +1,188 @@ +package service + +import ( + "fmt" + "io" + "strings" + "time" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/inspect" + apiclient "github.com/docker/docker/client" + "github.com/docker/docker/pkg/ioutils" + "github.com/docker/go-units" + "github.com/spf13/cobra" +) + +type inspectOptions struct { + refs []string + format string + pretty bool +} + +func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts inspectOptions + + cmd := &cobra.Command{ + Use: "inspect [OPTIONS] SERVICE [SERVICE...]", + Short: "Display detailed information on one or more services", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.refs = args + + if opts.pretty && len(opts.format) > 0 { + return fmt.Errorf("--format is incompatible with human friendly format") + } + return runInspect(dockerCli, opts) + }, + } + + flags := cmd.Flags() + flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template") + flags.BoolVar(&opts.pretty, "pretty", false, "Print the information in a human friendly format.") + return cmd +} + +func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { + client := dockerCli.Client() + ctx := context.Background() + + getRef := func(ref string) (interface{}, []byte, error) { + service, _, err := client.ServiceInspectWithRaw(ctx, ref) + if err == nil || !apiclient.IsErrServiceNotFound(err) { + return service, nil, err + } + return nil, nil, fmt.Errorf("Error: no such service: %s", ref) + } + + if !opts.pretty { + return inspect.Inspect(dockerCli.Out(), opts.refs, opts.format, getRef) + } + + return printHumanFriendly(dockerCli.Out(), opts.refs, getRef) +} + +func printHumanFriendly(out io.Writer, refs []string, getRef inspect.GetRefFunc) error { + for idx, ref := range refs { + obj, _, err := getRef(ref) + if err != nil { + return err + } + printService(out, obj.(swarm.Service)) + + // TODO: better way to do this? + // print extra space between objects, but not after the last one + if idx+1 != len(refs) { + fmt.Fprintf(out, "\n\n") + } + } + return nil +} + +// TODO: use a template +func printService(out io.Writer, service swarm.Service) { + fmt.Fprintf(out, "ID:\t\t%s\n", service.ID) + fmt.Fprintf(out, "Name:\t\t%s\n", service.Spec.Name) + if service.Spec.Labels != nil { + fmt.Fprintln(out, "Labels:") + for k, v := range service.Spec.Labels { + fmt.Fprintf(out, " - %s=%s\n", k, v) + } + } + + if service.Spec.Mode.Global != nil { + fmt.Fprintln(out, "Mode:\t\tGlobal") + } else { + fmt.Fprintln(out, "Mode:\t\tReplicated") + if service.Spec.Mode.Replicated.Replicas != nil { + fmt.Fprintf(out, " Replicas:\t%d\n", *service.Spec.Mode.Replicated.Replicas) + } + } + + if service.UpdateStatus.State != "" { + fmt.Fprintln(out, "Update status:") + fmt.Fprintf(out, " State:\t\t%s\n", service.UpdateStatus.State) + fmt.Fprintf(out, " Started:\t%s ago\n", strings.ToLower(units.HumanDuration(time.Since(service.UpdateStatus.StartedAt)))) + if service.UpdateStatus.State == swarm.UpdateStateCompleted { + fmt.Fprintf(out, " Completed:\t%s ago\n", strings.ToLower(units.HumanDuration(time.Since(service.UpdateStatus.CompletedAt)))) + } + fmt.Fprintf(out, " Message:\t%s\n", service.UpdateStatus.Message) + } + + fmt.Fprintln(out, "Placement:") + if service.Spec.TaskTemplate.Placement != nil && len(service.Spec.TaskTemplate.Placement.Constraints) > 0 { + ioutils.FprintfIfNotEmpty(out, " Constraints\t: %s\n", strings.Join(service.Spec.TaskTemplate.Placement.Constraints, ", ")) + } + if service.Spec.UpdateConfig != nil { + fmt.Fprintf(out, "UpdateConfig:\n") + fmt.Fprintf(out, " Parallelism:\t%d\n", service.Spec.UpdateConfig.Parallelism) + if service.Spec.UpdateConfig.Delay.Nanoseconds() > 0 { + fmt.Fprintf(out, " Delay:\t\t%s\n", service.Spec.UpdateConfig.Delay) + } + fmt.Fprintf(out, " On failure:\t%s\n", service.Spec.UpdateConfig.FailureAction) + } + + fmt.Fprintf(out, "ContainerSpec:\n") + printContainerSpec(out, service.Spec.TaskTemplate.ContainerSpec) + + resources := service.Spec.TaskTemplate.Resources + if resources != nil { + fmt.Fprintln(out, "Resources:") + printResources := func(out io.Writer, requirement string, r *swarm.Resources) { + if r == nil || (r.MemoryBytes == 0 && r.NanoCPUs == 0) { + return + } + fmt.Fprintf(out, " %s:\n", requirement) + if r.NanoCPUs != 0 { + fmt.Fprintf(out, " CPU:\t\t%g\n", float64(r.NanoCPUs)/1e9) + } + if r.MemoryBytes != 0 { + fmt.Fprintf(out, " Memory:\t%s\n", units.BytesSize(float64(r.MemoryBytes))) + } + } + printResources(out, "Reservations", resources.Reservations) + printResources(out, "Limits", resources.Limits) + } + if len(service.Spec.Networks) > 0 { + fmt.Fprintf(out, "Networks:") + for _, n := range service.Spec.Networks { + fmt.Fprintf(out, " %s", n.Target) + } + fmt.Fprintln(out, "") + } + + if len(service.Endpoint.Ports) > 0 { + fmt.Fprintln(out, "Ports:") + for _, port := range service.Endpoint.Ports { + ioutils.FprintfIfNotEmpty(out, " Name = %s\n", port.Name) + fmt.Fprintf(out, " Protocol = %s\n", port.Protocol) + fmt.Fprintf(out, " TargetPort = %d\n", port.TargetPort) + fmt.Fprintf(out, " PublishedPort = %d\n", port.PublishedPort) + } + } +} + +func printContainerSpec(out io.Writer, containerSpec swarm.ContainerSpec) { + fmt.Fprintf(out, " Image:\t\t%s\n", containerSpec.Image) + if len(containerSpec.Args) > 0 { + fmt.Fprintf(out, " Args:\t\t%s\n", strings.Join(containerSpec.Args, " ")) + } + if len(containerSpec.Env) > 0 { + fmt.Fprintf(out, " Env:\t\t%s\n", strings.Join(containerSpec.Env, " ")) + } + ioutils.FprintfIfNotEmpty(out, " Dir\t\t%s\n", containerSpec.Dir) + ioutils.FprintfIfNotEmpty(out, " User\t\t%s\n", containerSpec.User) + if len(containerSpec.Mounts) > 0 { + fmt.Fprintln(out, " Mounts:") + for _, v := range containerSpec.Mounts { + fmt.Fprintf(out, " Target = %s\n", v.Target) + fmt.Fprintf(out, " Source = %s\n", v.Source) + fmt.Fprintf(out, " ReadOnly = %v\n", v.ReadOnly) + fmt.Fprintf(out, " Type = %v\n", v.Type) + } + } +} diff --git a/components/cli/command/service/inspect_test.go b/components/cli/command/service/inspect_test.go new file mode 100644 index 0000000000..0e0f2ae74f --- /dev/null +++ b/components/cli/command/service/inspect_test.go @@ -0,0 +1,84 @@ +package service + +import ( + "bytes" + "strings" + "testing" + "time" + + "github.com/docker/docker/api/types/swarm" +) + +func TestPrettyPrintWithNoUpdateConfig(t *testing.T) { + b := new(bytes.Buffer) + + endpointSpec := &swarm.EndpointSpec{ + Mode: "vip", + Ports: []swarm.PortConfig{ + { + Protocol: swarm.PortConfigProtocolTCP, + TargetPort: 5000, + }, + }, + } + + two := uint64(2) + + s := swarm.Service{ + ID: "de179gar9d0o7ltdybungplod", + Meta: swarm.Meta{ + Version: swarm.Version{Index: 315}, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: "my_service", + Labels: map[string]string{"com.label": "foo"}, + }, + TaskTemplate: swarm.TaskSpec{ + ContainerSpec: swarm.ContainerSpec{ + Image: "foo/bar@sha256:this_is_a_test", + }, + }, + Mode: swarm.ServiceMode{ + Replicated: &swarm.ReplicatedService{ + Replicas: &two, + }, + }, + UpdateConfig: nil, + Networks: []swarm.NetworkAttachmentConfig{ + { + Target: "5vpyomhb6ievnk0i0o60gcnei", + Aliases: []string{"web"}, + }, + }, + EndpointSpec: endpointSpec, + }, + Endpoint: swarm.Endpoint{ + Spec: *endpointSpec, + Ports: []swarm.PortConfig{ + { + Protocol: swarm.PortConfigProtocolTCP, + TargetPort: 5000, + PublishedPort: 30000, + }, + }, + VirtualIPs: []swarm.EndpointVirtualIP{ + { + NetworkID: "6o4107cj2jx9tihgb0jyts6pj", + Addr: "10.255.0.4/16", + }, + }, + }, + UpdateStatus: swarm.UpdateStatus{ + StartedAt: time.Now(), + CompletedAt: time.Now(), + }, + } + + printService(b, s) + if strings.Contains(b.String(), "UpdateStatus") { + t.Fatal("Pretty print failed before parsing UpdateStatus") + } +} diff --git a/components/cli/command/service/list.go b/components/cli/command/service/list.go new file mode 100644 index 0000000000..681acd3f25 --- /dev/null +++ b/components/cli/command/service/list.go @@ -0,0 +1,133 @@ +package service + +import ( + "fmt" + "io" + "strings" + "text/tabwriter" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/opts" + "github.com/docker/docker/pkg/stringid" + "github.com/spf13/cobra" + "golang.org/x/net/context" +) + +const ( + listItemFmt = "%s\t%s\t%s\t%s\t%s\n" +) + +type listOptions struct { + quiet bool + filter opts.FilterOpt +} + +func newListCommand(dockerCli *command.DockerCli) *cobra.Command { + opts := listOptions{filter: opts.NewFilterOpt()} + + cmd := &cobra.Command{ + Use: "ls [OPTIONS]", + Aliases: []string{"list"}, + Short: "List services", + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runList(dockerCli, opts) + }, + } + + flags := cmd.Flags() + flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs") + flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") + + return cmd +} + +func runList(dockerCli *command.DockerCli, opts listOptions) error { + ctx := context.Background() + client := dockerCli.Client() + + services, err := client.ServiceList(ctx, types.ServiceListOptions{Filter: opts.filter.Value()}) + if err != nil { + return err + } + + out := dockerCli.Out() + if opts.quiet { + PrintQuiet(out, services) + } else { + taskFilter := filters.NewArgs() + for _, service := range services { + taskFilter.Add("service", service.ID) + } + + tasks, err := client.TaskList(ctx, types.TaskListOptions{Filter: taskFilter}) + if err != nil { + return err + } + + nodes, err := client.NodeList(ctx, types.NodeListOptions{}) + if err != nil { + return err + } + + PrintNotQuiet(out, services, nodes, tasks) + } + return nil +} + +// PrintNotQuiet shows service list in a non-quiet way. +// Besides this, command `docker stack services xxx` will call this, too. +func PrintNotQuiet(out io.Writer, services []swarm.Service, nodes []swarm.Node, tasks []swarm.Task) { + activeNodes := make(map[string]struct{}) + for _, n := range nodes { + if n.Status.State != swarm.NodeStateDown { + activeNodes[n.ID] = struct{}{} + } + } + + running := map[string]int{} + for _, task := range tasks { + if _, nodeActive := activeNodes[task.NodeID]; nodeActive && task.Status.State == "running" { + running[task.ServiceID]++ + } + } + + printTable(out, services, running) +} + +func printTable(out io.Writer, services []swarm.Service, running map[string]int) { + writer := tabwriter.NewWriter(out, 0, 4, 2, ' ', 0) + + // Ignore flushing errors + defer writer.Flush() + + fmt.Fprintf(writer, listItemFmt, "ID", "NAME", "REPLICAS", "IMAGE", "COMMAND") + for _, service := range services { + replicas := "" + if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil { + replicas = fmt.Sprintf("%d/%d", running[service.ID], *service.Spec.Mode.Replicated.Replicas) + } else if service.Spec.Mode.Global != nil { + replicas = "global" + } + fmt.Fprintf( + writer, + listItemFmt, + stringid.TruncateID(service.ID), + service.Spec.Name, + replicas, + service.Spec.TaskTemplate.ContainerSpec.Image, + strings.Join(service.Spec.TaskTemplate.ContainerSpec.Args, " ")) + } +} + +// PrintQuiet shows service list in a quiet way. +// Besides this, command `docker stack services xxx` will call this, too. +func PrintQuiet(out io.Writer, services []swarm.Service) { + for _, service := range services { + fmt.Fprintln(out, service.ID) + } +} diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go new file mode 100644 index 0000000000..7236980e80 --- /dev/null +++ b/components/cli/command/service/opts.go @@ -0,0 +1,567 @@ +package service + +import ( + "encoding/csv" + "fmt" + "math/big" + "strconv" + "strings" + "time" + + mounttypes "github.com/docker/docker/api/types/mount" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/opts" + runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/docker/go-connections/nat" + units "github.com/docker/go-units" + "github.com/spf13/cobra" +) + +type int64Value interface { + Value() int64 +} + +type memBytes int64 + +func (m *memBytes) String() string { + return units.BytesSize(float64(m.Value())) +} + +func (m *memBytes) Set(value string) error { + val, err := units.RAMInBytes(value) + *m = memBytes(val) + return err +} + +func (m *memBytes) Type() string { + return "MemoryBytes" +} + +func (m *memBytes) Value() int64 { + return int64(*m) +} + +type nanoCPUs int64 + +func (c *nanoCPUs) String() string { + return big.NewRat(c.Value(), 1e9).FloatString(3) +} + +func (c *nanoCPUs) Set(value string) error { + cpu, ok := new(big.Rat).SetString(value) + if !ok { + return fmt.Errorf("Failed to parse %v as a rational number", value) + } + nano := cpu.Mul(cpu, big.NewRat(1e9, 1)) + if !nano.IsInt() { + return fmt.Errorf("value is too precise") + } + *c = nanoCPUs(nano.Num().Int64()) + return nil +} + +func (c *nanoCPUs) Type() string { + return "NanoCPUs" +} + +func (c *nanoCPUs) Value() int64 { + return int64(*c) +} + +// DurationOpt is an option type for time.Duration that uses a pointer. This +// allows us to get nil values outside, instead of defaulting to 0 +type DurationOpt struct { + value *time.Duration +} + +// Set a new value on the option +func (d *DurationOpt) Set(s string) error { + v, err := time.ParseDuration(s) + d.value = &v + return err +} + +// Type returns the type of this option +func (d *DurationOpt) Type() string { + return "duration-ptr" +} + +// String returns a string repr of this option +func (d *DurationOpt) String() string { + if d.value != nil { + return d.value.String() + } + return "none" +} + +// Value returns the time.Duration +func (d *DurationOpt) Value() *time.Duration { + return d.value +} + +// Uint64Opt represents a uint64. +type Uint64Opt struct { + value *uint64 +} + +// Set a new value on the option +func (i *Uint64Opt) Set(s string) error { + v, err := strconv.ParseUint(s, 0, 64) + i.value = &v + return err +} + +// Type returns the type of this option +func (i *Uint64Opt) Type() string { + return "uint64-ptr" +} + +// String returns a string repr of this option +func (i *Uint64Opt) String() string { + if i.value != nil { + return fmt.Sprintf("%v", *i.value) + } + return "none" +} + +// Value returns the uint64 +func (i *Uint64Opt) Value() *uint64 { + return i.value +} + +// MountOpt is a Value type for parsing mounts +type MountOpt struct { + values []mounttypes.Mount +} + +// Set a new mount value +func (m *MountOpt) Set(value string) error { + csvReader := csv.NewReader(strings.NewReader(value)) + fields, err := csvReader.Read() + if err != nil { + return err + } + + mount := mounttypes.Mount{} + + volumeOptions := func() *mounttypes.VolumeOptions { + if mount.VolumeOptions == nil { + mount.VolumeOptions = &mounttypes.VolumeOptions{ + Labels: make(map[string]string), + } + } + if mount.VolumeOptions.DriverConfig == nil { + mount.VolumeOptions.DriverConfig = &mounttypes.Driver{} + } + return mount.VolumeOptions + } + + bindOptions := func() *mounttypes.BindOptions { + if mount.BindOptions == nil { + mount.BindOptions = new(mounttypes.BindOptions) + } + return mount.BindOptions + } + + setValueOnMap := func(target map[string]string, value string) { + parts := strings.SplitN(value, "=", 2) + if len(parts) == 1 { + target[value] = "" + } else { + target[parts[0]] = parts[1] + } + } + + mount.Type = mounttypes.TypeVolume // default to volume mounts + // Set writable as the default + for _, field := range fields { + parts := strings.SplitN(field, "=", 2) + key := strings.ToLower(parts[0]) + + if len(parts) == 1 { + switch key { + case "readonly", "ro": + mount.ReadOnly = true + continue + case "volume-nocopy": + volumeOptions().NoCopy = true + continue + } + } + + if len(parts) != 2 { + return fmt.Errorf("invalid field '%s' must be a key=value pair", field) + } + + value := parts[1] + switch key { + case "type": + mount.Type = mounttypes.Type(strings.ToLower(value)) + case "source", "src": + mount.Source = value + case "target", "dst", "destination": + mount.Target = value + case "readonly", "ro": + mount.ReadOnly, err = strconv.ParseBool(value) + if err != nil { + return fmt.Errorf("invalid value for %s: %s", key, value) + } + case "bind-propagation": + bindOptions().Propagation = mounttypes.Propagation(strings.ToLower(value)) + case "volume-nocopy": + volumeOptions().NoCopy, err = strconv.ParseBool(value) + if err != nil { + return fmt.Errorf("invalid value for populate: %s", value) + } + case "volume-label": + setValueOnMap(volumeOptions().Labels, value) + case "volume-driver": + volumeOptions().DriverConfig.Name = value + case "volume-opt": + if volumeOptions().DriverConfig.Options == nil { + volumeOptions().DriverConfig.Options = make(map[string]string) + } + setValueOnMap(volumeOptions().DriverConfig.Options, value) + default: + return fmt.Errorf("unexpected key '%s' in '%s'", key, field) + } + } + + if mount.Type == "" { + return fmt.Errorf("type is required") + } + + if mount.Target == "" { + return fmt.Errorf("target is required") + } + + if mount.VolumeOptions != nil && mount.Source == "" { + return fmt.Errorf("source is required when specifying volume-* options") + } + + if mount.Type == mounttypes.TypeBind && mount.VolumeOptions != nil { + return fmt.Errorf("cannot mix 'volume-*' options with mount type '%s'", mounttypes.TypeBind) + } + if mount.Type == mounttypes.TypeVolume && mount.BindOptions != nil { + return fmt.Errorf("cannot mix 'bind-*' options with mount type '%s'", mounttypes.TypeVolume) + } + + m.values = append(m.values, mount) + return nil +} + +// Type returns the type of this option +func (m *MountOpt) Type() string { + return "mount" +} + +// String returns a string repr of this option +func (m *MountOpt) String() string { + mounts := []string{} + for _, mount := range m.values { + repr := fmt.Sprintf("%s %s %s", mount.Type, mount.Source, mount.Target) + mounts = append(mounts, repr) + } + return strings.Join(mounts, ", ") +} + +// Value returns the mounts +func (m *MountOpt) Value() []mounttypes.Mount { + return m.values +} + +type updateOptions struct { + parallelism uint64 + delay time.Duration + onFailure string +} + +type resourceOptions struct { + limitCPU nanoCPUs + limitMemBytes memBytes + resCPU nanoCPUs + resMemBytes memBytes +} + +func (r *resourceOptions) ToResourceRequirements() *swarm.ResourceRequirements { + return &swarm.ResourceRequirements{ + Limits: &swarm.Resources{ + NanoCPUs: r.limitCPU.Value(), + MemoryBytes: r.limitMemBytes.Value(), + }, + Reservations: &swarm.Resources{ + NanoCPUs: r.resCPU.Value(), + MemoryBytes: r.resMemBytes.Value(), + }, + } +} + +type restartPolicyOptions struct { + condition string + delay DurationOpt + maxAttempts Uint64Opt + window DurationOpt +} + +func (r *restartPolicyOptions) ToRestartPolicy() *swarm.RestartPolicy { + return &swarm.RestartPolicy{ + Condition: swarm.RestartPolicyCondition(r.condition), + Delay: r.delay.Value(), + MaxAttempts: r.maxAttempts.Value(), + Window: r.window.Value(), + } +} + +func convertNetworks(networks []string) []swarm.NetworkAttachmentConfig { + nets := []swarm.NetworkAttachmentConfig{} + for _, network := range networks { + nets = append(nets, swarm.NetworkAttachmentConfig{Target: network}) + } + return nets +} + +type endpointOptions struct { + mode string + ports opts.ListOpts +} + +func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec { + portConfigs := []swarm.PortConfig{} + // We can ignore errors because the format was already validated by ValidatePort + ports, portBindings, _ := nat.ParsePortSpecs(e.ports.GetAll()) + + for port := range ports { + portConfigs = append(portConfigs, convertPortToPortConfig(port, portBindings)...) + } + + return &swarm.EndpointSpec{ + Mode: swarm.ResolutionMode(strings.ToLower(e.mode)), + Ports: portConfigs, + } +} + +func convertPortToPortConfig( + port nat.Port, + portBindings map[nat.Port][]nat.PortBinding, +) []swarm.PortConfig { + ports := []swarm.PortConfig{} + + for _, binding := range portBindings[port] { + hostPort, _ := strconv.ParseUint(binding.HostPort, 10, 16) + ports = append(ports, swarm.PortConfig{ + //TODO Name: ? + Protocol: swarm.PortConfigProtocol(strings.ToLower(port.Proto())), + TargetPort: uint32(port.Int()), + PublishedPort: uint32(hostPort), + }) + } + return ports +} + +type logDriverOptions struct { + name string + opts opts.ListOpts +} + +func newLogDriverOptions() logDriverOptions { + return logDriverOptions{opts: opts.NewListOpts(runconfigopts.ValidateEnv)} +} + +func (ldo *logDriverOptions) toLogDriver() *swarm.Driver { + if ldo.name == "" { + return nil + } + + // set the log driver only if specified. + return &swarm.Driver{ + Name: ldo.name, + Options: runconfigopts.ConvertKVStringsToMap(ldo.opts.GetAll()), + } +} + +// ValidatePort validates a string is in the expected format for a port definition +func ValidatePort(value string) (string, error) { + portMappings, err := nat.ParsePortSpec(value) + for _, portMapping := range portMappings { + if portMapping.Binding.HostIP != "" { + return "", fmt.Errorf("HostIP is not supported by a service.") + } + } + return value, err +} + +type serviceOptions struct { + name string + labels opts.ListOpts + containerLabels opts.ListOpts + image string + args []string + env opts.ListOpts + workdir string + user string + groups []string + mounts MountOpt + + resources resourceOptions + stopGrace DurationOpt + + replicas Uint64Opt + mode string + + restartPolicy restartPolicyOptions + constraints []string + update updateOptions + networks []string + endpoint endpointOptions + + registryAuth bool + + logDriver logDriverOptions +} + +func newServiceOptions() *serviceOptions { + return &serviceOptions{ + labels: opts.NewListOpts(runconfigopts.ValidateEnv), + containerLabels: opts.NewListOpts(runconfigopts.ValidateEnv), + env: opts.NewListOpts(runconfigopts.ValidateEnv), + endpoint: endpointOptions{ + ports: opts.NewListOpts(ValidatePort), + }, + logDriver: newLogDriverOptions(), + } +} + +func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { + var service swarm.ServiceSpec + + service = swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: opts.name, + Labels: runconfigopts.ConvertKVStringsToMap(opts.labels.GetAll()), + }, + TaskTemplate: swarm.TaskSpec{ + ContainerSpec: swarm.ContainerSpec{ + Image: opts.image, + Args: opts.args, + Env: opts.env.GetAll(), + Labels: runconfigopts.ConvertKVStringsToMap(opts.containerLabels.GetAll()), + Dir: opts.workdir, + User: opts.user, + Groups: opts.groups, + Mounts: opts.mounts.Value(), + StopGracePeriod: opts.stopGrace.Value(), + }, + Networks: convertNetworks(opts.networks), + Resources: opts.resources.ToResourceRequirements(), + RestartPolicy: opts.restartPolicy.ToRestartPolicy(), + Placement: &swarm.Placement{ + Constraints: opts.constraints, + }, + LogDriver: opts.logDriver.toLogDriver(), + }, + Networks: convertNetworks(opts.networks), + Mode: swarm.ServiceMode{}, + UpdateConfig: &swarm.UpdateConfig{ + Parallelism: opts.update.parallelism, + Delay: opts.update.delay, + FailureAction: opts.update.onFailure, + }, + EndpointSpec: opts.endpoint.ToEndpointSpec(), + } + + switch opts.mode { + case "global": + if opts.replicas.Value() != nil { + return service, fmt.Errorf("replicas can only be used with replicated mode") + } + + service.Mode.Global = &swarm.GlobalService{} + case "replicated": + service.Mode.Replicated = &swarm.ReplicatedService{ + Replicas: opts.replicas.Value(), + } + default: + return service, fmt.Errorf("Unknown mode: %s", opts.mode) + } + return service, nil +} + +// addServiceFlags adds all flags that are common to both `create` and `update`. +// Any flags that are not common are added separately in the individual command +func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { + flags := cmd.Flags() + flags.StringVar(&opts.name, flagName, "", "Service name") + + flags.StringVarP(&opts.workdir, flagWorkdir, "w", "", "Working directory inside the container") + flags.StringVarP(&opts.user, flagUser, "u", "", "Username or UID (format: [:])") + flags.StringSliceVar(&opts.groups, flagGroupAdd, []string{}, "Add additional user groups to the container") + + flags.Var(&opts.resources.limitCPU, flagLimitCPU, "Limit CPUs") + flags.Var(&opts.resources.limitMemBytes, flagLimitMemory, "Limit Memory") + flags.Var(&opts.resources.resCPU, flagReserveCPU, "Reserve CPUs") + flags.Var(&opts.resources.resMemBytes, flagReserveMemory, "Reserve Memory") + flags.Var(&opts.stopGrace, flagStopGracePeriod, "Time to wait before force killing a container") + + flags.Var(&opts.replicas, flagReplicas, "Number of tasks") + + flags.StringVar(&opts.restartPolicy.condition, flagRestartCondition, "", "Restart when condition is met (none, on-failure, or any)") + flags.Var(&opts.restartPolicy.delay, flagRestartDelay, "Delay between restart attempts") + flags.Var(&opts.restartPolicy.maxAttempts, flagRestartMaxAttempts, "Maximum number of restarts before giving up") + flags.Var(&opts.restartPolicy.window, flagRestartWindow, "Window used to evaluate the restart policy") + + flags.Uint64Var(&opts.update.parallelism, flagUpdateParallelism, 1, "Maximum number of tasks updated simultaneously (0 to update all at once)") + flags.DurationVar(&opts.update.delay, flagUpdateDelay, time.Duration(0), "Delay between updates") + flags.StringVar(&opts.update.onFailure, flagUpdateFailureAction, "pause", "Action on update failure (pause|continue)") + + flags.StringVar(&opts.endpoint.mode, flagEndpointMode, "", "Endpoint mode (vip or dnsrr)") + + flags.BoolVar(&opts.registryAuth, flagRegistryAuth, false, "Send registry authentication details to swarm agents") + + flags.StringVar(&opts.logDriver.name, flagLogDriver, "", "Logging driver for service") + flags.Var(&opts.logDriver.opts, flagLogOpt, "Logging driver options") +} + +const ( + flagConstraint = "constraint" + flagConstraintRemove = "constraint-rm" + flagConstraintAdd = "constraint-add" + flagContainerLabel = "container-label" + flagContainerLabelRemove = "container-label-rm" + flagContainerLabelAdd = "container-label-add" + flagEndpointMode = "endpoint-mode" + flagEnv = "env" + flagEnvRemove = "env-rm" + flagEnvAdd = "env-add" + flagGroupAdd = "group-add" + flagGroupRemove = "group-rm" + flagLabel = "label" + flagLabelRemove = "label-rm" + flagLabelAdd = "label-add" + flagLimitCPU = "limit-cpu" + flagLimitMemory = "limit-memory" + flagMode = "mode" + flagMount = "mount" + flagMountRemove = "mount-rm" + flagMountAdd = "mount-add" + flagName = "name" + flagNetwork = "network" + flagPublish = "publish" + flagPublishRemove = "publish-rm" + flagPublishAdd = "publish-add" + flagReplicas = "replicas" + flagReserveCPU = "reserve-cpu" + flagReserveMemory = "reserve-memory" + flagRestartCondition = "restart-condition" + flagRestartDelay = "restart-delay" + flagRestartMaxAttempts = "restart-max-attempts" + flagRestartWindow = "restart-window" + flagStopGracePeriod = "stop-grace-period" + flagUpdateDelay = "update-delay" + flagUpdateFailureAction = "update-failure-action" + flagUpdateParallelism = "update-parallelism" + flagUser = "user" + flagWorkdir = "workdir" + flagRegistryAuth = "with-registry-auth" + flagLogDriver = "log-driver" + flagLogOpt = "log-opt" +) diff --git a/components/cli/command/service/opts_test.go b/components/cli/command/service/opts_test.go new file mode 100644 index 0000000000..30e261b8de --- /dev/null +++ b/components/cli/command/service/opts_test.go @@ -0,0 +1,176 @@ +package service + +import ( + "testing" + "time" + + mounttypes "github.com/docker/docker/api/types/mount" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestMemBytesString(t *testing.T) { + var mem memBytes = 1048576 + assert.Equal(t, mem.String(), "1 MiB") +} + +func TestMemBytesSetAndValue(t *testing.T) { + var mem memBytes + assert.NilError(t, mem.Set("5kb")) + assert.Equal(t, mem.Value(), int64(5120)) +} + +func TestNanoCPUsString(t *testing.T) { + var cpus nanoCPUs = 6100000000 + assert.Equal(t, cpus.String(), "6.100") +} + +func TestNanoCPUsSetAndValue(t *testing.T) { + var cpus nanoCPUs + assert.NilError(t, cpus.Set("0.35")) + assert.Equal(t, cpus.Value(), int64(350000000)) +} + +func TestDurationOptString(t *testing.T) { + dur := time.Duration(300 * 10e8) + duration := DurationOpt{value: &dur} + assert.Equal(t, duration.String(), "5m0s") +} + +func TestDurationOptSetAndValue(t *testing.T) { + var duration DurationOpt + assert.NilError(t, duration.Set("300s")) + assert.Equal(t, *duration.Value(), time.Duration(300*10e8)) +} + +func TestUint64OptString(t *testing.T) { + value := uint64(2345678) + opt := Uint64Opt{value: &value} + assert.Equal(t, opt.String(), "2345678") + + opt = Uint64Opt{} + assert.Equal(t, opt.String(), "none") +} + +func TestUint64OptSetAndValue(t *testing.T) { + var opt Uint64Opt + assert.NilError(t, opt.Set("14445")) + assert.Equal(t, *opt.Value(), uint64(14445)) +} + +func TestMountOptString(t *testing.T) { + mount := MountOpt{ + values: []mounttypes.Mount{ + { + Type: mounttypes.TypeBind, + Source: "/home/path", + Target: "/target", + }, + { + Type: mounttypes.TypeVolume, + Source: "foo", + Target: "/target/foo", + }, + }, + } + expected := "bind /home/path /target, volume foo /target/foo" + assert.Equal(t, mount.String(), expected) +} + +func TestMountOptSetNoError(t *testing.T) { + for _, testcase := range []string{ + // tests several aliases that should have same result. + "type=bind,target=/target,source=/source", + "type=bind,src=/source,dst=/target", + "type=bind,source=/source,dst=/target", + "type=bind,src=/source,target=/target", + } { + var mount MountOpt + + assert.NilError(t, mount.Set(testcase)) + + mounts := mount.Value() + assert.Equal(t, len(mounts), 1) + assert.Equal(t, mounts[0], mounttypes.Mount{ + Type: mounttypes.TypeBind, + Source: "/source", + Target: "/target", + }) + } +} + +// TestMountOptDefaultType ensures that a mount without the type defaults to a +// volume mount. +func TestMountOptDefaultType(t *testing.T) { + var mount MountOpt + assert.NilError(t, mount.Set("target=/target,source=/foo")) + assert.Equal(t, mount.values[0].Type, mounttypes.TypeVolume) +} + +func TestMountOptSetErrorNoTarget(t *testing.T) { + var mount MountOpt + assert.Error(t, mount.Set("type=volume,source=/foo"), "target is required") +} + +func TestMountOptSetErrorInvalidKey(t *testing.T) { + var mount MountOpt + assert.Error(t, mount.Set("type=volume,bogus=foo"), "unexpected key 'bogus'") +} + +func TestMountOptSetErrorInvalidField(t *testing.T) { + var mount MountOpt + assert.Error(t, mount.Set("type=volume,bogus"), "invalid field 'bogus'") +} + +func TestMountOptSetErrorInvalidReadOnly(t *testing.T) { + var mount MountOpt + assert.Error(t, mount.Set("type=volume,readonly=no"), "invalid value for readonly: no") + assert.Error(t, mount.Set("type=volume,readonly=invalid"), "invalid value for readonly: invalid") +} + +func TestMountOptDefaultEnableReadOnly(t *testing.T) { + var m MountOpt + assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo")) + assert.Equal(t, m.values[0].ReadOnly, false) + + m = MountOpt{} + assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly")) + assert.Equal(t, m.values[0].ReadOnly, true) + + m = MountOpt{} + assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=1")) + assert.Equal(t, m.values[0].ReadOnly, true) + + m = MountOpt{} + assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=0")) + assert.Equal(t, m.values[0].ReadOnly, false) +} + +func TestMountOptVolumeNoCopy(t *testing.T) { + var m MountOpt + assert.Error(t, m.Set("type=volume,target=/foo,volume-nocopy"), "source is required") + + m = MountOpt{} + assert.NilError(t, m.Set("type=volume,target=/foo,source=foo")) + assert.Equal(t, m.values[0].VolumeOptions == nil, true) + + m = MountOpt{} + assert.NilError(t, m.Set("type=volume,target=/foo,source=foo,volume-nocopy=true")) + assert.Equal(t, m.values[0].VolumeOptions != nil, true) + assert.Equal(t, m.values[0].VolumeOptions.NoCopy, true) + + m = MountOpt{} + assert.NilError(t, m.Set("type=volume,target=/foo,source=foo,volume-nocopy")) + assert.Equal(t, m.values[0].VolumeOptions != nil, true) + assert.Equal(t, m.values[0].VolumeOptions.NoCopy, true) + + m = MountOpt{} + assert.NilError(t, m.Set("type=volume,target=/foo,source=foo,volume-nocopy=1")) + assert.Equal(t, m.values[0].VolumeOptions != nil, true) + assert.Equal(t, m.values[0].VolumeOptions.NoCopy, true) +} + +func TestMountOptTypeConflict(t *testing.T) { + var m MountOpt + assert.Error(t, m.Set("type=bind,target=/foo,source=/foo,volume-nocopy=true"), "cannot mix") + assert.Error(t, m.Set("type=volume,target=/foo,source=/foo,bind-propagation=rprivate"), "cannot mix") +} diff --git a/components/cli/command/service/ps.go b/components/cli/command/service/ps.go new file mode 100644 index 0000000000..23c3679d7a --- /dev/null +++ b/components/cli/command/service/ps.go @@ -0,0 +1,71 @@ +package service + +import ( + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/idresolver" + "github.com/docker/docker/cli/command/node" + "github.com/docker/docker/cli/command/task" + "github.com/docker/docker/opts" + "github.com/spf13/cobra" + "golang.org/x/net/context" +) + +type psOptions struct { + serviceID string + noResolve bool + noTrunc bool + filter opts.FilterOpt +} + +func newPsCommand(dockerCli *command.DockerCli) *cobra.Command { + opts := psOptions{filter: opts.NewFilterOpt()} + + cmd := &cobra.Command{ + Use: "ps [OPTIONS] SERVICE", + Short: "List the tasks of a service", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.serviceID = args[0] + return runPS(dockerCli, opts) + }, + } + flags := cmd.Flags() + flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output") + flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names") + flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") + + return cmd +} + +func runPS(dockerCli *command.DockerCli, opts psOptions) error { + client := dockerCli.Client() + ctx := context.Background() + + service, _, err := client.ServiceInspectWithRaw(ctx, opts.serviceID) + if err != nil { + return err + } + + filter := opts.filter.Value() + filter.Add("service", service.ID) + if filter.Include("node") { + nodeFilters := filter.Get("node") + for _, nodeFilter := range nodeFilters { + nodeReference, err := node.Reference(ctx, client, nodeFilter) + if err != nil { + return err + } + filter.Del("node", nodeFilter) + filter.Add("node", nodeReference) + } + } + + tasks, err := client.TaskList(ctx, types.TaskListOptions{Filter: filter}) + if err != nil { + return err + } + + return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), opts.noTrunc) +} diff --git a/components/cli/command/service/remove.go b/components/cli/command/service/remove.go new file mode 100644 index 0000000000..c3fbbabbca --- /dev/null +++ b/components/cli/command/service/remove.go @@ -0,0 +1,47 @@ +package service + +import ( + "fmt" + "strings" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" + "golang.org/x/net/context" +) + +func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { + + cmd := &cobra.Command{ + Use: "rm SERVICE [SERVICE...]", + Aliases: []string{"remove"}, + Short: "Remove one or more services", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runRemove(dockerCli, args) + }, + } + cmd.Flags() + + return cmd +} + +func runRemove(dockerCli *command.DockerCli, sids []string) error { + client := dockerCli.Client() + + ctx := context.Background() + + var errs []string + for _, sid := range sids { + err := client.ServiceRemove(ctx, sid) + if err != nil { + errs = append(errs, err.Error()) + continue + } + fmt.Fprintf(dockerCli.Out(), "%s\n", sid) + } + if len(errs) > 0 { + return fmt.Errorf(strings.Join(errs, "\n")) + } + return nil +} diff --git a/components/cli/command/service/scale.go b/components/cli/command/service/scale.go new file mode 100644 index 0000000000..2e2982db43 --- /dev/null +++ b/components/cli/command/service/scale.go @@ -0,0 +1,88 @@ +package service + +import ( + "fmt" + "strconv" + "strings" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +func newScaleCommand(dockerCli *command.DockerCli) *cobra.Command { + return &cobra.Command{ + Use: "scale SERVICE=REPLICAS [SERVICE=REPLICAS...]", + Short: "Scale one or multiple services", + Args: scaleArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runScale(dockerCli, args) + }, + } +} + +func scaleArgs(cmd *cobra.Command, args []string) error { + if err := cli.RequiresMinArgs(1)(cmd, args); err != nil { + return err + } + for _, arg := range args { + if parts := strings.SplitN(arg, "=", 2); len(parts) != 2 { + return fmt.Errorf( + "Invalid scale specifier '%s'.\nSee '%s --help'.\n\nUsage: %s\n\n%s", + arg, + cmd.CommandPath(), + cmd.UseLine(), + cmd.Short, + ) + } + } + return nil +} + +func runScale(dockerCli *command.DockerCli, args []string) error { + var errors []string + for _, arg := range args { + parts := strings.SplitN(arg, "=", 2) + serviceID, scale := parts[0], parts[1] + if err := runServiceScale(dockerCli, serviceID, scale); err != nil { + errors = append(errors, fmt.Sprintf("%s: %s", serviceID, err.Error())) + } + } + + if len(errors) == 0 { + return nil + } + return fmt.Errorf(strings.Join(errors, "\n")) +} + +func runServiceScale(dockerCli *command.DockerCli, serviceID string, scale string) error { + client := dockerCli.Client() + ctx := context.Background() + + service, _, err := client.ServiceInspectWithRaw(ctx, serviceID) + + if err != nil { + return err + } + + serviceMode := &service.Spec.Mode + if serviceMode.Replicated == nil { + return fmt.Errorf("scale can only be used with replicated mode") + } + uintScale, err := strconv.ParseUint(scale, 10, 64) + if err != nil { + return fmt.Errorf("invalid replicas value %s: %s", scale, err.Error()) + } + serviceMode.Replicated.Replicas = &uintScale + + err = client.ServiceUpdate(ctx, service.ID, service.Version, service.Spec, types.ServiceUpdateOptions{}) + if err != nil { + return err + } + + fmt.Fprintf(dockerCli.Out(), "%s scaled to %s\n", serviceID, scale) + return nil +} diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go new file mode 100644 index 0000000000..a86f20e585 --- /dev/null +++ b/components/cli/command/service/update.go @@ -0,0 +1,504 @@ +package service + +import ( + "fmt" + "sort" + "strings" + "time" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + mounttypes "github.com/docker/docker/api/types/mount" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/opts" + runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/docker/go-connections/nat" + shlex "github.com/flynn-archive/go-shlex" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { + opts := newServiceOptions() + + cmd := &cobra.Command{ + Use: "update [OPTIONS] SERVICE", + Short: "Update a service", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runUpdate(dockerCli, cmd.Flags(), args[0]) + }, + } + + flags := cmd.Flags() + flags.String("image", "", "Service image tag") + flags.String("args", "", "Service command args") + addServiceFlags(cmd, opts) + + flags.Var(newListOptsVar(), flagEnvRemove, "Remove an environment variable") + flags.Var(newListOptsVar(), flagGroupRemove, "Remove previously added user groups from the container") + flags.Var(newListOptsVar(), flagLabelRemove, "Remove a label by its key") + flags.Var(newListOptsVar(), flagContainerLabelRemove, "Remove a container label by its key") + flags.Var(newListOptsVar(), flagMountRemove, "Remove a mount by its target path") + flags.Var(newListOptsVar(), flagPublishRemove, "Remove a published port by its target port") + flags.Var(newListOptsVar(), flagConstraintRemove, "Remove a constraint") + flags.Var(&opts.labels, flagLabelAdd, "Add or update service labels") + flags.Var(&opts.containerLabels, flagContainerLabelAdd, "Add or update container labels") + flags.Var(&opts.env, flagEnvAdd, "Add or update environment variables") + flags.Var(&opts.mounts, flagMountAdd, "Add or update a mount on a service") + flags.StringSliceVar(&opts.constraints, flagConstraintAdd, []string{}, "Add or update placement constraints") + flags.Var(&opts.endpoint.ports, flagPublishAdd, "Add or update a published port") + return cmd +} + +func newListOptsVar() *opts.ListOpts { + return opts.NewListOptsRef(&[]string{}, nil) +} + +func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID string) error { + apiClient := dockerCli.Client() + ctx := context.Background() + updateOpts := types.ServiceUpdateOptions{} + + service, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID) + if err != nil { + return err + } + + err = updateService(flags, &service.Spec) + if err != nil { + return err + } + + // only send auth if flag was set + sendAuth, err := flags.GetBool(flagRegistryAuth) + if err != nil { + return err + } + if sendAuth { + // Retrieve encoded auth token from the image reference + // This would be the old image if it didn't change in this update + image := service.Spec.TaskTemplate.ContainerSpec.Image + encodedAuth, err := dockerCli.RetrieveAuthTokenFromImage(ctx, image) + if err != nil { + return err + } + updateOpts.EncodedRegistryAuth = encodedAuth + } + + err = apiClient.ServiceUpdate(ctx, service.ID, service.Version, service.Spec, updateOpts) + if err != nil { + return err + } + + fmt.Fprintf(dockerCli.Out(), "%s\n", serviceID) + return nil +} + +func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error { + updateString := func(flag string, field *string) { + if flags.Changed(flag) { + *field, _ = flags.GetString(flag) + } + } + + updateInt64Value := func(flag string, field *int64) { + if flags.Changed(flag) { + *field = flags.Lookup(flag).Value.(int64Value).Value() + } + } + + updateDuration := func(flag string, field *time.Duration) { + if flags.Changed(flag) { + *field, _ = flags.GetDuration(flag) + } + } + + updateDurationOpt := func(flag string, field **time.Duration) { + if flags.Changed(flag) { + val := *flags.Lookup(flag).Value.(*DurationOpt).Value() + *field = &val + } + } + + updateUint64 := func(flag string, field *uint64) { + if flags.Changed(flag) { + *field, _ = flags.GetUint64(flag) + } + } + + updateUint64Opt := func(flag string, field **uint64) { + if flags.Changed(flag) { + val := *flags.Lookup(flag).Value.(*Uint64Opt).Value() + *field = &val + } + } + + cspec := &spec.TaskTemplate.ContainerSpec + task := &spec.TaskTemplate + + taskResources := func() *swarm.ResourceRequirements { + if task.Resources == nil { + task.Resources = &swarm.ResourceRequirements{} + } + return task.Resources + } + + updateString(flagName, &spec.Name) + updateLabels(flags, &spec.Labels) + updateContainerLabels(flags, &cspec.Labels) + updateString("image", &cspec.Image) + updateStringToSlice(flags, "args", &cspec.Args) + updateEnvironment(flags, &cspec.Env) + updateString(flagWorkdir, &cspec.Dir) + updateString(flagUser, &cspec.User) + updateMounts(flags, &cspec.Mounts) + + if flags.Changed(flagLimitCPU) || flags.Changed(flagLimitMemory) { + taskResources().Limits = &swarm.Resources{} + updateInt64Value(flagLimitCPU, &task.Resources.Limits.NanoCPUs) + updateInt64Value(flagLimitMemory, &task.Resources.Limits.MemoryBytes) + } + if flags.Changed(flagReserveCPU) || flags.Changed(flagReserveMemory) { + taskResources().Reservations = &swarm.Resources{} + updateInt64Value(flagReserveCPU, &task.Resources.Reservations.NanoCPUs) + updateInt64Value(flagReserveMemory, &task.Resources.Reservations.MemoryBytes) + } + + updateDurationOpt(flagStopGracePeriod, &cspec.StopGracePeriod) + + if anyChanged(flags, flagRestartCondition, flagRestartDelay, flagRestartMaxAttempts, flagRestartWindow) { + if task.RestartPolicy == nil { + task.RestartPolicy = &swarm.RestartPolicy{} + } + + if flags.Changed(flagRestartCondition) { + value, _ := flags.GetString(flagRestartCondition) + task.RestartPolicy.Condition = swarm.RestartPolicyCondition(value) + } + updateDurationOpt(flagRestartDelay, &task.RestartPolicy.Delay) + updateUint64Opt(flagRestartMaxAttempts, &task.RestartPolicy.MaxAttempts) + updateDurationOpt(flagRestartWindow, &task.RestartPolicy.Window) + } + + if anyChanged(flags, flagConstraintAdd, flagConstraintRemove) { + if task.Placement == nil { + task.Placement = &swarm.Placement{} + } + updatePlacement(flags, task.Placement) + } + + if err := updateReplicas(flags, &spec.Mode); err != nil { + return err + } + + if anyChanged(flags, flagUpdateParallelism, flagUpdateDelay, flagUpdateFailureAction) { + if spec.UpdateConfig == nil { + spec.UpdateConfig = &swarm.UpdateConfig{} + } + updateUint64(flagUpdateParallelism, &spec.UpdateConfig.Parallelism) + updateDuration(flagUpdateDelay, &spec.UpdateConfig.Delay) + updateString(flagUpdateFailureAction, &spec.UpdateConfig.FailureAction) + } + + if flags.Changed(flagEndpointMode) { + value, _ := flags.GetString(flagEndpointMode) + if spec.EndpointSpec == nil { + spec.EndpointSpec = &swarm.EndpointSpec{} + } + spec.EndpointSpec.Mode = swarm.ResolutionMode(value) + } + + if anyChanged(flags, flagGroupAdd, flagGroupRemove) { + if err := updateGroups(flags, &cspec.Groups); err != nil { + return err + } + } + + if anyChanged(flags, flagPublishAdd, flagPublishRemove) { + if spec.EndpointSpec == nil { + spec.EndpointSpec = &swarm.EndpointSpec{} + } + if err := updatePorts(flags, &spec.EndpointSpec.Ports); err != nil { + return err + } + } + + if err := updateLogDriver(flags, &spec.TaskTemplate); err != nil { + return err + } + + return nil +} + +func updateStringToSlice(flags *pflag.FlagSet, flag string, field *[]string) error { + if !flags.Changed(flag) { + return nil + } + + value, _ := flags.GetString(flag) + valueSlice, err := shlex.Split(value) + *field = valueSlice + return err +} + +func anyChanged(flags *pflag.FlagSet, fields ...string) bool { + for _, flag := range fields { + if flags.Changed(flag) { + return true + } + } + return false +} + +func updatePlacement(flags *pflag.FlagSet, placement *swarm.Placement) { + field, _ := flags.GetStringSlice(flagConstraintAdd) + placement.Constraints = append(placement.Constraints, field...) + + toRemove := buildToRemoveSet(flags, flagConstraintRemove) + placement.Constraints = removeItems(placement.Constraints, toRemove, itemKey) +} + +func updateContainerLabels(flags *pflag.FlagSet, field *map[string]string) { + if flags.Changed(flagContainerLabelAdd) { + if *field == nil { + *field = map[string]string{} + } + + values := flags.Lookup(flagContainerLabelAdd).Value.(*opts.ListOpts).GetAll() + for key, value := range runconfigopts.ConvertKVStringsToMap(values) { + (*field)[key] = value + } + } + + if *field != nil && flags.Changed(flagContainerLabelRemove) { + toRemove := flags.Lookup(flagContainerLabelRemove).Value.(*opts.ListOpts).GetAll() + for _, label := range toRemove { + delete(*field, label) + } + } +} + +func updateLabels(flags *pflag.FlagSet, field *map[string]string) { + if flags.Changed(flagLabelAdd) { + if *field == nil { + *field = map[string]string{} + } + + values := flags.Lookup(flagLabelAdd).Value.(*opts.ListOpts).GetAll() + for key, value := range runconfigopts.ConvertKVStringsToMap(values) { + (*field)[key] = value + } + } + + if *field != nil && flags.Changed(flagLabelRemove) { + toRemove := flags.Lookup(flagLabelRemove).Value.(*opts.ListOpts).GetAll() + for _, label := range toRemove { + delete(*field, label) + } + } +} + +func updateEnvironment(flags *pflag.FlagSet, field *[]string) { + envSet := map[string]string{} + for _, v := range *field { + envSet[envKey(v)] = v + } + if flags.Changed(flagEnvAdd) { + value := flags.Lookup(flagEnvAdd).Value.(*opts.ListOpts) + for _, v := range value.GetAll() { + envSet[envKey(v)] = v + } + } + + *field = []string{} + for _, v := range envSet { + *field = append(*field, v) + } + + toRemove := buildToRemoveSet(flags, flagEnvRemove) + *field = removeItems(*field, toRemove, envKey) +} + +func envKey(value string) string { + kv := strings.SplitN(value, "=", 2) + return kv[0] +} + +func itemKey(value string) string { + return value +} + +func buildToRemoveSet(flags *pflag.FlagSet, flag string) map[string]struct{} { + var empty struct{} + toRemove := make(map[string]struct{}) + + if !flags.Changed(flag) { + return toRemove + } + + toRemoveSlice := flags.Lookup(flag).Value.(*opts.ListOpts).GetAll() + for _, key := range toRemoveSlice { + toRemove[key] = empty + } + return toRemove +} + +func removeItems( + seq []string, + toRemove map[string]struct{}, + keyFunc func(string) string, +) []string { + newSeq := []string{} + for _, item := range seq { + if _, exists := toRemove[keyFunc(item)]; !exists { + newSeq = append(newSeq, item) + } + } + return newSeq +} + +func updateMounts(flags *pflag.FlagSet, mounts *[]mounttypes.Mount) { + if flags.Changed(flagMountAdd) { + values := flags.Lookup(flagMountAdd).Value.(*MountOpt).Value() + *mounts = append(*mounts, values...) + } + toRemove := buildToRemoveSet(flags, flagMountRemove) + + newMounts := []mounttypes.Mount{} + for _, mount := range *mounts { + if _, exists := toRemove[mount.Target]; !exists { + newMounts = append(newMounts, mount) + } + } + *mounts = newMounts +} + +func updateGroups(flags *pflag.FlagSet, groups *[]string) error { + if flags.Changed(flagGroupAdd) { + values, err := flags.GetStringSlice(flagGroupAdd) + if err != nil { + return err + } + *groups = append(*groups, values...) + } + toRemove := buildToRemoveSet(flags, flagGroupRemove) + + newGroups := []string{} + for _, group := range *groups { + if _, exists := toRemove[group]; !exists { + newGroups = append(newGroups, group) + } + } + // Sort so that result is predictable. + sort.Strings(newGroups) + + *groups = newGroups + return nil +} + +type byPortConfig []swarm.PortConfig + +func (r byPortConfig) Len() int { return len(r) } +func (r byPortConfig) Swap(i, j int) { r[i], r[j] = r[j], r[i] } +func (r byPortConfig) Less(i, j int) bool { + // We convert PortConfig into `port/protocol`, e.g., `80/tcp` + // In updatePorts we already filter out with map so there is duplicate entries + return portConfigToString(&r[i]) < portConfigToString(&r[j]) +} + +func portConfigToString(portConfig *swarm.PortConfig) string { + protocol := portConfig.Protocol + if protocol == "" { + protocol = "tcp" + } + return fmt.Sprintf("%v/%s", portConfig.PublishedPort, protocol) +} + +func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) error { + // The key of the map is `port/protocol`, e.g., `80/tcp` + portSet := map[string]swarm.PortConfig{} + // Check to see if there are any conflict in flags. + if flags.Changed(flagPublishAdd) { + values := flags.Lookup(flagPublishAdd).Value.(*opts.ListOpts).GetAll() + ports, portBindings, _ := nat.ParsePortSpecs(values) + + for port := range ports { + newConfigs := convertPortToPortConfig(port, portBindings) + for _, entry := range newConfigs { + if v, ok := portSet[portConfigToString(&entry)]; ok && v != entry { + return fmt.Errorf("conflicting port mapping between %v:%v/%s and %v:%v/%s", entry.PublishedPort, entry.TargetPort, entry.Protocol, v.PublishedPort, v.TargetPort, v.Protocol) + } + portSet[portConfigToString(&entry)] = entry + } + } + } + + // Override previous PortConfig in service if there is any duplicate + for _, entry := range *portConfig { + if _, ok := portSet[portConfigToString(&entry)]; !ok { + portSet[portConfigToString(&entry)] = entry + } + } + + toRemove := flags.Lookup(flagPublishRemove).Value.(*opts.ListOpts).GetAll() + newPorts := []swarm.PortConfig{} +portLoop: + for _, port := range portSet { + for _, rawTargetPort := range toRemove { + targetPort := nat.Port(rawTargetPort) + if equalPort(targetPort, port) { + continue portLoop + } + } + newPorts = append(newPorts, port) + } + // Sort the PortConfig to avoid unnecessary updates + sort.Sort(byPortConfig(newPorts)) + *portConfig = newPorts + return nil +} + +func equalPort(targetPort nat.Port, port swarm.PortConfig) bool { + return (string(port.Protocol) == targetPort.Proto() && + port.TargetPort == uint32(targetPort.Int())) +} + +func updateReplicas(flags *pflag.FlagSet, serviceMode *swarm.ServiceMode) error { + if !flags.Changed(flagReplicas) { + return nil + } + + if serviceMode == nil || serviceMode.Replicated == nil { + return fmt.Errorf("replicas can only be used with replicated mode") + } + serviceMode.Replicated.Replicas = flags.Lookup(flagReplicas).Value.(*Uint64Opt).Value() + return nil +} + +// updateLogDriver updates the log driver only if the log driver flag is set. +// All options will be replaced with those provided on the command line. +func updateLogDriver(flags *pflag.FlagSet, taskTemplate *swarm.TaskSpec) error { + if !flags.Changed(flagLogDriver) { + return nil + } + + name, err := flags.GetString(flagLogDriver) + if err != nil { + return err + } + + if name == "" { + return nil + } + + taskTemplate.LogDriver = &swarm.Driver{ + Name: name, + Options: runconfigopts.ConvertKVStringsToMap(flags.Lookup(flagLogOpt).Value.(*opts.ListOpts).GetAll()), + } + + return nil +} diff --git a/components/cli/command/service/update_test.go b/components/cli/command/service/update_test.go new file mode 100644 index 0000000000..6e68e977ac --- /dev/null +++ b/components/cli/command/service/update_test.go @@ -0,0 +1,198 @@ +package service + +import ( + "sort" + "testing" + + mounttypes "github.com/docker/docker/api/types/mount" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestUpdateServiceArgs(t *testing.T) { + flags := newUpdateCommand(nil).Flags() + flags.Set("args", "the \"new args\"") + + spec := &swarm.ServiceSpec{} + cspec := &spec.TaskTemplate.ContainerSpec + cspec.Args = []string{"old", "args"} + + updateService(flags, spec) + assert.EqualStringSlice(t, cspec.Args, []string{"the", "new args"}) +} + +func TestUpdateLabels(t *testing.T) { + flags := newUpdateCommand(nil).Flags() + flags.Set("label-add", "toadd=newlabel") + flags.Set("label-rm", "toremove") + + labels := map[string]string{ + "toremove": "thelabeltoremove", + "tokeep": "value", + } + + updateLabels(flags, &labels) + assert.Equal(t, len(labels), 2) + assert.Equal(t, labels["tokeep"], "value") + assert.Equal(t, labels["toadd"], "newlabel") +} + +func TestUpdateLabelsRemoveALabelThatDoesNotExist(t *testing.T) { + flags := newUpdateCommand(nil).Flags() + flags.Set("label-rm", "dne") + + labels := map[string]string{"foo": "theoldlabel"} + updateLabels(flags, &labels) + assert.Equal(t, len(labels), 1) +} + +func TestUpdatePlacement(t *testing.T) { + flags := newUpdateCommand(nil).Flags() + flags.Set("constraint-add", "node=toadd") + flags.Set("constraint-rm", "node!=toremove") + + placement := &swarm.Placement{ + Constraints: []string{"node!=toremove", "container=tokeep"}, + } + + updatePlacement(flags, placement) + assert.Equal(t, len(placement.Constraints), 2) + assert.Equal(t, placement.Constraints[0], "container=tokeep") + assert.Equal(t, placement.Constraints[1], "node=toadd") +} + +func TestUpdateEnvironment(t *testing.T) { + flags := newUpdateCommand(nil).Flags() + flags.Set("env-add", "toadd=newenv") + flags.Set("env-rm", "toremove") + + envs := []string{"toremove=theenvtoremove", "tokeep=value"} + + updateEnvironment(flags, &envs) + assert.Equal(t, len(envs), 2) + // Order has been removed in updateEnvironment (map) + sort.Strings(envs) + assert.Equal(t, envs[0], "toadd=newenv") + assert.Equal(t, envs[1], "tokeep=value") +} + +func TestUpdateEnvironmentWithDuplicateValues(t *testing.T) { + flags := newUpdateCommand(nil).Flags() + flags.Set("env-add", "foo=newenv") + flags.Set("env-add", "foo=dupe") + flags.Set("env-rm", "foo") + + envs := []string{"foo=value"} + + updateEnvironment(flags, &envs) + assert.Equal(t, len(envs), 0) +} + +func TestUpdateEnvironmentWithDuplicateKeys(t *testing.T) { + // Test case for #25404 + flags := newUpdateCommand(nil).Flags() + flags.Set("env-add", "A=b") + + envs := []string{"A=c"} + + updateEnvironment(flags, &envs) + assert.Equal(t, len(envs), 1) + assert.Equal(t, envs[0], "A=b") +} + +func TestUpdateGroups(t *testing.T) { + flags := newUpdateCommand(nil).Flags() + flags.Set("group-add", "wheel") + flags.Set("group-add", "docker") + flags.Set("group-rm", "root") + flags.Set("group-add", "foo") + flags.Set("group-rm", "docker") + + groups := []string{"bar", "root"} + + updateGroups(flags, &groups) + assert.Equal(t, len(groups), 3) + assert.Equal(t, groups[0], "bar") + assert.Equal(t, groups[1], "foo") + assert.Equal(t, groups[2], "wheel") +} + +func TestUpdateMounts(t *testing.T) { + flags := newUpdateCommand(nil).Flags() + flags.Set("mount-add", "type=volume,target=/toadd") + flags.Set("mount-rm", "/toremove") + + mounts := []mounttypes.Mount{ + {Target: "/toremove", Type: mounttypes.TypeBind}, + {Target: "/tokeep", Type: mounttypes.TypeBind}, + } + + updateMounts(flags, &mounts) + assert.Equal(t, len(mounts), 2) + assert.Equal(t, mounts[0].Target, "/tokeep") + assert.Equal(t, mounts[1].Target, "/toadd") +} + +func TestUpdatePorts(t *testing.T) { + flags := newUpdateCommand(nil).Flags() + flags.Set("publish-add", "1000:1000") + flags.Set("publish-rm", "333/udp") + + portConfigs := []swarm.PortConfig{ + {TargetPort: 333, Protocol: swarm.PortConfigProtocolUDP}, + {TargetPort: 555}, + } + + err := updatePorts(flags, &portConfigs) + assert.Equal(t, err, nil) + assert.Equal(t, len(portConfigs), 2) + // Do a sort to have the order (might have changed by map) + targetPorts := []int{int(portConfigs[0].TargetPort), int(portConfigs[1].TargetPort)} + sort.Ints(targetPorts) + assert.Equal(t, targetPorts[0], 555) + assert.Equal(t, targetPorts[1], 1000) +} + +func TestUpdatePortsDuplicateEntries(t *testing.T) { + // Test case for #25375 + flags := newUpdateCommand(nil).Flags() + flags.Set("publish-add", "80:80") + + portConfigs := []swarm.PortConfig{ + {TargetPort: 80, PublishedPort: 80}, + } + + err := updatePorts(flags, &portConfigs) + assert.Equal(t, err, nil) + assert.Equal(t, len(portConfigs), 1) + assert.Equal(t, portConfigs[0].TargetPort, uint32(80)) +} + +func TestUpdatePortsDuplicateKeys(t *testing.T) { + // Test case for #25375 + flags := newUpdateCommand(nil).Flags() + flags.Set("publish-add", "80:20") + + portConfigs := []swarm.PortConfig{ + {TargetPort: 80, PublishedPort: 80}, + } + + err := updatePorts(flags, &portConfigs) + assert.Equal(t, err, nil) + assert.Equal(t, len(portConfigs), 1) + assert.Equal(t, portConfigs[0].TargetPort, uint32(20)) +} + +func TestUpdatePortsConflictingFlags(t *testing.T) { + // Test case for #25375 + flags := newUpdateCommand(nil).Flags() + flags.Set("publish-add", "80:80") + flags.Set("publish-add", "80:20") + + portConfigs := []swarm.PortConfig{ + {TargetPort: 80, PublishedPort: 80}, + } + + err := updatePorts(flags, &portConfigs) + assert.Error(t, err, "conflicting port mapping") +} diff --git a/components/cli/command/stack/cmd.go b/components/cli/command/stack/cmd.go new file mode 100644 index 0000000000..979e1a0b77 --- /dev/null +++ b/components/cli/command/stack/cmd.go @@ -0,0 +1,39 @@ +// +build experimental + +package stack + +import ( + "fmt" + + "github.com/docker/docker/api/client" + "github.com/docker/docker/cli" + "github.com/spf13/cobra" +) + +// NewStackCommand returns a cobra command for `stack` subcommands +func NewStackCommand(dockerCli *client.DockerCli) *cobra.Command { + cmd := &cobra.Command{ + Use: "stack", + Short: "Manage Docker stacks", + Args: cli.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + }, + } + cmd.AddCommand( + newConfigCommand(dockerCli), + newDeployCommand(dockerCli), + newRemoveCommand(dockerCli), + newServicesCommand(dockerCli), + newPsCommand(dockerCli), + ) + return cmd +} + +// NewTopLevelDeployCommand returns a command for `docker deploy` +func NewTopLevelDeployCommand(dockerCli *client.DockerCli) *cobra.Command { + cmd := newDeployCommand(dockerCli) + // Remove the aliases at the top level + cmd.Aliases = []string{} + return cmd +} diff --git a/components/cli/command/stack/cmd_stub.go b/components/cli/command/stack/cmd_stub.go new file mode 100644 index 0000000000..51cb2d1bcf --- /dev/null +++ b/components/cli/command/stack/cmd_stub.go @@ -0,0 +1,18 @@ +// +build !experimental + +package stack + +import ( + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +// NewStackCommand returns no command +func NewStackCommand(dockerCli *command.DockerCli) *cobra.Command { + return &cobra.Command{} +} + +// NewTopLevelDeployCommand returns no command +func NewTopLevelDeployCommand(dockerCli *command.DockerCli) *cobra.Command { + return &cobra.Command{} +} diff --git a/components/cli/command/stack/common.go b/components/cli/command/stack/common.go new file mode 100644 index 0000000000..2afdb5147d --- /dev/null +++ b/components/cli/command/stack/common.go @@ -0,0 +1,50 @@ +// +build experimental + +package stack + +import ( + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/client" +) + +const ( + labelNamespace = "com.docker.stack.namespace" +) + +func getStackLabels(namespace string, labels map[string]string) map[string]string { + if labels == nil { + labels = make(map[string]string) + } + labels[labelNamespace] = namespace + return labels +} + +func getStackFilter(namespace string) filters.Args { + filter := filters.NewArgs() + filter.Add("label", labelNamespace+"="+namespace) + return filter +} + +func getServices( + ctx context.Context, + apiclient client.APIClient, + namespace string, +) ([]swarm.Service, error) { + return apiclient.ServiceList( + ctx, + types.ServiceListOptions{Filter: getStackFilter(namespace)}) +} + +func getNetworks( + ctx context.Context, + apiclient client.APIClient, + namespace string, +) ([]types.NetworkResource, error) { + return apiclient.NetworkList( + ctx, + types.NetworkListOptions{Filters: getStackFilter(namespace)}) +} diff --git a/components/cli/command/stack/config.go b/components/cli/command/stack/config.go new file mode 100644 index 0000000000..696c0c3fc7 --- /dev/null +++ b/components/cli/command/stack/config.go @@ -0,0 +1,41 @@ +// +build experimental + +package stack + +import ( + "github.com/docker/docker/api/client" + "github.com/docker/docker/api/client/bundlefile" + "github.com/docker/docker/cli" + "github.com/spf13/cobra" +) + +type configOptions struct { + bundlefile string + namespace string +} + +func newConfigCommand(dockerCli *client.DockerCli) *cobra.Command { + var opts configOptions + + cmd := &cobra.Command{ + Use: "config [OPTIONS] STACK", + Short: "Print the stack configuration", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.namespace = args[0] + return runConfig(dockerCli, opts) + }, + } + + flags := cmd.Flags() + addBundlefileFlag(&opts.bundlefile, flags) + return cmd +} + +func runConfig(dockerCli *client.DockerCli, opts configOptions) error { + bundle, err := loadBundlefile(dockerCli.Err(), opts.namespace, opts.bundlefile) + if err != nil { + return err + } + return bundlefile.Print(dockerCli.Out(), bundle) +} diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go new file mode 100644 index 0000000000..5c03dc3d31 --- /dev/null +++ b/components/cli/command/stack/deploy.go @@ -0,0 +1,236 @@ +// +build experimental + +package stack + +import ( + "fmt" + + "github.com/spf13/cobra" + "golang.org/x/net/context" + + "github.com/docker/docker/api/client" + "github.com/docker/docker/api/client/bundlefile" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli" +) + +const ( + defaultNetworkDriver = "overlay" +) + +type deployOptions struct { + bundlefile string + namespace string + sendRegistryAuth bool +} + +func newDeployCommand(dockerCli *client.DockerCli) *cobra.Command { + var opts deployOptions + + cmd := &cobra.Command{ + Use: "deploy [OPTIONS] STACK", + Aliases: []string{"up"}, + Short: "Create and update a stack from a Distributed Application Bundle (DAB)", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.namespace = args[0] + return runDeploy(dockerCli, opts) + }, + } + + flags := cmd.Flags() + addBundlefileFlag(&opts.bundlefile, flags) + addRegistryAuthFlag(&opts.sendRegistryAuth, flags) + return cmd +} + +func runDeploy(dockerCli *client.DockerCli, opts deployOptions) error { + bundle, err := loadBundlefile(dockerCli.Err(), opts.namespace, opts.bundlefile) + if err != nil { + return err + } + + info, err := dockerCli.Client().Info(context.Background()) + if err != nil { + return err + } + if !info.Swarm.ControlAvailable { + return fmt.Errorf("This node is not a swarm manager. Use \"docker swarm init\" or \"docker swarm join\" to connect this node to swarm and try again.") + } + + networks := getUniqueNetworkNames(bundle.Services) + ctx := context.Background() + + if err := updateNetworks(ctx, dockerCli, networks, opts.namespace); err != nil { + return err + } + return deployServices(ctx, dockerCli, bundle.Services, opts.namespace, opts.sendRegistryAuth) +} + +func getUniqueNetworkNames(services map[string]bundlefile.Service) []string { + networkSet := make(map[string]bool) + for _, service := range services { + for _, network := range service.Networks { + networkSet[network] = true + } + } + + networks := []string{} + for network := range networkSet { + networks = append(networks, network) + } + return networks +} + +func updateNetworks( + ctx context.Context, + dockerCli *client.DockerCli, + networks []string, + namespace string, +) error { + client := dockerCli.Client() + + existingNetworks, err := getNetworks(ctx, client, namespace) + if err != nil { + return err + } + + existingNetworkMap := make(map[string]types.NetworkResource) + for _, network := range existingNetworks { + existingNetworkMap[network.Name] = network + } + + createOpts := types.NetworkCreate{ + Labels: getStackLabels(namespace, nil), + Driver: defaultNetworkDriver, + } + + for _, internalName := range networks { + name := fmt.Sprintf("%s_%s", namespace, internalName) + + if _, exists := existingNetworkMap[name]; exists { + continue + } + fmt.Fprintf(dockerCli.Out(), "Creating network %s\n", name) + if _, err := client.NetworkCreate(ctx, name, createOpts); err != nil { + return err + } + } + return nil +} + +func convertNetworks(networks []string, namespace string, name string) []swarm.NetworkAttachmentConfig { + nets := []swarm.NetworkAttachmentConfig{} + for _, network := range networks { + nets = append(nets, swarm.NetworkAttachmentConfig{ + Target: namespace + "_" + network, + Aliases: []string{name}, + }) + } + return nets +} + +func deployServices( + ctx context.Context, + dockerCli *client.DockerCli, + services map[string]bundlefile.Service, + namespace string, + sendAuth bool, +) error { + apiClient := dockerCli.Client() + out := dockerCli.Out() + + existingServices, err := getServices(ctx, apiClient, namespace) + if err != nil { + return err + } + + existingServiceMap := make(map[string]swarm.Service) + for _, service := range existingServices { + existingServiceMap[service.Spec.Name] = service + } + + for internalName, service := range services { + name := fmt.Sprintf("%s_%s", namespace, internalName) + + var ports []swarm.PortConfig + for _, portSpec := range service.Ports { + ports = append(ports, swarm.PortConfig{ + Protocol: swarm.PortConfigProtocol(portSpec.Protocol), + TargetPort: portSpec.Port, + }) + } + + serviceSpec := swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: name, + Labels: getStackLabels(namespace, service.Labels), + }, + TaskTemplate: swarm.TaskSpec{ + ContainerSpec: swarm.ContainerSpec{ + Image: service.Image, + Command: service.Command, + Args: service.Args, + Env: service.Env, + // Service Labels will not be copied to Containers + // automatically during the deployment so we apply + // it here. + Labels: getStackLabels(namespace, nil), + }, + }, + EndpointSpec: &swarm.EndpointSpec{ + Ports: ports, + }, + Networks: convertNetworks(service.Networks, namespace, internalName), + } + + cspec := &serviceSpec.TaskTemplate.ContainerSpec + if service.WorkingDir != nil { + cspec.Dir = *service.WorkingDir + } + if service.User != nil { + cspec.User = *service.User + } + + encodedAuth := "" + if sendAuth { + // Retrieve encoded auth token from the image reference + image := serviceSpec.TaskTemplate.ContainerSpec.Image + encodedAuth, err = dockerCli.RetrieveAuthTokenFromImage(ctx, image) + if err != nil { + return err + } + } + + if service, exists := existingServiceMap[name]; exists { + fmt.Fprintf(out, "Updating service %s (id: %s)\n", name, service.ID) + + updateOpts := types.ServiceUpdateOptions{} + if sendAuth { + updateOpts.EncodedRegistryAuth = encodedAuth + } + if err := apiClient.ServiceUpdate( + ctx, + service.ID, + service.Version, + serviceSpec, + updateOpts, + ); err != nil { + return err + } + } else { + fmt.Fprintf(out, "Creating service %s\n", name) + + createOpts := types.ServiceCreateOptions{} + if sendAuth { + createOpts.EncodedRegistryAuth = encodedAuth + } + if _, err := apiClient.ServiceCreate(ctx, serviceSpec, createOpts); err != nil { + return err + } + } + } + + return nil +} diff --git a/components/cli/command/stack/opts.go b/components/cli/command/stack/opts.go new file mode 100644 index 0000000000..345bdc38f5 --- /dev/null +++ b/components/cli/command/stack/opts.go @@ -0,0 +1,49 @@ +// +build experimental + +package stack + +import ( + "fmt" + "io" + "os" + + "github.com/docker/docker/api/client/bundlefile" + "github.com/spf13/pflag" +) + +func addBundlefileFlag(opt *string, flags *pflag.FlagSet) { + flags.StringVar( + opt, + "file", "", + "Path to a Distributed Application Bundle file (Default: STACK.dab)") +} + +func addRegistryAuthFlag(opt *bool, flags *pflag.FlagSet) { + flags.BoolVar(opt, "with-registry-auth", false, "Send registry authentication details to Swarm agents") +} + +func loadBundlefile(stderr io.Writer, namespace string, path string) (*bundlefile.Bundlefile, error) { + defaultPath := fmt.Sprintf("%s.dab", namespace) + + if path == "" { + path = defaultPath + } + if _, err := os.Stat(path); err != nil { + return nil, fmt.Errorf( + "Bundle %s not found. Specify the path with --file", + path) + } + + fmt.Fprintf(stderr, "Loading bundle from %s\n", path) + reader, err := os.Open(path) + if err != nil { + return nil, err + } + defer reader.Close() + + bundle, err := bundlefile.LoadFile(reader) + if err != nil { + return nil, fmt.Errorf("Error reading %s: %v\n", path, err) + } + return bundle, err +} diff --git a/components/cli/command/stack/ps.go b/components/cli/command/stack/ps.go new file mode 100644 index 0000000000..9d9458d85f --- /dev/null +++ b/components/cli/command/stack/ps.go @@ -0,0 +1,72 @@ +// +build experimental + +package stack + +import ( + "fmt" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/client" + "github.com/docker/docker/api/client/idresolver" + "github.com/docker/docker/api/client/task" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli" + "github.com/docker/docker/opts" + "github.com/spf13/cobra" +) + +type psOptions struct { + all bool + filter opts.FilterOpt + noTrunc bool + namespace string + noResolve bool +} + +func newPsCommand(dockerCli *client.DockerCli) *cobra.Command { + opts := psOptions{filter: opts.NewFilterOpt()} + + cmd := &cobra.Command{ + Use: "ps [OPTIONS] STACK", + Short: "List the tasks in the stack", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.namespace = args[0] + return runPS(dockerCli, opts) + }, + } + flags := cmd.Flags() + flags.BoolVarP(&opts.all, "all", "a", false, "Display all tasks") + flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output") + flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names") + flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") + + return cmd +} + +func runPS(dockerCli *client.DockerCli, opts psOptions) error { + namespace := opts.namespace + client := dockerCli.Client() + ctx := context.Background() + + filter := opts.filter.Value() + filter.Add("label", labelNamespace+"="+opts.namespace) + if !opts.all && !filter.Include("desired-state") { + filter.Add("desired-state", string(swarm.TaskStateRunning)) + filter.Add("desired-state", string(swarm.TaskStateAccepted)) + } + + tasks, err := client.TaskList(ctx, types.TaskListOptions{Filter: filter}) + if err != nil { + return err + } + + if len(tasks) == 0 { + fmt.Fprintf(dockerCli.Out(), "Nothing found in stack: %s\n", namespace) + return nil + } + + return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), opts.noTrunc) +} diff --git a/components/cli/command/stack/remove.go b/components/cli/command/stack/remove.go new file mode 100644 index 0000000000..9ba91e5c23 --- /dev/null +++ b/components/cli/command/stack/remove.go @@ -0,0 +1,75 @@ +// +build experimental + +package stack + +import ( + "fmt" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/client" + "github.com/docker/docker/cli" + "github.com/spf13/cobra" +) + +type removeOptions struct { + namespace string +} + +func newRemoveCommand(dockerCli *client.DockerCli) *cobra.Command { + var opts removeOptions + + cmd := &cobra.Command{ + Use: "rm STACK", + Aliases: []string{"remove", "down"}, + Short: "Remove the stack", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.namespace = args[0] + return runRemove(dockerCli, opts) + }, + } + return cmd +} + +func runRemove(dockerCli *client.DockerCli, opts removeOptions) error { + namespace := opts.namespace + client := dockerCli.Client() + stderr := dockerCli.Err() + ctx := context.Background() + hasError := false + + services, err := getServices(ctx, client, namespace) + if err != nil { + return err + } + for _, service := range services { + fmt.Fprintf(stderr, "Removing service %s\n", service.Spec.Name) + if err := client.ServiceRemove(ctx, service.ID); err != nil { + hasError = true + fmt.Fprintf(stderr, "Failed to remove service %s: %s", service.ID, err) + } + } + + networks, err := getNetworks(ctx, client, namespace) + if err != nil { + return err + } + for _, network := range networks { + fmt.Fprintf(stderr, "Removing network %s\n", network.Name) + if err := client.NetworkRemove(ctx, network.ID); err != nil { + hasError = true + fmt.Fprintf(stderr, "Failed to remove network %s: %s", network.ID, err) + } + } + + if len(services) == 0 && len(networks) == 0 { + fmt.Fprintf(dockerCli.Out(), "Nothing found in stack: %s\n", namespace) + return nil + } + + if hasError { + return fmt.Errorf("Failed to remove some resources") + } + return nil +} diff --git a/components/cli/command/stack/services.go b/components/cli/command/stack/services.go new file mode 100644 index 0000000000..819b1c6759 --- /dev/null +++ b/components/cli/command/stack/services.go @@ -0,0 +1,87 @@ +// +build experimental + +package stack + +import ( + "fmt" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/client" + "github.com/docker/docker/api/client/service" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/cli" + "github.com/docker/docker/opts" + "github.com/spf13/cobra" +) + +const ( + listItemFmt = "%s\t%s\t%s\t%s\t%s\n" +) + +type servicesOptions struct { + quiet bool + filter opts.FilterOpt + namespace string +} + +func newServicesCommand(dockerCli *client.DockerCli) *cobra.Command { + opts := servicesOptions{filter: opts.NewFilterOpt()} + + cmd := &cobra.Command{ + Use: "services [OPTIONS] STACK", + Short: "List the services in the stack", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.namespace = args[0] + return runServices(dockerCli, opts) + }, + } + flags := cmd.Flags() + flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs") + flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") + + return cmd +} + +func runServices(dockerCli *client.DockerCli, opts servicesOptions) error { + ctx := context.Background() + client := dockerCli.Client() + + filter := opts.filter.Value() + filter.Add("label", labelNamespace+"="+opts.namespace) + + services, err := client.ServiceList(ctx, types.ServiceListOptions{Filter: filter}) + if err != nil { + return err + } + + out := dockerCli.Out() + + // if no services in this stack, print message and exit 0 + if len(services) == 0 { + fmt.Fprintf(out, "Nothing found in stack: %s\n", opts.namespace) + return nil + } + + if opts.quiet { + service.PrintQuiet(out, services) + } else { + taskFilter := filters.NewArgs() + for _, service := range services { + taskFilter.Add("service", service.ID) + } + + tasks, err := client.TaskList(ctx, types.TaskListOptions{Filter: taskFilter}) + if err != nil { + return err + } + nodes, err := client.NodeList(ctx, types.NodeListOptions{}) + if err != nil { + return err + } + service.PrintNotQuiet(out, services, nodes, tasks) + } + return nil +} diff --git a/components/cli/command/swarm/cmd.go b/components/cli/command/swarm/cmd.go new file mode 100644 index 0000000000..db2b6a2530 --- /dev/null +++ b/components/cli/command/swarm/cmd.go @@ -0,0 +1,30 @@ +package swarm + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" +) + +// NewSwarmCommand returns a cobra command for `swarm` subcommands +func NewSwarmCommand(dockerCli *command.DockerCli) *cobra.Command { + cmd := &cobra.Command{ + Use: "swarm", + Short: "Manage Docker Swarm", + Args: cli.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + }, + } + cmd.AddCommand( + newInitCommand(dockerCli), + newJoinCommand(dockerCli), + newJoinTokenCommand(dockerCli), + newUpdateCommand(dockerCli), + newLeaveCommand(dockerCli), + ) + return cmd +} diff --git a/components/cli/command/swarm/init.go b/components/cli/command/swarm/init.go new file mode 100644 index 0000000000..9a17224bde --- /dev/null +++ b/components/cli/command/swarm/init.go @@ -0,0 +1,81 @@ +package swarm + +import ( + "errors" + "fmt" + "strings" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +const ( + generatedSecretEntropyBytes = 16 + generatedSecretBase = 36 + // floor(log(2^128-1, 36)) + 1 + maxGeneratedSecretLength = 25 +) + +type initOptions struct { + swarmOptions + listenAddr NodeAddrOption + // Not a NodeAddrOption because it has no default port. + advertiseAddr string + forceNewCluster bool +} + +func newInitCommand(dockerCli *command.DockerCli) *cobra.Command { + opts := initOptions{ + listenAddr: NewListenAddrOption(), + } + + cmd := &cobra.Command{ + Use: "init [OPTIONS]", + Short: "Initialize a swarm", + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runInit(dockerCli, cmd.Flags(), opts) + }, + } + + flags := cmd.Flags() + flags.Var(&opts.listenAddr, flagListenAddr, "Listen address (format: [:port])") + flags.StringVar(&opts.advertiseAddr, flagAdvertiseAddr, "", "Advertised address (format: [:port])") + flags.BoolVar(&opts.forceNewCluster, "force-new-cluster", false, "Force create a new cluster from current state.") + addSwarmFlags(flags, &opts.swarmOptions) + return cmd +} + +func runInit(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts initOptions) error { + client := dockerCli.Client() + ctx := context.Background() + + req := swarm.InitRequest{ + ListenAddr: opts.listenAddr.String(), + AdvertiseAddr: opts.advertiseAddr, + ForceNewCluster: opts.forceNewCluster, + Spec: opts.swarmOptions.ToSpec(), + } + + nodeID, err := client.SwarmInit(ctx, req) + if err != nil { + if strings.Contains(err.Error(), "could not choose an IP address to advertise") || strings.Contains(err.Error(), "could not find the system's IP address") { + return errors.New(err.Error() + " - specify one with --advertise-addr") + } + return err + } + + fmt.Fprintf(dockerCli.Out(), "Swarm initialized: current node (%s) is now a manager.\n\n", nodeID) + + if err := printJoinCommand(ctx, dockerCli, nodeID, true, false); err != nil { + return err + } + + fmt.Fprint(dockerCli.Out(), "To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.\n\n") + return nil +} diff --git a/components/cli/command/swarm/join.go b/components/cli/command/swarm/join.go new file mode 100644 index 0000000000..72f97c015e --- /dev/null +++ b/components/cli/command/swarm/join.go @@ -0,0 +1,75 @@ +package swarm + +import ( + "fmt" + "strings" + + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" + "golang.org/x/net/context" +) + +type joinOptions struct { + remote string + listenAddr NodeAddrOption + // Not a NodeAddrOption because it has no default port. + advertiseAddr string + token string +} + +func newJoinCommand(dockerCli *command.DockerCli) *cobra.Command { + opts := joinOptions{ + listenAddr: NewListenAddrOption(), + } + + cmd := &cobra.Command{ + Use: "join [OPTIONS] HOST:PORT", + Short: "Join a swarm as a node and/or manager", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.remote = args[0] + return runJoin(dockerCli, opts) + }, + } + + flags := cmd.Flags() + flags.Var(&opts.listenAddr, flagListenAddr, "Listen address (format: [:port])") + flags.StringVar(&opts.advertiseAddr, flagAdvertiseAddr, "", "Advertised address (format: [:port])") + flags.StringVar(&opts.token, flagToken, "", "Token for entry into the swarm") + return cmd +} + +func runJoin(dockerCli *command.DockerCli, opts joinOptions) error { + client := dockerCli.Client() + ctx := context.Background() + + req := swarm.JoinRequest{ + JoinToken: opts.token, + ListenAddr: opts.listenAddr.String(), + AdvertiseAddr: opts.advertiseAddr, + RemoteAddrs: []string{opts.remote}, + } + err := client.SwarmJoin(ctx, req) + if err != nil { + return err + } + + info, err := client.Info(ctx) + if err != nil { + return err + } + + _, _, err = client.NodeInspectWithRaw(ctx, info.Swarm.NodeID) + if err != nil { + // TODO(aaronl): is there a better way to do this? + if strings.Contains(err.Error(), "This node is not a swarm manager.") { + fmt.Fprintln(dockerCli.Out(), "This node joined a swarm as a worker.") + } + } else { + fmt.Fprintln(dockerCli.Out(), "This node joined a swarm as a manager.") + } + + return nil +} diff --git a/components/cli/command/swarm/join_token.go b/components/cli/command/swarm/join_token.go new file mode 100644 index 0000000000..b411202083 --- /dev/null +++ b/components/cli/command/swarm/join_token.go @@ -0,0 +1,105 @@ +package swarm + +import ( + "errors" + "fmt" + + "github.com/spf13/cobra" + + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "golang.org/x/net/context" +) + +func newJoinTokenCommand(dockerCli *command.DockerCli) *cobra.Command { + var rotate, quiet bool + + cmd := &cobra.Command{ + Use: "join-token [OPTIONS] (worker|manager)", + Short: "Manage join tokens", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + worker := args[0] == "worker" + manager := args[0] == "manager" + + if !worker && !manager { + return errors.New("unknown role " + args[0]) + } + + client := dockerCli.Client() + ctx := context.Background() + + if rotate { + var flags swarm.UpdateFlags + + swarm, err := client.SwarmInspect(ctx) + if err != nil { + return err + } + + flags.RotateWorkerToken = worker + flags.RotateManagerToken = manager + + err = client.SwarmUpdate(ctx, swarm.Version, swarm.Spec, flags) + if err != nil { + return err + } + if !quiet { + fmt.Fprintf(dockerCli.Out(), "Succesfully rotated %s join token.\n\n", args[0]) + } + } + + swarm, err := client.SwarmInspect(ctx) + if err != nil { + return err + } + + if quiet { + if worker { + fmt.Fprintln(dockerCli.Out(), swarm.JoinTokens.Worker) + } else { + fmt.Fprintln(dockerCli.Out(), swarm.JoinTokens.Manager) + } + } else { + info, err := client.Info(ctx) + if err != nil { + return err + } + return printJoinCommand(ctx, dockerCli, info.Swarm.NodeID, worker, manager) + } + return nil + }, + } + + flags := cmd.Flags() + flags.BoolVar(&rotate, flagRotate, false, "Rotate join token") + flags.BoolVarP(&quiet, flagQuiet, "q", false, "Only display token") + + return cmd +} + +func printJoinCommand(ctx context.Context, dockerCli *command.DockerCli, nodeID string, worker bool, manager bool) error { + client := dockerCli.Client() + + swarm, err := client.SwarmInspect(ctx) + if err != nil { + return err + } + + node, _, err := client.NodeInspectWithRaw(ctx, nodeID) + if err != nil { + return err + } + + if node.ManagerStatus != nil { + if worker { + fmt.Fprintf(dockerCli.Out(), "To add a worker to this swarm, run the following command:\n\n docker swarm join \\\n --token %s \\\n %s\n\n", swarm.JoinTokens.Worker, node.ManagerStatus.Addr) + } + if manager { + fmt.Fprintf(dockerCli.Out(), "To add a manager to this swarm, run the following command:\n\n docker swarm join \\\n --token %s \\\n %s\n\n", swarm.JoinTokens.Manager, node.ManagerStatus.Addr) + } + } + + return nil +} diff --git a/components/cli/command/swarm/leave.go b/components/cli/command/swarm/leave.go new file mode 100644 index 0000000000..9224113409 --- /dev/null +++ b/components/cli/command/swarm/leave.go @@ -0,0 +1,44 @@ +package swarm + +import ( + "fmt" + + "golang.org/x/net/context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +type leaveOptions struct { + force bool +} + +func newLeaveCommand(dockerCli *command.DockerCli) *cobra.Command { + opts := leaveOptions{} + + cmd := &cobra.Command{ + Use: "leave [OPTIONS]", + Short: "Leave a swarm", + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runLeave(dockerCli, opts) + }, + } + + flags := cmd.Flags() + flags.BoolVar(&opts.force, "force", false, "Force leave ignoring warnings.") + return cmd +} + +func runLeave(dockerCli *command.DockerCli, opts leaveOptions) error { + client := dockerCli.Client() + ctx := context.Background() + + if err := client.SwarmLeave(ctx, opts.force); err != nil { + return err + } + + fmt.Fprintln(dockerCli.Out(), "Node left the swarm.") + return nil +} diff --git a/components/cli/command/swarm/opts.go b/components/cli/command/swarm/opts.go new file mode 100644 index 0000000000..7fcf25d136 --- /dev/null +++ b/components/cli/command/swarm/opts.go @@ -0,0 +1,179 @@ +package swarm + +import ( + "encoding/csv" + "errors" + "fmt" + "strings" + "time" + + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/opts" + "github.com/spf13/pflag" +) + +const ( + defaultListenAddr = "0.0.0.0:2377" + + flagCertExpiry = "cert-expiry" + flagDispatcherHeartbeat = "dispatcher-heartbeat" + flagListenAddr = "listen-addr" + flagAdvertiseAddr = "advertise-addr" + flagQuiet = "quiet" + flagRotate = "rotate" + flagToken = "token" + flagTaskHistoryLimit = "task-history-limit" + flagExternalCA = "external-ca" +) + +type swarmOptions struct { + taskHistoryLimit int64 + dispatcherHeartbeat time.Duration + nodeCertExpiry time.Duration + externalCA ExternalCAOption +} + +// NodeAddrOption is a pflag.Value for listen and remote addresses +type NodeAddrOption struct { + addr string +} + +// String prints the representation of this flag +func (a *NodeAddrOption) String() string { + return a.Value() +} + +// Set the value for this flag +func (a *NodeAddrOption) Set(value string) error { + addr, err := opts.ParseTCPAddr(value, a.addr) + if err != nil { + return err + } + a.addr = addr + return nil +} + +// Type returns the type of this flag +func (a *NodeAddrOption) Type() string { + return "node-addr" +} + +// Value returns the value of this option as addr:port +func (a *NodeAddrOption) Value() string { + return strings.TrimPrefix(a.addr, "tcp://") +} + +// NewNodeAddrOption returns a new node address option +func NewNodeAddrOption(addr string) NodeAddrOption { + return NodeAddrOption{addr} +} + +// NewListenAddrOption returns a NodeAddrOption with default values +func NewListenAddrOption() NodeAddrOption { + return NewNodeAddrOption(defaultListenAddr) +} + +// ExternalCAOption is a Value type for parsing external CA specifications. +type ExternalCAOption struct { + values []*swarm.ExternalCA +} + +// Set parses an external CA option. +func (m *ExternalCAOption) Set(value string) error { + parsed, err := parseExternalCA(value) + if err != nil { + return err + } + + m.values = append(m.values, parsed) + return nil +} + +// Type returns the type of this option. +func (m *ExternalCAOption) Type() string { + return "external-ca" +} + +// String returns a string repr of this option. +func (m *ExternalCAOption) String() string { + externalCAs := []string{} + for _, externalCA := range m.values { + repr := fmt.Sprintf("%s: %s", externalCA.Protocol, externalCA.URL) + externalCAs = append(externalCAs, repr) + } + return strings.Join(externalCAs, ", ") +} + +// Value returns the external CAs +func (m *ExternalCAOption) Value() []*swarm.ExternalCA { + return m.values +} + +// parseExternalCA parses an external CA specification from the command line, +// such as protocol=cfssl,url=https://example.com. +func parseExternalCA(caSpec string) (*swarm.ExternalCA, error) { + csvReader := csv.NewReader(strings.NewReader(caSpec)) + fields, err := csvReader.Read() + if err != nil { + return nil, err + } + + externalCA := swarm.ExternalCA{ + Options: make(map[string]string), + } + + var ( + hasProtocol bool + hasURL bool + ) + + for _, field := range fields { + parts := strings.SplitN(field, "=", 2) + + if len(parts) != 2 { + return nil, fmt.Errorf("invalid field '%s' must be a key=value pair", field) + } + + key, value := parts[0], parts[1] + + switch strings.ToLower(key) { + case "protocol": + hasProtocol = true + if strings.ToLower(value) == string(swarm.ExternalCAProtocolCFSSL) { + externalCA.Protocol = swarm.ExternalCAProtocolCFSSL + } else { + return nil, fmt.Errorf("unrecognized external CA protocol %s", value) + } + case "url": + hasURL = true + externalCA.URL = value + default: + externalCA.Options[key] = value + } + } + + if !hasProtocol { + return nil, errors.New("the external-ca option needs a protocol= parameter") + } + if !hasURL { + return nil, errors.New("the external-ca option needs a url= parameter") + } + + return &externalCA, nil +} + +func addSwarmFlags(flags *pflag.FlagSet, opts *swarmOptions) { + flags.Int64Var(&opts.taskHistoryLimit, flagTaskHistoryLimit, 5, "Task history retention limit") + flags.DurationVar(&opts.dispatcherHeartbeat, flagDispatcherHeartbeat, time.Duration(5*time.Second), "Dispatcher heartbeat period") + flags.DurationVar(&opts.nodeCertExpiry, flagCertExpiry, time.Duration(90*24*time.Hour), "Validity period for node certificates") + flags.Var(&opts.externalCA, flagExternalCA, "Specifications of one or more certificate signing endpoints") +} + +func (opts *swarmOptions) ToSpec() swarm.Spec { + spec := swarm.Spec{} + spec.Orchestration.TaskHistoryRetentionLimit = opts.taskHistoryLimit + spec.Dispatcher.HeartbeatPeriod = opts.dispatcherHeartbeat + spec.CAConfig.NodeCertExpiry = opts.nodeCertExpiry + spec.CAConfig.ExternalCAs = opts.externalCA.Value() + return spec +} diff --git a/components/cli/command/swarm/opts_test.go b/components/cli/command/swarm/opts_test.go new file mode 100644 index 0000000000..568dc87302 --- /dev/null +++ b/components/cli/command/swarm/opts_test.go @@ -0,0 +1,37 @@ +package swarm + +import ( + "testing" + + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestNodeAddrOptionSetHostAndPort(t *testing.T) { + opt := NewNodeAddrOption("old:123") + addr := "newhost:5555" + assert.NilError(t, opt.Set(addr)) + assert.Equal(t, opt.Value(), addr) +} + +func TestNodeAddrOptionSetHostOnly(t *testing.T) { + opt := NewListenAddrOption() + assert.NilError(t, opt.Set("newhost")) + assert.Equal(t, opt.Value(), "newhost:2377") +} + +func TestNodeAddrOptionSetHostOnlyIPv6(t *testing.T) { + opt := NewListenAddrOption() + assert.NilError(t, opt.Set("::1")) + assert.Equal(t, opt.Value(), "[::1]:2377") +} + +func TestNodeAddrOptionSetPortOnly(t *testing.T) { + opt := NewListenAddrOption() + assert.NilError(t, opt.Set(":4545")) + assert.Equal(t, opt.Value(), "0.0.0.0:4545") +} + +func TestNodeAddrOptionSetInvalidFormat(t *testing.T) { + opt := NewListenAddrOption() + assert.Error(t, opt.Set("http://localhost:4545"), "Invalid") +} diff --git a/components/cli/command/swarm/update.go b/components/cli/command/swarm/update.go new file mode 100644 index 0000000000..9884b79169 --- /dev/null +++ b/components/cli/command/swarm/update.go @@ -0,0 +1,82 @@ +package swarm + +import ( + "fmt" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { + opts := swarmOptions{} + + cmd := &cobra.Command{ + Use: "update [OPTIONS]", + Short: "Update the swarm", + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runUpdate(dockerCli, cmd.Flags(), opts) + }, + } + + addSwarmFlags(cmd.Flags(), &opts) + return cmd +} + +func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts swarmOptions) error { + client := dockerCli.Client() + ctx := context.Background() + + var updateFlags swarm.UpdateFlags + + swarm, err := client.SwarmInspect(ctx) + if err != nil { + return err + } + + err = mergeSwarm(&swarm, flags) + if err != nil { + return err + } + + err = client.SwarmUpdate(ctx, swarm.Version, swarm.Spec, updateFlags) + if err != nil { + return err + } + + fmt.Fprintln(dockerCli.Out(), "Swarm updated.") + + return nil +} + +func mergeSwarm(swarm *swarm.Swarm, flags *pflag.FlagSet) error { + spec := &swarm.Spec + + if flags.Changed(flagTaskHistoryLimit) { + spec.Orchestration.TaskHistoryRetentionLimit, _ = flags.GetInt64(flagTaskHistoryLimit) + } + + if flags.Changed(flagDispatcherHeartbeat) { + if v, err := flags.GetDuration(flagDispatcherHeartbeat); err == nil { + spec.Dispatcher.HeartbeatPeriod = v + } + } + + if flags.Changed(flagCertExpiry) { + if v, err := flags.GetDuration(flagCertExpiry); err == nil { + spec.CAConfig.NodeCertExpiry = v + } + } + + if flags.Changed(flagExternalCA) { + value := flags.Lookup(flagExternalCA).Value.(*ExternalCAOption) + spec.CAConfig.ExternalCAs = value.Value() + } + + return nil +} diff --git a/components/cli/command/system/events.go b/components/cli/command/system/events.go new file mode 100644 index 0000000000..456e81b4ce --- /dev/null +++ b/components/cli/command/system/events.go @@ -0,0 +1,115 @@ +package system + +import ( + "fmt" + "io" + "sort" + "strings" + "time" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + eventtypes "github.com/docker/docker/api/types/events" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/pkg/jsonlog" + "github.com/spf13/cobra" +) + +type eventsOptions struct { + since string + until string + filter []string +} + +// NewEventsCommand creates a new cobra.Command for `docker events` +func NewEventsCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts eventsOptions + + cmd := &cobra.Command{ + Use: "events [OPTIONS]", + Short: "Get real time events from the server", + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runEvents(dockerCli, &opts) + }, + } + + flags := cmd.Flags() + flags.StringVar(&opts.since, "since", "", "Show all events created since timestamp") + flags.StringVar(&opts.until, "until", "", "Stream events until this timestamp") + flags.StringSliceVarP(&opts.filter, "filter", "f", []string{}, "Filter output based on conditions provided") + + return cmd +} + +func runEvents(dockerCli *command.DockerCli, opts *eventsOptions) error { + eventFilterArgs := filters.NewArgs() + + // Consolidate all filter flags, and sanity check them early. + // They'll get process in the daemon/server. + for _, f := range opts.filter { + var err error + eventFilterArgs, err = filters.ParseFlag(f, eventFilterArgs) + if err != nil { + return err + } + } + + options := types.EventsOptions{ + Since: opts.since, + Until: opts.until, + Filters: eventFilterArgs, + } + + responseBody, err := dockerCli.Client().Events(context.Background(), options) + if err != nil { + return err + } + defer responseBody.Close() + + return streamEvents(responseBody, dockerCli.Out()) +} + +// streamEvents decodes prints the incoming events in the provided output. +func streamEvents(input io.Reader, output io.Writer) error { + return DecodeEvents(input, func(event eventtypes.Message, err error) error { + if err != nil { + return err + } + printOutput(event, output) + return nil + }) +} + +type eventProcessor func(event eventtypes.Message, err error) error + +// printOutput prints all types of event information. +// Each output includes the event type, actor id, name and action. +// Actor attributes are printed at the end if the actor has any. +func printOutput(event eventtypes.Message, output io.Writer) { + if event.TimeNano != 0 { + fmt.Fprintf(output, "%s ", time.Unix(0, event.TimeNano).Format(jsonlog.RFC3339NanoFixed)) + } else if event.Time != 0 { + fmt.Fprintf(output, "%s ", time.Unix(event.Time, 0).Format(jsonlog.RFC3339NanoFixed)) + } + + fmt.Fprintf(output, "%s %s %s", event.Type, event.Action, event.Actor.ID) + + if len(event.Actor.Attributes) > 0 { + var attrs []string + var keys []string + for k := range event.Actor.Attributes { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + v := event.Actor.Attributes[k] + attrs = append(attrs, fmt.Sprintf("%s=%s", k, v)) + } + fmt.Fprintf(output, " (%s)", strings.Join(attrs, ", ")) + } + fmt.Fprint(output, "\n") +} diff --git a/components/cli/command/system/events_utils.go b/components/cli/command/system/events_utils.go new file mode 100644 index 0000000000..71c1b0476b --- /dev/null +++ b/components/cli/command/system/events_utils.go @@ -0,0 +1,66 @@ +package system + +import ( + "encoding/json" + "io" + "sync" + + "github.com/Sirupsen/logrus" + eventtypes "github.com/docker/docker/api/types/events" +) + +// EventHandler is abstract interface for user to customize +// own handle functions of each type of events +type EventHandler interface { + Handle(action string, h func(eventtypes.Message)) + Watch(c <-chan eventtypes.Message) +} + +// InitEventHandler initializes and returns an EventHandler +func InitEventHandler() EventHandler { + return &eventHandler{handlers: make(map[string]func(eventtypes.Message))} +} + +type eventHandler struct { + handlers map[string]func(eventtypes.Message) + mu sync.Mutex +} + +func (w *eventHandler) Handle(action string, h func(eventtypes.Message)) { + w.mu.Lock() + w.handlers[action] = h + w.mu.Unlock() +} + +// Watch ranges over the passed in event chan and processes the events based on the +// handlers created for a given action. +// To stop watching, close the event chan. +func (w *eventHandler) Watch(c <-chan eventtypes.Message) { + for e := range c { + w.mu.Lock() + h, exists := w.handlers[e.Action] + w.mu.Unlock() + if !exists { + continue + } + logrus.Debugf("event handler: received event: %v", e) + go h(e) + } +} + +// DecodeEvents decodes event from input stream +func DecodeEvents(input io.Reader, ep eventProcessor) error { + dec := json.NewDecoder(input) + for { + var event eventtypes.Message + err := dec.Decode(&event) + if err != nil && err == io.EOF { + break + } + + if procErr := ep(event, err); procErr != nil { + return procErr + } + } + return nil +} diff --git a/components/cli/command/system/info.go b/components/cli/command/system/info.go new file mode 100644 index 0000000000..259b254bd0 --- /dev/null +++ b/components/cli/command/system/info.go @@ -0,0 +1,261 @@ +package system + +import ( + "fmt" + "strings" + "time" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/pkg/ioutils" + "github.com/docker/docker/utils" + "github.com/docker/docker/utils/templates" + "github.com/docker/go-units" + "github.com/spf13/cobra" +) + +type infoOptions struct { + format string +} + +// NewInfoCommand creates a new cobra.Command for `docker info` +func NewInfoCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts infoOptions + + cmd := &cobra.Command{ + Use: "info [OPTIONS]", + Short: "Display system-wide information", + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runInfo(dockerCli, &opts) + }, + } + + flags := cmd.Flags() + + flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template") + + return cmd +} + +func runInfo(dockerCli *command.DockerCli, opts *infoOptions) error { + ctx := context.Background() + info, err := dockerCli.Client().Info(ctx) + if err != nil { + return err + } + if opts.format == "" { + return prettyPrintInfo(dockerCli, info) + } + return formatInfo(dockerCli, info, opts.format) +} + +func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error { + fmt.Fprintf(dockerCli.Out(), "Containers: %d\n", info.Containers) + fmt.Fprintf(dockerCli.Out(), " Running: %d\n", info.ContainersRunning) + fmt.Fprintf(dockerCli.Out(), " Paused: %d\n", info.ContainersPaused) + fmt.Fprintf(dockerCli.Out(), " Stopped: %d\n", info.ContainersStopped) + fmt.Fprintf(dockerCli.Out(), "Images: %d\n", info.Images) + ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Server Version: %s\n", info.ServerVersion) + ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Storage Driver: %s\n", info.Driver) + if info.DriverStatus != nil { + for _, pair := range info.DriverStatus { + fmt.Fprintf(dockerCli.Out(), " %s: %s\n", pair[0], pair[1]) + + // print a warning if devicemapper is using a loopback file + if pair[0] == "Data loop file" { + fmt.Fprintln(dockerCli.Err(), " WARNING: Usage of loopback devices is strongly discouraged for production use. Use `--storage-opt dm.thinpooldev` to specify a custom block storage device.") + } + } + + } + if info.SystemStatus != nil { + for _, pair := range info.SystemStatus { + fmt.Fprintf(dockerCli.Out(), "%s: %s\n", pair[0], pair[1]) + } + } + ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Logging Driver: %s\n", info.LoggingDriver) + ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Cgroup Driver: %s\n", info.CgroupDriver) + + fmt.Fprintf(dockerCli.Out(), "Plugins: \n") + fmt.Fprintf(dockerCli.Out(), " Volume:") + fmt.Fprintf(dockerCli.Out(), " %s", strings.Join(info.Plugins.Volume, " ")) + fmt.Fprintf(dockerCli.Out(), "\n") + fmt.Fprintf(dockerCli.Out(), " Network:") + fmt.Fprintf(dockerCli.Out(), " %s", strings.Join(info.Plugins.Network, " ")) + fmt.Fprintf(dockerCli.Out(), "\n") + + if len(info.Plugins.Authorization) != 0 { + fmt.Fprintf(dockerCli.Out(), " Authorization:") + fmt.Fprintf(dockerCli.Out(), " %s", strings.Join(info.Plugins.Authorization, " ")) + fmt.Fprintf(dockerCli.Out(), "\n") + } + + fmt.Fprintf(dockerCli.Out(), "Swarm: %v\n", info.Swarm.LocalNodeState) + if info.Swarm.LocalNodeState != swarm.LocalNodeStateInactive { + fmt.Fprintf(dockerCli.Out(), " NodeID: %s\n", info.Swarm.NodeID) + if info.Swarm.Error != "" { + fmt.Fprintf(dockerCli.Out(), " Error: %v\n", info.Swarm.Error) + } + fmt.Fprintf(dockerCli.Out(), " Is Manager: %v\n", info.Swarm.ControlAvailable) + if info.Swarm.ControlAvailable { + fmt.Fprintf(dockerCli.Out(), " ClusterID: %s\n", info.Swarm.Cluster.ID) + fmt.Fprintf(dockerCli.Out(), " Managers: %d\n", info.Swarm.Managers) + fmt.Fprintf(dockerCli.Out(), " Nodes: %d\n", info.Swarm.Nodes) + fmt.Fprintf(dockerCli.Out(), " Orchestration:\n") + fmt.Fprintf(dockerCli.Out(), " Task History Retention Limit: %d\n", info.Swarm.Cluster.Spec.Orchestration.TaskHistoryRetentionLimit) + fmt.Fprintf(dockerCli.Out(), " Raft:\n") + fmt.Fprintf(dockerCli.Out(), " Snapshot Interval: %d\n", info.Swarm.Cluster.Spec.Raft.SnapshotInterval) + fmt.Fprintf(dockerCli.Out(), " Heartbeat Tick: %d\n", info.Swarm.Cluster.Spec.Raft.HeartbeatTick) + fmt.Fprintf(dockerCli.Out(), " Election Tick: %d\n", info.Swarm.Cluster.Spec.Raft.ElectionTick) + fmt.Fprintf(dockerCli.Out(), " Dispatcher:\n") + fmt.Fprintf(dockerCli.Out(), " Heartbeat Period: %s\n", units.HumanDuration(time.Duration(info.Swarm.Cluster.Spec.Dispatcher.HeartbeatPeriod))) + fmt.Fprintf(dockerCli.Out(), " CA Configuration:\n") + fmt.Fprintf(dockerCli.Out(), " Expiry Duration: %s\n", units.HumanDuration(info.Swarm.Cluster.Spec.CAConfig.NodeCertExpiry)) + if len(info.Swarm.Cluster.Spec.CAConfig.ExternalCAs) > 0 { + fmt.Fprintf(dockerCli.Out(), " External CAs:\n") + for _, entry := range info.Swarm.Cluster.Spec.CAConfig.ExternalCAs { + fmt.Fprintf(dockerCli.Out(), " %s: %s\n", entry.Protocol, entry.URL) + } + } + } + fmt.Fprintf(dockerCli.Out(), " Node Address: %s\n", info.Swarm.NodeAddr) + } + + if len(info.Runtimes) > 0 { + fmt.Fprintf(dockerCli.Out(), "Runtimes:") + for name := range info.Runtimes { + fmt.Fprintf(dockerCli.Out(), " %s", name) + } + fmt.Fprint(dockerCli.Out(), "\n") + fmt.Fprintf(dockerCli.Out(), "Default Runtime: %s\n", info.DefaultRuntime) + } + + fmt.Fprintf(dockerCli.Out(), "Security Options:") + ioutils.FprintfIfNotEmpty(dockerCli.Out(), " %s", strings.Join(info.SecurityOptions, " ")) + fmt.Fprintf(dockerCli.Out(), "\n") + + ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Kernel Version: %s\n", info.KernelVersion) + ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Operating System: %s\n", info.OperatingSystem) + ioutils.FprintfIfNotEmpty(dockerCli.Out(), "OSType: %s\n", info.OSType) + ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Architecture: %s\n", info.Architecture) + fmt.Fprintf(dockerCli.Out(), "CPUs: %d\n", info.NCPU) + fmt.Fprintf(dockerCli.Out(), "Total Memory: %s\n", units.BytesSize(float64(info.MemTotal))) + ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Name: %s\n", info.Name) + ioutils.FprintfIfNotEmpty(dockerCli.Out(), "ID: %s\n", info.ID) + fmt.Fprintf(dockerCli.Out(), "Docker Root Dir: %s\n", info.DockerRootDir) + fmt.Fprintf(dockerCli.Out(), "Debug Mode (client): %v\n", utils.IsDebugEnabled()) + fmt.Fprintf(dockerCli.Out(), "Debug Mode (server): %v\n", info.Debug) + + if info.Debug { + fmt.Fprintf(dockerCli.Out(), " File Descriptors: %d\n", info.NFd) + fmt.Fprintf(dockerCli.Out(), " Goroutines: %d\n", info.NGoroutines) + fmt.Fprintf(dockerCli.Out(), " System Time: %s\n", info.SystemTime) + fmt.Fprintf(dockerCli.Out(), " EventsListeners: %d\n", info.NEventsListener) + } + + ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Http Proxy: %s\n", info.HTTPProxy) + ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Https Proxy: %s\n", info.HTTPSProxy) + ioutils.FprintfIfNotEmpty(dockerCli.Out(), "No Proxy: %s\n", info.NoProxy) + + if info.IndexServerAddress != "" { + u := dockerCli.ConfigFile().AuthConfigs[info.IndexServerAddress].Username + if len(u) > 0 { + fmt.Fprintf(dockerCli.Out(), "Username: %v\n", u) + } + fmt.Fprintf(dockerCli.Out(), "Registry: %v\n", info.IndexServerAddress) + } + + // Only output these warnings if the server does not support these features + if info.OSType != "windows" { + if !info.MemoryLimit { + fmt.Fprintln(dockerCli.Err(), "WARNING: No memory limit support") + } + if !info.SwapLimit { + fmt.Fprintln(dockerCli.Err(), "WARNING: No swap limit support") + } + if !info.KernelMemory { + fmt.Fprintln(dockerCli.Err(), "WARNING: No kernel memory limit support") + } + if !info.OomKillDisable { + fmt.Fprintln(dockerCli.Err(), "WARNING: No oom kill disable support") + } + if !info.CPUCfsQuota { + fmt.Fprintln(dockerCli.Err(), "WARNING: No cpu cfs quota support") + } + if !info.CPUCfsPeriod { + fmt.Fprintln(dockerCli.Err(), "WARNING: No cpu cfs period support") + } + if !info.CPUShares { + fmt.Fprintln(dockerCli.Err(), "WARNING: No cpu shares support") + } + if !info.CPUSet { + fmt.Fprintln(dockerCli.Err(), "WARNING: No cpuset support") + } + if !info.IPv4Forwarding { + fmt.Fprintln(dockerCli.Err(), "WARNING: IPv4 forwarding is disabled") + } + if !info.BridgeNfIptables { + fmt.Fprintln(dockerCli.Err(), "WARNING: bridge-nf-call-iptables is disabled") + } + if !info.BridgeNfIP6tables { + fmt.Fprintln(dockerCli.Err(), "WARNING: bridge-nf-call-ip6tables is disabled") + } + } + + if info.Labels != nil { + fmt.Fprintln(dockerCli.Out(), "Labels:") + for _, attribute := range info.Labels { + fmt.Fprintf(dockerCli.Out(), " %s\n", attribute) + } + } + + ioutils.FprintfIfTrue(dockerCli.Out(), "Experimental: %v\n", info.ExperimentalBuild) + if info.ClusterStore != "" { + fmt.Fprintf(dockerCli.Out(), "Cluster Store: %s\n", info.ClusterStore) + } + + if info.ClusterAdvertise != "" { + fmt.Fprintf(dockerCli.Out(), "Cluster Advertise: %s\n", info.ClusterAdvertise) + } + + if info.RegistryConfig != nil && (len(info.RegistryConfig.InsecureRegistryCIDRs) > 0 || len(info.RegistryConfig.IndexConfigs) > 0) { + fmt.Fprintln(dockerCli.Out(), "Insecure Registries:") + for _, registry := range info.RegistryConfig.IndexConfigs { + if registry.Secure == false { + fmt.Fprintf(dockerCli.Out(), " %s\n", registry.Name) + } + } + + for _, registry := range info.RegistryConfig.InsecureRegistryCIDRs { + mask, _ := registry.Mask.Size() + fmt.Fprintf(dockerCli.Out(), " %s/%d\n", registry.IP.String(), mask) + } + } + + if info.RegistryConfig != nil && len(info.RegistryConfig.Mirrors) > 0 { + fmt.Fprintln(dockerCli.Out(), "Registry Mirrors:") + for _, mirror := range info.RegistryConfig.Mirrors { + fmt.Fprintf(dockerCli.Out(), " %s\n", mirror) + } + } + + fmt.Fprintf(dockerCli.Out(), "Live Restore Enabled: %v\n", info.LiveRestoreEnabled) + + return nil +} + +func formatInfo(dockerCli *command.DockerCli, info types.Info, format string) error { + tmpl, err := templates.Parse(format) + if err != nil { + return cli.StatusError{StatusCode: 64, + Status: "Template parsing error: " + err.Error()} + } + err = tmpl.Execute(dockerCli.Out(), info) + dockerCli.Out().Write([]byte{'\n'}) + return err +} diff --git a/components/cli/command/system/inspect.go b/components/cli/command/system/inspect.go new file mode 100644 index 0000000000..e4f67cf643 --- /dev/null +++ b/components/cli/command/system/inspect.go @@ -0,0 +1,136 @@ +package system + +import ( + "fmt" + "strings" + + "golang.org/x/net/context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/inspect" + apiclient "github.com/docker/docker/client" + "github.com/spf13/cobra" +) + +type inspectOptions struct { + format string + inspectType string + size bool + ids []string +} + +// NewInspectCommand creates a new cobra.Command for `docker inspect` +func NewInspectCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts inspectOptions + + cmd := &cobra.Command{ + Use: "inspect [OPTIONS] CONTAINER|IMAGE|TASK [CONTAINER|IMAGE|TASK...]", + Short: "Return low-level information on a container, image or task", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.ids = args + return runInspect(dockerCli, opts) + }, + } + + flags := cmd.Flags() + flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template") + flags.StringVar(&opts.inspectType, "type", "", "Return JSON for specified type") + flags.BoolVarP(&opts.size, "size", "s", false, "Display total file sizes if the type is container") + + return cmd +} + +func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { + var elementSearcher inspect.GetRefFunc + switch opts.inspectType { + case "", "container", "image", "node", "network", "service", "volume", "task": + elementSearcher = inspectAll(context.Background(), dockerCli, opts.size, opts.inspectType) + default: + return fmt.Errorf("%q is not a valid value for --type", opts.inspectType) + } + return inspect.Inspect(dockerCli.Out(), opts.ids, opts.format, elementSearcher) +} + +func inspectContainers(ctx context.Context, dockerCli *command.DockerCli, getSize bool) inspect.GetRefFunc { + return func(ref string) (interface{}, []byte, error) { + return dockerCli.Client().ContainerInspectWithRaw(ctx, ref, getSize) + } +} + +func inspectImages(ctx context.Context, dockerCli *command.DockerCli) inspect.GetRefFunc { + return func(ref string) (interface{}, []byte, error) { + return dockerCli.Client().ImageInspectWithRaw(ctx, ref) + } +} + +func inspectNetwork(ctx context.Context, dockerCli *command.DockerCli) inspect.GetRefFunc { + return func(ref string) (interface{}, []byte, error) { + return dockerCli.Client().NetworkInspectWithRaw(ctx, ref) + } +} + +func inspectNode(ctx context.Context, dockerCli *command.DockerCli) inspect.GetRefFunc { + return func(ref string) (interface{}, []byte, error) { + return dockerCli.Client().NodeInspectWithRaw(ctx, ref) + } +} + +func inspectService(ctx context.Context, dockerCli *command.DockerCli) inspect.GetRefFunc { + return func(ref string) (interface{}, []byte, error) { + return dockerCli.Client().ServiceInspectWithRaw(ctx, ref) + } +} + +func inspectTasks(ctx context.Context, dockerCli *command.DockerCli) inspect.GetRefFunc { + return func(ref string) (interface{}, []byte, error) { + return dockerCli.Client().TaskInspectWithRaw(ctx, ref) + } +} + +func inspectVolume(ctx context.Context, dockerCli *command.DockerCli) inspect.GetRefFunc { + return func(ref string) (interface{}, []byte, error) { + return dockerCli.Client().VolumeInspectWithRaw(ctx, ref) + } +} + +func inspectAll(ctx context.Context, dockerCli *command.DockerCli, getSize bool, typeConstraint string) inspect.GetRefFunc { + var inspectAutodetect = []struct { + ObjectType string + IsSizeSupported bool + ObjectInspector func(string) (interface{}, []byte, error) + }{ + {"container", true, inspectContainers(ctx, dockerCli, getSize)}, + {"image", true, inspectImages(ctx, dockerCli)}, + {"network", false, inspectNetwork(ctx, dockerCli)}, + {"volume", false, inspectVolume(ctx, dockerCli)}, + {"service", false, inspectService(ctx, dockerCli)}, + {"task", false, inspectTasks(ctx, dockerCli)}, + {"node", false, inspectNode(ctx, dockerCli)}, + } + + isErrNotSwarmManager := func(err error) bool { + return strings.Contains(err.Error(), "This node is not a swarm manager") + } + + return func(ref string) (interface{}, []byte, error) { + for _, inspectData := range inspectAutodetect { + if typeConstraint != "" && inspectData.ObjectType != typeConstraint { + continue + } + v, raw, err := inspectData.ObjectInspector(ref) + if err != nil { + if typeConstraint == "" && (apiclient.IsErrNotFound(err) || isErrNotSwarmManager(err)) { + continue + } + return v, raw, err + } + if !inspectData.IsSizeSupported { + fmt.Fprintf(dockerCli.Err(), "WARNING: --size ignored for %s\n", inspectData.ObjectType) + } + return v, raw, err + } + return nil, nil, fmt.Errorf("Error: No such object: %s", ref) + } +} diff --git a/components/cli/command/system/version.go b/components/cli/command/system/version.go new file mode 100644 index 0000000000..e77719ec3b --- /dev/null +++ b/components/cli/command/system/version.go @@ -0,0 +1,110 @@ +package system + +import ( + "runtime" + "time" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/dockerversion" + "github.com/docker/docker/utils" + "github.com/docker/docker/utils/templates" + "github.com/spf13/cobra" +) + +var versionTemplate = `Client: + Version: {{.Client.Version}} + API version: {{.Client.APIVersion}} + Go version: {{.Client.GoVersion}} + Git commit: {{.Client.GitCommit}} + Built: {{.Client.BuildTime}} + OS/Arch: {{.Client.Os}}/{{.Client.Arch}}{{if .Client.Experimental}} + Experimental: {{.Client.Experimental}}{{end}}{{if .ServerOK}} + +Server: + Version: {{.Server.Version}} + API version: {{.Server.APIVersion}} + Go version: {{.Server.GoVersion}} + Git commit: {{.Server.GitCommit}} + Built: {{.Server.BuildTime}} + OS/Arch: {{.Server.Os}}/{{.Server.Arch}}{{if .Server.Experimental}} + Experimental: {{.Server.Experimental}}{{end}}{{end}}` + +type versionOptions struct { + format string +} + +// NewVersionCommand creates a new cobra.Command for `docker version` +func NewVersionCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts versionOptions + + cmd := &cobra.Command{ + Use: "version [OPTIONS]", + Short: "Show the Docker version information", + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runVersion(dockerCli, &opts) + }, + } + + flags := cmd.Flags() + + flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template") + + return cmd +} + +func runVersion(dockerCli *command.DockerCli, opts *versionOptions) error { + ctx := context.Background() + + templateFormat := versionTemplate + if opts.format != "" { + templateFormat = opts.format + } + + tmpl, err := templates.Parse(templateFormat) + if err != nil { + return cli.StatusError{StatusCode: 64, + Status: "Template parsing error: " + err.Error()} + } + + vd := types.VersionResponse{ + Client: &types.Version{ + Version: dockerversion.Version, + APIVersion: dockerCli.Client().ClientVersion(), + GoVersion: runtime.Version(), + GitCommit: dockerversion.GitCommit, + BuildTime: dockerversion.BuildTime, + Os: runtime.GOOS, + Arch: runtime.GOARCH, + Experimental: utils.ExperimentalBuild(), + }, + } + + serverVersion, err := dockerCli.Client().ServerVersion(ctx) + if err == nil { + vd.Server = &serverVersion + } + + // first we need to make BuildTime more human friendly + t, errTime := time.Parse(time.RFC3339Nano, vd.Client.BuildTime) + if errTime == nil { + vd.Client.BuildTime = t.Format(time.ANSIC) + } + + if vd.ServerOK() { + t, errTime = time.Parse(time.RFC3339Nano, vd.Server.BuildTime) + if errTime == nil { + vd.Server.BuildTime = t.Format(time.ANSIC) + } + } + + if err2 := tmpl.Execute(dockerCli.Out(), vd); err2 != nil && err == nil { + err = err2 + } + dockerCli.Out().Write([]byte{'\n'}) + return err +} diff --git a/components/cli/command/task/print.go b/components/cli/command/task/print.go new file mode 100644 index 0000000000..963aea95ce --- /dev/null +++ b/components/cli/command/task/print.go @@ -0,0 +1,100 @@ +package task + +import ( + "fmt" + "sort" + "strings" + "text/tabwriter" + "time" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/idresolver" + "github.com/docker/go-units" +) + +const ( + psTaskItemFmt = "%s\t%s\t%s\t%s\t%s\t%s %s ago\t%s\n" + maxErrLength = 30 +) + +type tasksBySlot []swarm.Task + +func (t tasksBySlot) Len() int { + return len(t) +} + +func (t tasksBySlot) Swap(i, j int) { + t[i], t[j] = t[j], t[i] +} + +func (t tasksBySlot) Less(i, j int) bool { + // Sort by slot. + if t[i].Slot != t[j].Slot { + return t[i].Slot < t[j].Slot + } + + // If same slot, sort by most recent. + return t[j].Meta.CreatedAt.Before(t[i].CreatedAt) +} + +// Print task information in a table format +func Print(dockerCli *command.DockerCli, ctx context.Context, tasks []swarm.Task, resolver *idresolver.IDResolver, noTrunc bool) error { + sort.Stable(tasksBySlot(tasks)) + + writer := tabwriter.NewWriter(dockerCli.Out(), 0, 4, 2, ' ', 0) + + // Ignore flushing errors + defer writer.Flush() + fmt.Fprintln(writer, strings.Join([]string{"ID", "NAME", "IMAGE", "NODE", "DESIRED STATE", "CURRENT STATE", "ERROR"}, "\t")) + + prevName := "" + for _, task := range tasks { + serviceValue, err := resolver.Resolve(ctx, swarm.Service{}, task.ServiceID) + if err != nil { + return err + } + nodeValue, err := resolver.Resolve(ctx, swarm.Node{}, task.NodeID) + if err != nil { + return err + } + + name := serviceValue + if task.Slot > 0 { + name = fmt.Sprintf("%s.%d", name, task.Slot) + } + + // Indent the name if necessary + indentedName := name + if prevName == name { + indentedName = fmt.Sprintf(" \\_ %s", indentedName) + } + prevName = name + + // Trim and quote the error message. + taskErr := task.Status.Err + if !noTrunc && len(taskErr) > maxErrLength { + taskErr = fmt.Sprintf("%s…", taskErr[:maxErrLength-1]) + } + if len(taskErr) > 0 { + taskErr = fmt.Sprintf("\"%s\"", taskErr) + } + + fmt.Fprintf( + writer, + psTaskItemFmt, + task.ID, + indentedName, + task.Spec.ContainerSpec.Image, + nodeValue, + command.PrettyPrint(task.DesiredState), + command.PrettyPrint(task.Status.State), + strings.ToLower(units.HumanDuration(time.Since(task.Status.Timestamp))), + taskErr, + ) + } + + return nil +} diff --git a/components/cli/command/trust.go b/components/cli/command/trust.go new file mode 100644 index 0000000000..329da52515 --- /dev/null +++ b/components/cli/command/trust.go @@ -0,0 +1,598 @@ +package command + +import ( + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/url" + "os" + "path" + "path/filepath" + "sort" + "strconv" + "time" + + "golang.org/x/net/context" + + "github.com/Sirupsen/logrus" + "github.com/docker/distribution/digest" + "github.com/docker/distribution/registry/client/auth" + "github.com/docker/distribution/registry/client/transport" + "github.com/docker/docker/api/types" + registrytypes "github.com/docker/docker/api/types/registry" + "github.com/docker/docker/cliconfig" + "github.com/docker/docker/distribution" + "github.com/docker/docker/pkg/jsonmessage" + "github.com/docker/docker/reference" + "github.com/docker/docker/registry" + "github.com/docker/go-connections/tlsconfig" + "github.com/docker/notary/client" + "github.com/docker/notary/passphrase" + "github.com/docker/notary/trustmanager" + "github.com/docker/notary/trustpinning" + "github.com/docker/notary/tuf/data" + "github.com/docker/notary/tuf/signed" + "github.com/docker/notary/tuf/store" + "github.com/spf13/pflag" +) + +var ( + releasesRole = path.Join(data.CanonicalTargetsRole, "releases") + untrusted bool +) + +// AddTrustedFlags adds content trust flags to the current command flagset +func AddTrustedFlags(fs *pflag.FlagSet, verify bool) { + trusted, message := setupTrustedFlag(verify) + fs.BoolVar(&untrusted, "disable-content-trust", !trusted, message) +} + +func setupTrustedFlag(verify bool) (bool, string) { + var trusted bool + if e := os.Getenv("DOCKER_CONTENT_TRUST"); e != "" { + if t, err := strconv.ParseBool(e); t || err != nil { + // treat any other value as true + trusted = true + } + } + message := "Skip image signing" + if verify { + message = "Skip image verification" + } + return trusted, message +} + +// IsTrusted returns true if content trust is enabled +func IsTrusted() bool { + return !untrusted +} + +type target struct { + reference registry.Reference + digest digest.Digest + size int64 +} + +func (cli *DockerCli) trustDirectory() string { + return filepath.Join(cliconfig.ConfigDir(), "trust") +} + +// certificateDirectory returns the directory containing +// TLS certificates for the given server. An error is +// returned if there was an error parsing the server string. +func (cli *DockerCli) certificateDirectory(server string) (string, error) { + u, err := url.Parse(server) + if err != nil { + return "", err + } + + return filepath.Join(cliconfig.ConfigDir(), "tls", u.Host), nil +} + +func trustServer(index *registrytypes.IndexInfo) (string, error) { + if s := os.Getenv("DOCKER_CONTENT_TRUST_SERVER"); s != "" { + urlObj, err := url.Parse(s) + if err != nil || urlObj.Scheme != "https" { + return "", fmt.Errorf("valid https URL required for trust server, got %s", s) + } + + return s, nil + } + if index.Official { + return registry.NotaryServer, nil + } + return "https://" + index.Name, nil +} + +type simpleCredentialStore struct { + auth types.AuthConfig +} + +func (scs simpleCredentialStore) Basic(u *url.URL) (string, string) { + return scs.auth.Username, scs.auth.Password +} + +func (scs simpleCredentialStore) RefreshToken(u *url.URL, service string) string { + return scs.auth.IdentityToken +} + +func (scs simpleCredentialStore) SetRefreshToken(*url.URL, string, string) { +} + +// getNotaryRepository returns a NotaryRepository which stores all the +// information needed to operate on a notary repository. +// It creates an HTTP transport providing authentication support. +func (cli *DockerCli) getNotaryRepository(repoInfo *registry.RepositoryInfo, authConfig types.AuthConfig, actions ...string) (*client.NotaryRepository, error) { + server, err := trustServer(repoInfo.Index) + if err != nil { + return nil, err + } + + var cfg = tlsconfig.ClientDefault() + cfg.InsecureSkipVerify = !repoInfo.Index.Secure + + // Get certificate base directory + certDir, err := cli.certificateDirectory(server) + if err != nil { + return nil, err + } + logrus.Debugf("reading certificate directory: %s", certDir) + + if err := registry.ReadCertsDirectory(cfg, certDir); err != nil { + return nil, err + } + + base := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + Dial: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + }).Dial, + TLSHandshakeTimeout: 10 * time.Second, + TLSClientConfig: cfg, + DisableKeepAlives: true, + } + + // Skip configuration headers since request is not going to Docker daemon + modifiers := registry.DockerHeaders(clientUserAgent(), http.Header{}) + authTransport := transport.NewTransport(base, modifiers...) + pingClient := &http.Client{ + Transport: authTransport, + Timeout: 5 * time.Second, + } + endpointStr := server + "/v2/" + req, err := http.NewRequest("GET", endpointStr, nil) + if err != nil { + return nil, err + } + + challengeManager := auth.NewSimpleChallengeManager() + + resp, err := pingClient.Do(req) + if err != nil { + // Ignore error on ping to operate in offline mode + logrus.Debugf("Error pinging notary server %q: %s", endpointStr, err) + } else { + defer resp.Body.Close() + + // Add response to the challenge manager to parse out + // authentication header and register authentication method + if err := challengeManager.AddResponse(resp); err != nil { + return nil, err + } + } + + creds := simpleCredentialStore{auth: authConfig} + tokenHandler := auth.NewTokenHandler(authTransport, creds, repoInfo.FullName(), actions...) + basicHandler := auth.NewBasicHandler(creds) + modifiers = append(modifiers, transport.RequestModifier(auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))) + tr := transport.NewTransport(base, modifiers...) + + return client.NewNotaryRepository( + cli.trustDirectory(), repoInfo.FullName(), server, tr, cli.getPassphraseRetriever(), + trustpinning.TrustPinConfig{}) +} + +func convertTarget(t client.Target) (target, error) { + h, ok := t.Hashes["sha256"] + if !ok { + return target{}, errors.New("no valid hash, expecting sha256") + } + return target{ + reference: registry.ParseReference(t.Name), + digest: digest.NewDigestFromHex("sha256", hex.EncodeToString(h)), + size: t.Length, + }, nil +} + +func (cli *DockerCli) getPassphraseRetriever() passphrase.Retriever { + aliasMap := map[string]string{ + "root": "root", + "snapshot": "repository", + "targets": "repository", + "default": "repository", + } + baseRetriever := passphrase.PromptRetrieverWithInOut(cli.in, cli.out, aliasMap) + env := map[string]string{ + "root": os.Getenv("DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE"), + "snapshot": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), + "targets": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), + "default": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), + } + + return func(keyName string, alias string, createNew bool, numAttempts int) (string, bool, error) { + if v := env[alias]; v != "" { + return v, numAttempts > 1, nil + } + // For non-root roles, we can also try the "default" alias if it is specified + if v := env["default"]; v != "" && alias != data.CanonicalRootRole { + return v, numAttempts > 1, nil + } + return baseRetriever(keyName, alias, createNew, numAttempts) + } +} + +// TrustedReference returns the canonical trusted reference for an image reference +func (cli *DockerCli) TrustedReference(ctx context.Context, ref reference.NamedTagged) (reference.Canonical, error) { + repoInfo, err := registry.ParseRepositoryInfo(ref) + if err != nil { + return nil, err + } + + // Resolve the Auth config relevant for this server + authConfig := cli.ResolveAuthConfig(ctx, repoInfo.Index) + + notaryRepo, err := cli.getNotaryRepository(repoInfo, authConfig, "pull") + if err != nil { + fmt.Fprintf(cli.out, "Error establishing connection to trust repository: %s\n", err) + return nil, err + } + + t, err := notaryRepo.GetTargetByName(ref.Tag(), releasesRole, data.CanonicalTargetsRole) + if err != nil { + return nil, err + } + // Only list tags in the top level targets role or the releases delegation role - ignore + // all other delegation roles + if t.Role != releasesRole && t.Role != data.CanonicalTargetsRole { + return nil, notaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", ref.Tag())) + } + r, err := convertTarget(t.Target) + if err != nil { + return nil, err + + } + + return reference.WithDigest(ref, r.digest) +} + +// TagTrusted tags a trusted ref +func (cli *DockerCli) TagTrusted(ctx context.Context, trustedRef reference.Canonical, ref reference.NamedTagged) error { + fmt.Fprintf(cli.out, "Tagging %s as %s\n", trustedRef.String(), ref.String()) + + return cli.client.ImageTag(ctx, trustedRef.String(), ref.String()) +} + +func notaryError(repoName string, err error) error { + switch err.(type) { + case *json.SyntaxError: + logrus.Debugf("Notary syntax error: %s", err) + return fmt.Errorf("Error: no trust data available for remote repository %s. Try running notary server and setting DOCKER_CONTENT_TRUST_SERVER to its HTTPS address?", repoName) + case signed.ErrExpired: + return fmt.Errorf("Error: remote repository %s out-of-date: %v", repoName, err) + case trustmanager.ErrKeyNotFound: + return fmt.Errorf("Error: signing keys for remote repository %s not found: %v", repoName, err) + case *net.OpError: + return fmt.Errorf("Error: error contacting notary server: %v", err) + case store.ErrMetaNotFound: + return fmt.Errorf("Error: trust data missing for remote repository %s or remote repository not found: %v", repoName, err) + case signed.ErrInvalidKeyType: + return fmt.Errorf("Warning: potential malicious behavior - trust data mismatch for remote repository %s: %v", repoName, err) + case signed.ErrNoKeys: + return fmt.Errorf("Error: could not find signing keys for remote repository %s, or could not decrypt signing key: %v", repoName, err) + case signed.ErrLowVersion: + return fmt.Errorf("Warning: potential malicious behavior - trust data version is lower than expected for remote repository %s: %v", repoName, err) + case signed.ErrRoleThreshold: + return fmt.Errorf("Warning: potential malicious behavior - trust data has insufficient signatures for remote repository %s: %v", repoName, err) + case client.ErrRepositoryNotExist: + return fmt.Errorf("Error: remote trust data does not exist for %s: %v", repoName, err) + case signed.ErrInsufficientSignatures: + return fmt.Errorf("Error: could not produce valid signature for %s. If Yubikey was used, was touch input provided?: %v", repoName, err) + } + + return err +} + +// TrustedPull handles content trust pulling of an image +func (cli *DockerCli) TrustedPull(ctx context.Context, repoInfo *registry.RepositoryInfo, ref registry.Reference, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error { + var refs []target + + notaryRepo, err := cli.getNotaryRepository(repoInfo, authConfig, "pull") + if err != nil { + fmt.Fprintf(cli.out, "Error establishing connection to trust repository: %s\n", err) + return err + } + + if ref.String() == "" { + // List all targets + targets, err := notaryRepo.ListTargets(releasesRole, data.CanonicalTargetsRole) + if err != nil { + return notaryError(repoInfo.FullName(), err) + } + for _, tgt := range targets { + t, err := convertTarget(tgt.Target) + if err != nil { + fmt.Fprintf(cli.out, "Skipping target for %q\n", repoInfo.Name()) + continue + } + // Only list tags in the top level targets role or the releases delegation role - ignore + // all other delegation roles + if tgt.Role != releasesRole && tgt.Role != data.CanonicalTargetsRole { + continue + } + refs = append(refs, t) + } + if len(refs) == 0 { + return notaryError(repoInfo.FullName(), fmt.Errorf("No trusted tags for %s", repoInfo.FullName())) + } + } else { + t, err := notaryRepo.GetTargetByName(ref.String(), releasesRole, data.CanonicalTargetsRole) + if err != nil { + return notaryError(repoInfo.FullName(), err) + } + // Only get the tag if it's in the top level targets role or the releases delegation role + // ignore it if it's in any other delegation roles + if t.Role != releasesRole && t.Role != data.CanonicalTargetsRole { + return notaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", ref.String())) + } + + logrus.Debugf("retrieving target for %s role\n", t.Role) + r, err := convertTarget(t.Target) + if err != nil { + return err + + } + refs = append(refs, r) + } + + for i, r := range refs { + displayTag := r.reference.String() + if displayTag != "" { + displayTag = ":" + displayTag + } + fmt.Fprintf(cli.out, "Pull (%d of %d): %s%s@%s\n", i+1, len(refs), repoInfo.Name(), displayTag, r.digest) + + ref, err := reference.WithDigest(repoInfo, r.digest) + if err != nil { + return err + } + if err := cli.ImagePullPrivileged(ctx, authConfig, ref.String(), requestPrivilege, false); err != nil { + return err + } + + // If reference is not trusted, tag by trusted reference + if !r.reference.HasDigest() { + tagged, err := reference.WithTag(repoInfo, r.reference.String()) + if err != nil { + return err + } + trustedRef, err := reference.WithDigest(repoInfo, r.digest) + if err != nil { + return err + } + if err := cli.TagTrusted(ctx, trustedRef, tagged); err != nil { + return err + } + } + } + return nil +} + +// TrustedPush handles content trust pushing of an image +func (cli *DockerCli) TrustedPush(ctx context.Context, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error { + responseBody, err := cli.ImagePushPrivileged(ctx, authConfig, ref.String(), requestPrivilege) + if err != nil { + return err + } + + defer responseBody.Close() + + // If it is a trusted push we would like to find the target entry which match the + // tag provided in the function and then do an AddTarget later. + target := &client.Target{} + // Count the times of calling for handleTarget, + // if it is called more that once, that should be considered an error in a trusted push. + cnt := 0 + handleTarget := func(aux *json.RawMessage) { + cnt++ + if cnt > 1 { + // handleTarget should only be called one. This will be treated as an error. + return + } + + var pushResult distribution.PushResult + err := json.Unmarshal(*aux, &pushResult) + if err == nil && pushResult.Tag != "" && pushResult.Digest.Validate() == nil { + h, err := hex.DecodeString(pushResult.Digest.Hex()) + if err != nil { + target = nil + return + } + target.Name = registry.ParseReference(pushResult.Tag).String() + target.Hashes = data.Hashes{string(pushResult.Digest.Algorithm()): h} + target.Length = int64(pushResult.Size) + } + } + + var tag string + switch x := ref.(type) { + case reference.Canonical: + return errors.New("cannot push a digest reference") + case reference.NamedTagged: + tag = x.Tag() + } + + // We want trust signatures to always take an explicit tag, + // otherwise it will act as an untrusted push. + if tag == "" { + if err = jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), nil); err != nil { + return err + } + fmt.Fprintln(cli.out, "No tag specified, skipping trust metadata push") + return nil + } + + if err = jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), handleTarget); err != nil { + return err + } + + if cnt > 1 { + return fmt.Errorf("internal error: only one call to handleTarget expected") + } + + if target == nil { + fmt.Fprintln(cli.out, "No targets found, please provide a specific tag in order to sign it") + return nil + } + + fmt.Fprintln(cli.out, "Signing and pushing trust metadata") + + repo, err := cli.getNotaryRepository(repoInfo, authConfig, "push", "pull") + if err != nil { + fmt.Fprintf(cli.out, "Error establishing connection to notary repository: %s\n", err) + return err + } + + // get the latest repository metadata so we can figure out which roles to sign + err = repo.Update(false) + + switch err.(type) { + case client.ErrRepoNotInitialized, client.ErrRepositoryNotExist: + keys := repo.CryptoService.ListKeys(data.CanonicalRootRole) + var rootKeyID string + // always select the first root key + if len(keys) > 0 { + sort.Strings(keys) + rootKeyID = keys[0] + } else { + rootPublicKey, err := repo.CryptoService.Create(data.CanonicalRootRole, "", data.ECDSAKey) + if err != nil { + return err + } + rootKeyID = rootPublicKey.ID() + } + + // Initialize the notary repository with a remotely managed snapshot key + if err := repo.Initialize(rootKeyID, data.CanonicalSnapshotRole); err != nil { + return notaryError(repoInfo.FullName(), err) + } + fmt.Fprintf(cli.out, "Finished initializing %q\n", repoInfo.FullName()) + err = repo.AddTarget(target, data.CanonicalTargetsRole) + case nil: + // already initialized and we have successfully downloaded the latest metadata + err = cli.addTargetToAllSignableRoles(repo, target) + default: + return notaryError(repoInfo.FullName(), err) + } + + if err == nil { + err = repo.Publish() + } + + if err != nil { + fmt.Fprintf(cli.out, "Failed to sign %q:%s - %s\n", repoInfo.FullName(), tag, err.Error()) + return notaryError(repoInfo.FullName(), err) + } + + fmt.Fprintf(cli.out, "Successfully signed %q:%s\n", repoInfo.FullName(), tag) + return nil +} + +// Attempt to add the image target to all the top level delegation roles we can +// (based on whether we have the signing key and whether the role's path allows +// us to). +// If there are no delegation roles, we add to the targets role. +func (cli *DockerCli) addTargetToAllSignableRoles(repo *client.NotaryRepository, target *client.Target) error { + var signableRoles []string + + // translate the full key names, which includes the GUN, into just the key IDs + allCanonicalKeyIDs := make(map[string]struct{}) + for fullKeyID := range repo.CryptoService.ListAllKeys() { + allCanonicalKeyIDs[path.Base(fullKeyID)] = struct{}{} + } + + allDelegationRoles, err := repo.GetDelegationRoles() + if err != nil { + return err + } + + // if there are no delegation roles, then just try to sign it into the targets role + if len(allDelegationRoles) == 0 { + return repo.AddTarget(target, data.CanonicalTargetsRole) + } + + // there are delegation roles, find every delegation role we have a key for, and + // attempt to sign into into all those roles. + for _, delegationRole := range allDelegationRoles { + // We do not support signing any delegation role that isn't a direct child of the targets role. + // Also don't bother checking the keys if we can't add the target + // to this role due to path restrictions + if path.Dir(delegationRole.Name) != data.CanonicalTargetsRole || !delegationRole.CheckPaths(target.Name) { + continue + } + + for _, canonicalKeyID := range delegationRole.KeyIDs { + if _, ok := allCanonicalKeyIDs[canonicalKeyID]; ok { + signableRoles = append(signableRoles, delegationRole.Name) + break + } + } + } + + if len(signableRoles) == 0 { + return fmt.Errorf("no valid signing keys for delegation roles") + } + + return repo.AddTarget(target, signableRoles...) +} + +// ImagePullPrivileged pulls the image and displays it to the output +func (cli *DockerCli) ImagePullPrivileged(ctx context.Context, authConfig types.AuthConfig, ref string, requestPrivilege types.RequestPrivilegeFunc, all bool) error { + + encodedAuth, err := EncodeAuthToBase64(authConfig) + if err != nil { + return err + } + options := types.ImagePullOptions{ + RegistryAuth: encodedAuth, + PrivilegeFunc: requestPrivilege, + All: all, + } + + responseBody, err := cli.client.ImagePull(ctx, ref, options) + if err != nil { + return err + } + defer responseBody.Close() + + return jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), nil) +} + +// ImagePushPrivileged push the image +func (cli *DockerCli) ImagePushPrivileged(ctx context.Context, authConfig types.AuthConfig, ref string, requestPrivilege types.RequestPrivilegeFunc) (io.ReadCloser, error) { + encodedAuth, err := EncodeAuthToBase64(authConfig) + if err != nil { + return nil, err + } + options := types.ImagePushOptions{ + RegistryAuth: encodedAuth, + PrivilegeFunc: requestPrivilege, + } + + return cli.client.ImagePush(ctx, ref, options) +} diff --git a/components/cli/command/trust_test.go b/components/cli/command/trust_test.go new file mode 100644 index 0000000000..534815f379 --- /dev/null +++ b/components/cli/command/trust_test.go @@ -0,0 +1,56 @@ +package command + +import ( + "os" + "testing" + + registrytypes "github.com/docker/docker/api/types/registry" + "github.com/docker/docker/registry" +) + +func unsetENV() { + os.Unsetenv("DOCKER_CONTENT_TRUST") + os.Unsetenv("DOCKER_CONTENT_TRUST_SERVER") +} + +func TestENVTrustServer(t *testing.T) { + defer unsetENV() + indexInfo := ®istrytypes.IndexInfo{Name: "testserver"} + if err := os.Setenv("DOCKER_CONTENT_TRUST_SERVER", "https://notary-test.com:5000"); err != nil { + t.Fatal("Failed to set ENV variable") + } + output, err := trustServer(indexInfo) + expectedStr := "https://notary-test.com:5000" + if err != nil || output != expectedStr { + t.Fatalf("Expected server to be %s, got %s", expectedStr, output) + } +} + +func TestHTTPENVTrustServer(t *testing.T) { + defer unsetENV() + indexInfo := ®istrytypes.IndexInfo{Name: "testserver"} + if err := os.Setenv("DOCKER_CONTENT_TRUST_SERVER", "http://notary-test.com:5000"); err != nil { + t.Fatal("Failed to set ENV variable") + } + _, err := trustServer(indexInfo) + if err == nil { + t.Fatal("Expected error with invalid scheme") + } +} + +func TestOfficialTrustServer(t *testing.T) { + indexInfo := ®istrytypes.IndexInfo{Name: "testserver", Official: true} + output, err := trustServer(indexInfo) + if err != nil || output != registry.NotaryServer { + t.Fatalf("Expected server to be %s, got %s", registry.NotaryServer, output) + } +} + +func TestNonOfficialTrustServer(t *testing.T) { + indexInfo := ®istrytypes.IndexInfo{Name: "testserver", Official: false} + output, err := trustServer(indexInfo) + expectedStr := "https://" + indexInfo.Name + if err != nil || output != expectedStr { + t.Fatalf("Expected server to be %s, got %s", expectedStr, output) + } +} diff --git a/components/cli/command/utils.go b/components/cli/command/utils.go new file mode 100644 index 0000000000..bceb7b335c --- /dev/null +++ b/components/cli/command/utils.go @@ -0,0 +1,59 @@ +package command + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" +) + +// CopyToFile writes the content of the reader to the specified file +func CopyToFile(outfile string, r io.Reader) error { + tmpFile, err := ioutil.TempFile(filepath.Dir(outfile), ".docker_temp_") + if err != nil { + return err + } + + tmpPath := tmpFile.Name() + + _, err = io.Copy(tmpFile, r) + tmpFile.Close() + + if err != nil { + os.Remove(tmpPath) + return err + } + + if err = os.Rename(tmpPath, outfile); err != nil { + os.Remove(tmpPath) + return err + } + + return nil +} + +// capitalizeFirst capitalizes the first character of string +func capitalizeFirst(s string) string { + switch l := len(s); l { + case 0: + return s + case 1: + return strings.ToLower(s) + default: + return strings.ToUpper(string(s[0])) + strings.ToLower(s[1:]) + } +} + +// PrettyPrint outputs arbitrary data for human formatted output by uppercasing the first letter. +func PrettyPrint(i interface{}) string { + switch t := i.(type) { + case nil: + return "None" + case string: + return capitalizeFirst(t) + default: + return capitalizeFirst(fmt.Sprintf("%s", t)) + } +} diff --git a/components/cli/command/volume/cmd.go b/components/cli/command/volume/cmd.go new file mode 100644 index 0000000000..090a006439 --- /dev/null +++ b/components/cli/command/volume/cmd.go @@ -0,0 +1,48 @@ +package volume + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" +) + +// NewVolumeCommand returns a cobra command for `volume` subcommands +func NewVolumeCommand(dockerCli *command.DockerCli) *cobra.Command { + cmd := &cobra.Command{ + Use: "volume COMMAND", + Short: "Manage Docker volumes", + Long: volumeDescription, + Args: cli.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + }, + } + cmd.AddCommand( + newCreateCommand(dockerCli), + newInspectCommand(dockerCli), + newListCommand(dockerCli), + newRemoveCommand(dockerCli), + ) + return cmd +} + +var volumeDescription = ` +The **docker volume** command has subcommands for managing data volumes. A data +volume is a specially-designated directory that by-passes storage driver +management. + +Data volumes persist data independent of a container's life cycle. When you +delete a container, the Engine daemon does not delete any data volumes. You can +share volumes across multiple containers. Moreover, you can share data volumes +with other computing resources in your system. + +To see help for a subcommand, use: + + docker volume CMD help + +For full details on using docker volume visit Docker's online documentation. + +` diff --git a/components/cli/command/volume/create.go b/components/cli/command/volume/create.go new file mode 100644 index 0000000000..4427ff1ea7 --- /dev/null +++ b/components/cli/command/volume/create.go @@ -0,0 +1,110 @@ +package volume + +import ( + "fmt" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/opts" + runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/spf13/cobra" +) + +type createOptions struct { + name string + driver string + driverOpts opts.MapOpts + labels []string +} + +func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { + opts := createOptions{ + driverOpts: *opts.NewMapOpts(nil, nil), + } + + cmd := &cobra.Command{ + Use: "create [OPTIONS] [VOLUME]", + Short: "Create a volume", + Long: createDescription, + Args: cli.RequiresMaxArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 1 { + if opts.name != "" { + fmt.Fprint(dockerCli.Err(), "Conflicting options: either specify --name or provide positional arg, not both\n") + return cli.StatusError{StatusCode: 1} + } + opts.name = args[0] + } + return runCreate(dockerCli, opts) + }, + } + flags := cmd.Flags() + flags.StringVarP(&opts.driver, "driver", "d", "local", "Specify volume driver name") + flags.StringVar(&opts.name, "name", "", "Specify volume name") + flags.Lookup("name").Hidden = true + flags.VarP(&opts.driverOpts, "opt", "o", "Set driver specific options") + flags.StringSliceVar(&opts.labels, "label", []string{}, "Set metadata for a volume") + + return cmd +} + +func runCreate(dockerCli *command.DockerCli, opts createOptions) error { + client := dockerCli.Client() + + volReq := types.VolumeCreateRequest{ + Driver: opts.driver, + DriverOpts: opts.driverOpts.GetAll(), + Name: opts.name, + Labels: runconfigopts.ConvertKVStringsToMap(opts.labels), + } + + vol, err := client.VolumeCreate(context.Background(), volReq) + if err != nil { + return err + } + + fmt.Fprintf(dockerCli.Out(), "%s\n", vol.Name) + return nil +} + +var createDescription = ` +Creates a new volume that containers can consume and store data in. If a name +is not specified, Docker generates a random name. You create a volume and then +configure the container to use it, for example: + + $ docker volume create hello + hello + $ docker run -d -v hello:/world busybox ls /world + +The mount is created inside the container's **/src** directory. Docker doesn't +not support relative paths for mount points inside the container. + +Multiple containers can use the same volume in the same time period. This is +useful if two containers need access to shared data. For example, if one +container writes and the other reads the data. + +## Driver specific options + +Some volume drivers may take options to customize the volume creation. Use the +**-o** or **--opt** flags to pass driver options: + + $ docker volume create --driver fake --opt tardis=blue --opt timey=wimey + +These options are passed directly to the volume driver. Options for different +volume drivers may do different things (or nothing at all). + +The built-in **local** driver on Windows does not support any options. + +The built-in **local** driver on Linux accepts options similar to the linux +**mount** command: + + $ docker volume create --driver local --opt type=tmpfs --opt device=tmpfs --opt o=size=100m,uid=1000 + +Another example: + + $ docker volume create --driver local --opt type=btrfs --opt device=/dev/sda2 + +` diff --git a/components/cli/command/volume/inspect.go b/components/cli/command/volume/inspect.go new file mode 100644 index 0000000000..ab06e03807 --- /dev/null +++ b/components/cli/command/volume/inspect.go @@ -0,0 +1,55 @@ +package volume + +import ( + "golang.org/x/net/context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/inspect" + "github.com/spf13/cobra" +) + +type inspectOptions struct { + format string + names []string +} + +func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts inspectOptions + + cmd := &cobra.Command{ + Use: "inspect [OPTIONS] VOLUME [VOLUME...]", + Short: "Display detailed information on one or more volumes", + Long: inspectDescription, + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.names = args + return runInspect(dockerCli, opts) + }, + } + + cmd.Flags().StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template") + + return cmd +} + +func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { + client := dockerCli.Client() + + ctx := context.Background() + + getVolFunc := func(name string) (interface{}, []byte, error) { + i, err := client.VolumeInspect(ctx, name) + return i, nil, err + } + + return inspect.Inspect(dockerCli.Out(), opts.names, opts.format, getVolFunc) +} + +var inspectDescription = ` +Returns information about one or more volumes. By default, this command renders +all results in a JSON array. You can specify an alternate format to execute a +given template is executed for each result. Go's https://golang.org/pkg/text/template/ +package describes all the details of the format. + +` diff --git a/components/cli/command/volume/list.go b/components/cli/command/volume/list.go new file mode 100644 index 0000000000..75e77f828f --- /dev/null +++ b/components/cli/command/volume/list.go @@ -0,0 +1,108 @@ +package volume + +import ( + "sort" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/formatter" + "github.com/spf13/cobra" +) + +type byVolumeName []*types.Volume + +func (r byVolumeName) Len() int { return len(r) } +func (r byVolumeName) Swap(i, j int) { r[i], r[j] = r[j], r[i] } +func (r byVolumeName) Less(i, j int) bool { + return r[i].Name < r[j].Name +} + +type listOptions struct { + quiet bool + format string + filter []string +} + +func newListCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts listOptions + + cmd := &cobra.Command{ + Use: "ls [OPTIONS]", + Aliases: []string{"list"}, + Short: "List volumes", + Long: listDescription, + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runList(dockerCli, opts) + }, + } + + flags := cmd.Flags() + flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display volume names") + flags.StringVar(&opts.format, "format", "", "Pretty-print volumes using a Go template") + flags.StringSliceVarP(&opts.filter, "filter", "f", []string{}, "Provide filter values (e.g. 'dangling=true')") + + return cmd +} + +func runList(dockerCli *command.DockerCli, opts listOptions) error { + client := dockerCli.Client() + + volFilterArgs := filters.NewArgs() + for _, f := range opts.filter { + var err error + volFilterArgs, err = filters.ParseFlag(f, volFilterArgs) + if err != nil { + return err + } + } + + volumes, err := client.VolumeList(context.Background(), volFilterArgs) + if err != nil { + return err + } + + f := opts.format + if len(f) == 0 { + if len(dockerCli.ConfigFile().VolumesFormat) > 0 && !opts.quiet { + f = dockerCli.ConfigFile().VolumesFormat + } else { + f = "table" + } + } + + sort.Sort(byVolumeName(volumes.Volumes)) + + volumeCtx := formatter.VolumeContext{ + Context: formatter.Context{ + Output: dockerCli.Out(), + Format: f, + Quiet: opts.quiet, + }, + Volumes: volumes.Volumes, + } + + volumeCtx.Write() + + return nil +} + +var listDescription = ` + +Lists all the volumes Docker knows about. You can filter using the **-f** or +**--filter** flag. The filtering format is a **key=value** pair. To specify +more than one filter, pass multiple flags (for example, +**--filter "foo=bar" --filter "bif=baz"**) + +The currently supported filters are: + +* **dangling** (boolean - **true** or **false**, **1** or **0**) +* **driver** (a volume driver's name) +* **label** (**label=** or **label==**) +* **name** (a volume's name) + +` diff --git a/components/cli/command/volume/remove.go b/components/cli/command/volume/remove.go new file mode 100644 index 0000000000..213ad26ab5 --- /dev/null +++ b/components/cli/command/volume/remove.go @@ -0,0 +1,68 @@ +package volume + +import ( + "fmt" + + "golang.org/x/net/context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +type removeOptions struct { + force bool + + volumes []string +} + +func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts removeOptions + + cmd := &cobra.Command{ + Use: "rm [OPTIONS] VOLUME [VOLUME...]", + Aliases: []string{"remove"}, + Short: "Remove one or more volumes", + Long: removeDescription, + Example: removeExample, + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.volumes = args + return runRemove(dockerCli, &opts) + }, + } + + flags := cmd.Flags() + flags.BoolVarP(&opts.force, "force", "f", false, "Force the removal of one or more volumes") + + return cmd +} + +func runRemove(dockerCli *command.DockerCli, opts *removeOptions) error { + client := dockerCli.Client() + ctx := context.Background() + status := 0 + + for _, name := range opts.volumes { + if err := client.VolumeRemove(ctx, name, opts.force); err != nil { + fmt.Fprintf(dockerCli.Err(), "%s\n", err) + status = 1 + continue + } + fmt.Fprintf(dockerCli.Out(), "%s\n", name) + } + + if status != 0 { + return cli.StatusError{StatusCode: status} + } + return nil +} + +var removeDescription = ` +Remove one or more volumes. You cannot remove a volume that is in use by a container. +` + +var removeExample = ` +$ docker volume rm hello +hello +` From a5bf28b72a0c1a8d2fc98a221568144509f9d6a6 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 8 Sep 2016 13:11:39 -0400 Subject: [PATCH 115/978] Move api/client -> cli/command Using gomvpkg -from github.com/docker/docker/api/client -to github.com/docker/docker/cli/command -vcs_mv_cmd 'git mv {{.Src}} {{.Dst}}' Signed-off-by: Daniel Nephin Upstream-commit: b2c77abc35514e3f37fcbdf592a994d55fc698a0 Component: cli --- components/cli/docker.go | 10 +++++----- components/cli/docker_test.go | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/components/cli/docker.go b/components/cli/docker.go index 38907970d3..969cd80876 100644 --- a/components/cli/docker.go +++ b/components/cli/docker.go @@ -5,9 +5,9 @@ import ( "os" "github.com/Sirupsen/logrus" - "github.com/docker/docker/api/client" - "github.com/docker/docker/api/client/command" "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/commands" cliflags "github.com/docker/docker/cli/flags" "github.com/docker/docker/cliconfig" "github.com/docker/docker/dockerversion" @@ -17,7 +17,7 @@ import ( "github.com/spf13/pflag" ) -func newDockerCommand(dockerCli *client.DockerCli) *cobra.Command { +func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command { opts := cliflags.NewClientOptions() var flags *pflag.FlagSet @@ -52,7 +52,7 @@ func newDockerCommand(dockerCli *client.DockerCli) *cobra.Command { cmd.SetOutput(dockerCli.Out()) cmd.AddCommand(newDaemonCommand()) - command.AddCommands(cmd, dockerCli) + commands.AddCommands(cmd, dockerCli) return cmd } @@ -70,7 +70,7 @@ func main() { stdin, stdout, stderr := term.StdStreams() logrus.SetOutput(stderr) - dockerCli := client.NewDockerCli(stdin, stdout, stderr) + dockerCli := command.NewDockerCli(stdin, stdout, stderr) cmd := newDockerCommand(dockerCli) if err := cmd.Execute(); err != nil { diff --git a/components/cli/docker_test.go b/components/cli/docker_test.go index 72d2311521..47e24eb0da 100644 --- a/components/cli/docker_test.go +++ b/components/cli/docker_test.go @@ -7,13 +7,13 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/docker/utils" - "github.com/docker/docker/api/client" + "github.com/docker/docker/cli/command" ) func TestClientDebugEnabled(t *testing.T) { defer utils.DisableDebug() - cmd := newDockerCommand(&client.DockerCli{}) + cmd := newDockerCommand(&command.DockerCli{}) cmd.Flags().Set("debug", "true") if err := cmd.PersistentPreRunE(cmd, []string{}); err != nil { From bc178aeaa8682a9e1fd0891c0d48cfadf847d8b1 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 8 Sep 2016 14:54:01 -0400 Subject: [PATCH 116/978] Replace api/client imports with cli/command in experimental files. Using git grep -l 'client\.DockerCli' cli/command/stack/ | xargs sed -i -e 's/client\.DockerCli/command\.Dockercli/g' Signed-off-by: Daniel Nephin Upstream-commit: 8f3e3fb6e5db5be1d71e340c29cf161fb5c78e26 Component: cli --- components/cli/command/commands/commands.go | 2 +- components/cli/command/plugin/cmd_experimental.go | 4 ++-- components/cli/command/plugin/disable.go | 6 +++--- components/cli/command/plugin/enable.go | 6 +++--- components/cli/command/plugin/inspect.go | 8 ++++---- components/cli/command/plugin/install.go | 10 +++++----- components/cli/command/plugin/list.go | 6 +++--- components/cli/command/plugin/push.go | 8 ++++---- components/cli/command/plugin/remove.go | 6 +++--- components/cli/command/plugin/set.go | 6 +++--- components/cli/command/stack/cmd.go | 6 +++--- components/cli/command/stack/config.go | 8 ++++---- components/cli/command/stack/deploy.go | 12 ++++++------ components/cli/command/stack/opts.go | 2 +- components/cli/command/stack/ps.go | 10 +++++----- components/cli/command/stack/remove.go | 6 +++--- components/cli/command/stack/services.go | 8 ++++---- 17 files changed, 57 insertions(+), 57 deletions(-) diff --git a/components/cli/command/commands/commands.go b/components/cli/command/commands/commands.go index 3eb1828d57..35fd6860b0 100644 --- a/components/cli/command/commands/commands.go +++ b/components/cli/command/commands/commands.go @@ -16,7 +16,7 @@ import ( "github.com/spf13/cobra" ) -// AddCommands adds all the commands from api/client to the root command +// AddCommands adds all the commands from cli/command to the root command func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) { cmd.AddCommand( node.NewNodeCommand(dockerCli), diff --git a/components/cli/command/plugin/cmd_experimental.go b/components/cli/command/plugin/cmd_experimental.go index 6c991937fe..cc779143fa 100644 --- a/components/cli/command/plugin/cmd_experimental.go +++ b/components/cli/command/plugin/cmd_experimental.go @@ -5,13 +5,13 @@ package plugin import ( "fmt" - "github.com/docker/docker/api/client" "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" "github.com/spf13/cobra" ) // NewPluginCommand returns a cobra command for `plugin` subcommands -func NewPluginCommand(rootCmd *cobra.Command, dockerCli *client.DockerCli) { +func NewPluginCommand(rootCmd *cobra.Command, dockerCli *command.DockerCli) { cmd := &cobra.Command{ Use: "plugin", Short: "Manage Docker plugins", diff --git a/components/cli/command/plugin/disable.go b/components/cli/command/plugin/disable.go index 704eb75286..3b5c69a018 100644 --- a/components/cli/command/plugin/disable.go +++ b/components/cli/command/plugin/disable.go @@ -5,14 +5,14 @@ package plugin import ( "fmt" - "github.com/docker/docker/api/client" "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" "github.com/docker/docker/reference" "github.com/spf13/cobra" "golang.org/x/net/context" ) -func newDisableCommand(dockerCli *client.DockerCli) *cobra.Command { +func newDisableCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := &cobra.Command{ Use: "disable PLUGIN", Short: "Disable a plugin", @@ -25,7 +25,7 @@ func newDisableCommand(dockerCli *client.DockerCli) *cobra.Command { return cmd } -func runDisable(dockerCli *client.DockerCli, name string) error { +func runDisable(dockerCli *command.DockerCli, name string) error { named, err := reference.ParseNamed(name) // FIXME: validate if err != nil { return err diff --git a/components/cli/command/plugin/enable.go b/components/cli/command/plugin/enable.go index c31258bbb6..cfc3580f43 100644 --- a/components/cli/command/plugin/enable.go +++ b/components/cli/command/plugin/enable.go @@ -5,14 +5,14 @@ package plugin import ( "fmt" - "github.com/docker/docker/api/client" "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" "github.com/docker/docker/reference" "github.com/spf13/cobra" "golang.org/x/net/context" ) -func newEnableCommand(dockerCli *client.DockerCli) *cobra.Command { +func newEnableCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := &cobra.Command{ Use: "enable PLUGIN", Short: "Enable a plugin", @@ -25,7 +25,7 @@ func newEnableCommand(dockerCli *client.DockerCli) *cobra.Command { return cmd } -func runEnable(dockerCli *client.DockerCli, name string) error { +func runEnable(dockerCli *command.DockerCli, name string) error { named, err := reference.ParseNamed(name) // FIXME: validate if err != nil { return err diff --git a/components/cli/command/plugin/inspect.go b/components/cli/command/plugin/inspect.go index b43e3e9453..a1cf1f7b0e 100644 --- a/components/cli/command/plugin/inspect.go +++ b/components/cli/command/plugin/inspect.go @@ -5,9 +5,9 @@ package plugin import ( "fmt" - "github.com/docker/docker/api/client" - "github.com/docker/docker/api/client/inspect" "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/inspect" "github.com/docker/docker/reference" "github.com/spf13/cobra" "golang.org/x/net/context" @@ -18,7 +18,7 @@ type inspectOptions struct { format string } -func newInspectCommand(dockerCli *client.DockerCli) *cobra.Command { +func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { var opts inspectOptions cmd := &cobra.Command{ @@ -36,7 +36,7 @@ func newInspectCommand(dockerCli *client.DockerCli) *cobra.Command { return cmd } -func runInspect(dockerCli *client.DockerCli, opts inspectOptions) error { +func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { client := dockerCli.Client() ctx := context.Background() getRef := func(name string) (interface{}, []byte, error) { diff --git a/components/cli/command/plugin/install.go b/components/cli/command/plugin/install.go index 05dc8e8268..2867247a84 100644 --- a/components/cli/command/plugin/install.go +++ b/components/cli/command/plugin/install.go @@ -7,9 +7,9 @@ import ( "fmt" "strings" - "github.com/docker/docker/api/client" "github.com/docker/docker/api/types" "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" "github.com/docker/docker/reference" "github.com/docker/docker/registry" "github.com/spf13/cobra" @@ -22,7 +22,7 @@ type pluginOptions struct { disable bool } -func newInstallCommand(dockerCli *client.DockerCli) *cobra.Command { +func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command { var options pluginOptions cmd := &cobra.Command{ Use: "install [OPTIONS] PLUGIN", @@ -41,7 +41,7 @@ func newInstallCommand(dockerCli *client.DockerCli) *cobra.Command { return cmd } -func runInstall(dockerCli *client.DockerCli, opts pluginOptions) error { +func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error { named, err := reference.ParseNamed(opts.name) // FIXME: validate if err != nil { return err @@ -63,7 +63,7 @@ func runInstall(dockerCli *client.DockerCli, opts pluginOptions) error { authConfig := dockerCli.ResolveAuthConfig(ctx, repoInfo.Index) - encodedAuth, err := client.EncodeAuthToBase64(authConfig) + encodedAuth, err := command.EncodeAuthToBase64(authConfig) if err != nil { return err } @@ -85,7 +85,7 @@ func runInstall(dockerCli *client.DockerCli, opts pluginOptions) error { return nil } -func acceptPrivileges(dockerCli *client.DockerCli, name string) func(privileges types.PluginPrivileges) (bool, error) { +func acceptPrivileges(dockerCli *command.DockerCli, name string) func(privileges types.PluginPrivileges) (bool, error) { return func(privileges types.PluginPrivileges) (bool, error) { fmt.Fprintf(dockerCli.Out(), "Plugin %q is requesting the following privileges:\n", name) for _, privilege := range privileges { diff --git a/components/cli/command/plugin/list.go b/components/cli/command/plugin/list.go index b50b2066a7..b8f5e5e08a 100644 --- a/components/cli/command/plugin/list.go +++ b/components/cli/command/plugin/list.go @@ -7,8 +7,8 @@ import ( "strings" "text/tabwriter" - "github.com/docker/docker/api/client" "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" "github.com/docker/docker/pkg/stringutils" "github.com/spf13/cobra" "golang.org/x/net/context" @@ -18,7 +18,7 @@ type listOptions struct { noTrunc bool } -func newListCommand(dockerCli *client.DockerCli) *cobra.Command { +func newListCommand(dockerCli *command.DockerCli) *cobra.Command { var opts listOptions cmd := &cobra.Command{ @@ -38,7 +38,7 @@ func newListCommand(dockerCli *client.DockerCli) *cobra.Command { return cmd } -func runList(dockerCli *client.DockerCli, opts listOptions) error { +func runList(dockerCli *command.DockerCli, opts listOptions) error { plugins, err := dockerCli.Client().PluginList(context.Background()) if err != nil { return err diff --git a/components/cli/command/plugin/push.go b/components/cli/command/plugin/push.go index 9ef4907961..5174828ea8 100644 --- a/components/cli/command/plugin/push.go +++ b/components/cli/command/plugin/push.go @@ -7,14 +7,14 @@ import ( "golang.org/x/net/context" - "github.com/docker/docker/api/client" "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" "github.com/docker/docker/reference" "github.com/docker/docker/registry" "github.com/spf13/cobra" ) -func newPushCommand(dockerCli *client.DockerCli) *cobra.Command { +func newPushCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := &cobra.Command{ Use: "push PLUGIN", Short: "Push a plugin", @@ -26,7 +26,7 @@ func newPushCommand(dockerCli *client.DockerCli) *cobra.Command { return cmd } -func runPush(dockerCli *client.DockerCli, name string) error { +func runPush(dockerCli *command.DockerCli, name string) error { named, err := reference.ParseNamed(name) // FIXME: validate if err != nil { return err @@ -47,7 +47,7 @@ func runPush(dockerCli *client.DockerCli, name string) error { } authConfig := dockerCli.ResolveAuthConfig(ctx, repoInfo.Index) - encodedAuth, err := client.EncodeAuthToBase64(authConfig) + encodedAuth, err := command.EncodeAuthToBase64(authConfig) if err != nil { return err } diff --git a/components/cli/command/plugin/remove.go b/components/cli/command/plugin/remove.go index 3b61374009..800fc1b97f 100644 --- a/components/cli/command/plugin/remove.go +++ b/components/cli/command/plugin/remove.go @@ -5,9 +5,9 @@ package plugin import ( "fmt" - "github.com/docker/docker/api/client" "github.com/docker/docker/api/types" "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" "github.com/docker/docker/reference" "github.com/spf13/cobra" "golang.org/x/net/context" @@ -19,7 +19,7 @@ type rmOptions struct { plugins []string } -func newRemoveCommand(dockerCli *client.DockerCli) *cobra.Command { +func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { var opts rmOptions cmd := &cobra.Command{ @@ -38,7 +38,7 @@ func newRemoveCommand(dockerCli *client.DockerCli) *cobra.Command { return cmd } -func runRemove(dockerCli *client.DockerCli, opts *rmOptions) error { +func runRemove(dockerCli *command.DockerCli, opts *rmOptions) error { ctx := context.Background() var errs cli.Errors diff --git a/components/cli/command/plugin/set.go b/components/cli/command/plugin/set.go index 188bd63cc4..f2d3b082c6 100644 --- a/components/cli/command/plugin/set.go +++ b/components/cli/command/plugin/set.go @@ -7,13 +7,13 @@ import ( "golang.org/x/net/context" - "github.com/docker/docker/api/client" "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" "github.com/docker/docker/reference" "github.com/spf13/cobra" ) -func newSetCommand(dockerCli *client.DockerCli) *cobra.Command { +func newSetCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := &cobra.Command{ Use: "set PLUGIN key1=value1 [key2=value2...]", Short: "Change settings for a plugin", @@ -26,7 +26,7 @@ func newSetCommand(dockerCli *client.DockerCli) *cobra.Command { return cmd } -func runSet(dockerCli *client.DockerCli, name string, args []string) error { +func runSet(dockerCli *command.DockerCli, name string, args []string) error { named, err := reference.ParseNamed(name) // FIXME: validate if err != nil { return err diff --git a/components/cli/command/stack/cmd.go b/components/cli/command/stack/cmd.go index 979e1a0b77..d459e0a9a1 100644 --- a/components/cli/command/stack/cmd.go +++ b/components/cli/command/stack/cmd.go @@ -5,13 +5,13 @@ package stack import ( "fmt" - "github.com/docker/docker/api/client" "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" "github.com/spf13/cobra" ) // NewStackCommand returns a cobra command for `stack` subcommands -func NewStackCommand(dockerCli *client.DockerCli) *cobra.Command { +func NewStackCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := &cobra.Command{ Use: "stack", Short: "Manage Docker stacks", @@ -31,7 +31,7 @@ func NewStackCommand(dockerCli *client.DockerCli) *cobra.Command { } // NewTopLevelDeployCommand returns a command for `docker deploy` -func NewTopLevelDeployCommand(dockerCli *client.DockerCli) *cobra.Command { +func NewTopLevelDeployCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := newDeployCommand(dockerCli) // Remove the aliases at the top level cmd.Aliases = []string{} diff --git a/components/cli/command/stack/config.go b/components/cli/command/stack/config.go index 696c0c3fc7..bdcf7d4835 100644 --- a/components/cli/command/stack/config.go +++ b/components/cli/command/stack/config.go @@ -3,9 +3,9 @@ package stack import ( - "github.com/docker/docker/api/client" - "github.com/docker/docker/api/client/bundlefile" "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/bundlefile" "github.com/spf13/cobra" ) @@ -14,7 +14,7 @@ type configOptions struct { namespace string } -func newConfigCommand(dockerCli *client.DockerCli) *cobra.Command { +func newConfigCommand(dockerCli *command.DockerCli) *cobra.Command { var opts configOptions cmd := &cobra.Command{ @@ -32,7 +32,7 @@ func newConfigCommand(dockerCli *client.DockerCli) *cobra.Command { return cmd } -func runConfig(dockerCli *client.DockerCli, opts configOptions) error { +func runConfig(dockerCli *command.DockerCli, opts configOptions) error { bundle, err := loadBundlefile(dockerCli.Err(), opts.namespace, opts.bundlefile) if err != nil { return err diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index 5c03dc3d31..d72c2bd08f 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -8,11 +8,11 @@ import ( "github.com/spf13/cobra" "golang.org/x/net/context" - "github.com/docker/docker/api/client" - "github.com/docker/docker/api/client/bundlefile" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/bundlefile" ) const ( @@ -25,7 +25,7 @@ type deployOptions struct { sendRegistryAuth bool } -func newDeployCommand(dockerCli *client.DockerCli) *cobra.Command { +func newDeployCommand(dockerCli *command.DockerCli) *cobra.Command { var opts deployOptions cmd := &cobra.Command{ @@ -45,7 +45,7 @@ func newDeployCommand(dockerCli *client.DockerCli) *cobra.Command { return cmd } -func runDeploy(dockerCli *client.DockerCli, opts deployOptions) error { +func runDeploy(dockerCli *command.DockerCli, opts deployOptions) error { bundle, err := loadBundlefile(dockerCli.Err(), opts.namespace, opts.bundlefile) if err != nil { return err @@ -85,7 +85,7 @@ func getUniqueNetworkNames(services map[string]bundlefile.Service) []string { func updateNetworks( ctx context.Context, - dockerCli *client.DockerCli, + dockerCli *command.DockerCli, networks []string, namespace string, ) error { @@ -133,7 +133,7 @@ func convertNetworks(networks []string, namespace string, name string) []swarm.N func deployServices( ctx context.Context, - dockerCli *client.DockerCli, + dockerCli *command.DockerCli, services map[string]bundlefile.Service, namespace string, sendAuth bool, diff --git a/components/cli/command/stack/opts.go b/components/cli/command/stack/opts.go index 345bdc38f5..eef4d0e45b 100644 --- a/components/cli/command/stack/opts.go +++ b/components/cli/command/stack/opts.go @@ -7,7 +7,7 @@ import ( "io" "os" - "github.com/docker/docker/api/client/bundlefile" + "github.com/docker/docker/cli/command/bundlefile" "github.com/spf13/pflag" ) diff --git a/components/cli/command/stack/ps.go b/components/cli/command/stack/ps.go index 9d9458d85f..c4683b68a0 100644 --- a/components/cli/command/stack/ps.go +++ b/components/cli/command/stack/ps.go @@ -7,12 +7,12 @@ import ( "golang.org/x/net/context" - "github.com/docker/docker/api/client" - "github.com/docker/docker/api/client/idresolver" - "github.com/docker/docker/api/client/task" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/idresolver" + "github.com/docker/docker/cli/command/task" "github.com/docker/docker/opts" "github.com/spf13/cobra" ) @@ -25,7 +25,7 @@ type psOptions struct { noResolve bool } -func newPsCommand(dockerCli *client.DockerCli) *cobra.Command { +func newPsCommand(dockerCli *command.DockerCli) *cobra.Command { opts := psOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -46,7 +46,7 @@ func newPsCommand(dockerCli *client.DockerCli) *cobra.Command { return cmd } -func runPS(dockerCli *client.DockerCli, opts psOptions) error { +func runPS(dockerCli *command.DockerCli, opts psOptions) error { namespace := opts.namespace client := dockerCli.Client() ctx := context.Background() diff --git a/components/cli/command/stack/remove.go b/components/cli/command/stack/remove.go index 9ba91e5c23..6ab005d71d 100644 --- a/components/cli/command/stack/remove.go +++ b/components/cli/command/stack/remove.go @@ -7,8 +7,8 @@ import ( "golang.org/x/net/context" - "github.com/docker/docker/api/client" "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" "github.com/spf13/cobra" ) @@ -16,7 +16,7 @@ type removeOptions struct { namespace string } -func newRemoveCommand(dockerCli *client.DockerCli) *cobra.Command { +func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { var opts removeOptions cmd := &cobra.Command{ @@ -32,7 +32,7 @@ func newRemoveCommand(dockerCli *client.DockerCli) *cobra.Command { return cmd } -func runRemove(dockerCli *client.DockerCli, opts removeOptions) error { +func runRemove(dockerCli *command.DockerCli, opts removeOptions) error { namespace := opts.namespace client := dockerCli.Client() stderr := dockerCli.Err() diff --git a/components/cli/command/stack/services.go b/components/cli/command/stack/services.go index 819b1c6759..22906378d6 100644 --- a/components/cli/command/stack/services.go +++ b/components/cli/command/stack/services.go @@ -7,11 +7,11 @@ import ( "golang.org/x/net/context" - "github.com/docker/docker/api/client" - "github.com/docker/docker/api/client/service" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/service" "github.com/docker/docker/opts" "github.com/spf13/cobra" ) @@ -26,7 +26,7 @@ type servicesOptions struct { namespace string } -func newServicesCommand(dockerCli *client.DockerCli) *cobra.Command { +func newServicesCommand(dockerCli *command.DockerCli) *cobra.Command { opts := servicesOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -45,7 +45,7 @@ func newServicesCommand(dockerCli *client.DockerCli) *cobra.Command { return cmd } -func runServices(dockerCli *client.DockerCli, opts servicesOptions) error { +func runServices(dockerCli *command.DockerCli, opts servicesOptions) error { ctx := context.Background() client := dockerCli.Client() From b924adb9864618965d22eb43ffbc78f09e1ba7ce Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 8 Sep 2016 15:11:38 -0400 Subject: [PATCH 117/978] Fix a test that expects whitespace at the end of the line. Signed-off-by: Daniel Nephin Upstream-commit: e2f7387906ded9b495ac7be8e4d1fcca7d645609 Component: cli --- components/cli/command/container/hijack.go | 2 +- components/cli/command/container/tty.go | 2 +- .../cli/command/formatter/container_test.go | 29 +++++++++---------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/components/cli/command/container/hijack.go b/components/cli/command/container/hijack.go index 855a152904..ea429245cf 100644 --- a/components/cli/command/container/hijack.go +++ b/components/cli/command/container/hijack.go @@ -6,8 +6,8 @@ import ( "sync" "github.com/Sirupsen/logrus" - "github.com/docker/docker/cli/command" "github.com/docker/docker/api/types" + "github.com/docker/docker/cli/command" "github.com/docker/docker/pkg/stdcopy" "golang.org/x/net/context" ) diff --git a/components/cli/command/container/tty.go b/components/cli/command/container/tty.go index 5360c6b040..edb11592d3 100644 --- a/components/cli/command/container/tty.go +++ b/components/cli/command/container/tty.go @@ -8,8 +8,8 @@ import ( "time" "github.com/Sirupsen/logrus" - "github.com/docker/docker/cli/command" "github.com/docker/docker/api/types" + "github.com/docker/docker/cli/command" "github.com/docker/docker/client" "github.com/docker/docker/pkg/signal" "golang.org/x/net/context" diff --git a/components/cli/command/formatter/container_test.go b/components/cli/command/formatter/container_test.go index deaa915a89..29b8450db9 100644 --- a/components/cli/command/formatter/container_test.go +++ b/components/cli/command/formatter/container_test.go @@ -9,6 +9,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/testutil/assert" ) func TestContainerPsContext(t *testing.T) { @@ -232,19 +233,19 @@ containerID2 ubuntu "" 24 hours ago image: ubuntu command: "" created_at: %s -status: +status: names: foobar_baz -labels: -ports: +labels: +ports: container_id: containerID2 image: ubuntu command: "" created_at: %s -status: +status: names: foobar_bar -labels: -ports: +labels: +ports: `, expectedTime, expectedTime), }, @@ -259,20 +260,20 @@ ports: image: ubuntu command: "" created_at: %s -status: +status: names: foobar_baz -labels: -ports: +labels: +ports: size: 0 B container_id: containerID2 image: ubuntu command: "" created_at: %s -status: +status: names: foobar_bar -labels: -ports: +labels: +ports: size: 0 B `, expectedTime, expectedTime), @@ -316,9 +317,7 @@ size: 0 B context.context.Containers = containers context.context.Write() actual := out.String() - if actual != context.expected { - t.Fatalf("Expected \n%s, got \n%s", context.expected, actual) - } + assert.Equal(t, actual, context.expected) // Clean buffer out.Reset() } From 92630b806732e39f19b16c64a803e6c311e762f9 Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Thu, 8 Sep 2016 15:37:45 -0700 Subject: [PATCH 118/978] 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 Upstream-commit: 450b3123e30d7920b2e7203a546483f06e9ad4d1 Component: cli --- components/cli/request.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/components/cli/request.go b/components/cli/request.go index 024e973520..7b4f5406b8 100644 --- a/components/cli/request.go +++ b/components/cli/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 9c57f9b583c41892c207df3a4ee463fe4212c970 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 4 Sep 2016 14:44:34 -0700 Subject: [PATCH 119/978] Restrict size to 2 fractional digits for `docker images` This fix tries to address the issue raised in 26300. Previously `docker images` will use `HumanSize()` to display the size which has a fixed precision of 4 (thus 3 fractional digits). This could be problematic in certain languages (e.g. , German, see 26300) as `.` may be interpreted as thousands-separator in number. This fix use `CustomSize()` instead and limit the precision to 3 (thus 2 fractional digits). This fix has been tested manually. This fix fixes 26300. Signed-off-by: Yong Tang Upstream-commit: c68bb5795901881bf1c42faca66c474e1445d3b3 Component: cli --- components/cli/command/container/stats_helpers.go | 4 ++-- components/cli/command/container/stats_unit_test.go | 2 +- components/cli/command/formatter/container.go | 4 ++-- components/cli/command/formatter/image.go | 2 +- components/cli/command/image/history.go | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/components/cli/command/container/stats_helpers.go b/components/cli/command/container/stats_helpers.go index b5e8e0472f..54cc5589c1 100644 --- a/components/cli/command/container/stats_helpers.go +++ b/components/cli/command/container/stats_helpers.go @@ -194,8 +194,8 @@ func (s *containerStats) Display(w io.Writer) error { s.CPUPercentage, units.BytesSize(s.Memory), units.BytesSize(s.MemoryLimit), s.MemoryPercentage, - units.HumanSize(s.NetworkRx), units.HumanSize(s.NetworkTx), - units.HumanSize(s.BlockRead), units.HumanSize(s.BlockWrite), + units.HumanSizeWithPrecision(s.NetworkRx, 3), units.HumanSizeWithPrecision(s.NetworkTx, 3), + units.HumanSizeWithPrecision(s.BlockRead, 3), units.HumanSizeWithPrecision(s.BlockWrite, 3), s.PidsCurrent) return nil } diff --git a/components/cli/command/container/stats_unit_test.go b/components/cli/command/container/stats_unit_test.go index 6f6a468068..182ab5b30d 100644 --- a/components/cli/command/container/stats_unit_test.go +++ b/components/cli/command/container/stats_unit_test.go @@ -25,7 +25,7 @@ func TestDisplay(t *testing.T) { t.Fatalf("c.Display() gave error: %s", err) } got := b.String() - want := "app\t30.00%\t100 MiB / 2 GiB\t4.88%\t104.9 MB / 838.9 MB\t104.9 MB / 838.9 MB\t1\n" + want := "app\t30.00%\t100 MiB / 2 GiB\t4.88%\t105 MB / 839 MB\t105 MB / 839 MB\t1\n" if got != want { t.Fatalf("c.Display() = %q, want %q", got, want) } diff --git a/components/cli/command/formatter/container.go b/components/cli/command/formatter/container.go index f1c985791b..6f519e4493 100644 --- a/components/cli/command/formatter/container.go +++ b/components/cli/command/formatter/container.go @@ -152,8 +152,8 @@ func (c *containerContext) Status() string { func (c *containerContext) Size() string { c.addHeader(sizeHeader) - srw := units.HumanSize(float64(c.c.SizeRw)) - sv := units.HumanSize(float64(c.c.SizeRootFs)) + srw := units.HumanSizeWithPrecision(float64(c.c.SizeRw), 3) + sv := units.HumanSizeWithPrecision(float64(c.c.SizeRootFs), 3) sf := srw if c.c.SizeRootFs > 0 { diff --git a/components/cli/command/formatter/image.go b/components/cli/command/formatter/image.go index 0ffcfaf728..012860e04e 100644 --- a/components/cli/command/formatter/image.go +++ b/components/cli/command/formatter/image.go @@ -225,5 +225,5 @@ func (c *imageContext) CreatedAt() string { func (c *imageContext) Size() string { c.addHeader(sizeHeader) - return units.HumanSize(float64(c.i.Size)) + return units.HumanSizeWithPrecision(float64(c.i.Size), 3) } diff --git a/components/cli/command/image/history.go b/components/cli/command/image/history.go index a75403a45f..91c8f75a63 100644 --- a/components/cli/command/image/history.go +++ b/components/cli/command/image/history.go @@ -86,7 +86,7 @@ func runHistory(dockerCli *command.DockerCli, opts historyOptions) error { if opts.human { created = units.HumanDuration(time.Now().UTC().Sub(time.Unix(entry.Created, 0))) + " ago" - size = units.HumanSize(float64(entry.Size)) + size = units.HumanSizeWithPrecision(float64(entry.Size), 3) } else { created = time.Unix(entry.Created, 0).Format(time.RFC3339) size = strconv.FormatInt(entry.Size, 10) From cef9a66721a120f541265eb538701a59888aac4a Mon Sep 17 00:00:00 2001 From: boucher Date: Thu, 12 May 2016 10:52:00 -0400 Subject: [PATCH 120/978] Initial implementation of containerd Checkpoint API. Signed-off-by: boucher Upstream-commit: f0647193dc8b6a3d468d10e9c1fca10668836cb5 Component: cli --- components/cli/command/checkpoint/cmd.go | 12 +++++ .../command/checkpoint/cmd_experimental.go | 31 +++++++++++ components/cli/command/checkpoint/create.go | 54 +++++++++++++++++++ components/cli/command/checkpoint/list.go | 47 ++++++++++++++++ components/cli/command/checkpoint/remove.go | 28 ++++++++++ components/cli/command/commands/commands.go | 2 + components/cli/command/container/start.go | 19 ++++++- .../cli/command/container/start_utils.go | 8 +++ .../container/start_utils_experimental.go | 9 ++++ 9 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 components/cli/command/checkpoint/cmd.go create mode 100644 components/cli/command/checkpoint/cmd_experimental.go create mode 100644 components/cli/command/checkpoint/create.go create mode 100644 components/cli/command/checkpoint/list.go create mode 100644 components/cli/command/checkpoint/remove.go create mode 100644 components/cli/command/container/start_utils.go create mode 100644 components/cli/command/container/start_utils_experimental.go diff --git a/components/cli/command/checkpoint/cmd.go b/components/cli/command/checkpoint/cmd.go new file mode 100644 index 0000000000..cbeb951793 --- /dev/null +++ b/components/cli/command/checkpoint/cmd.go @@ -0,0 +1,12 @@ +// +build !experimental + +package checkpoint + +import ( + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +// NewCheckpointCommand returns a cobra command for `checkpoint` subcommands +func NewCheckpointCommand(rootCmd *cobra.Command, dockerCli *command.DockerCli) { +} diff --git a/components/cli/command/checkpoint/cmd_experimental.go b/components/cli/command/checkpoint/cmd_experimental.go new file mode 100644 index 0000000000..b7e614ca6f --- /dev/null +++ b/components/cli/command/checkpoint/cmd_experimental.go @@ -0,0 +1,31 @@ +// +build experimental + +package checkpoint + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" +) + +// NewCheckpointCommand returns a cobra command for `checkpoint` subcommands +func NewCheckpointCommand(rootCmd *cobra.Command, dockerCli *command.DockerCli) { + cmd := &cobra.Command{ + Use: "checkpoint", + Short: "Manage Container Checkpoints", + Args: cli.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + }, + } + cmd.AddCommand( + newCreateCommand(dockerCli), + newListCommand(dockerCli), + newRemoveCommand(dockerCli), + ) + + rootCmd.AddCommand(cmd) +} diff --git a/components/cli/command/checkpoint/create.go b/components/cli/command/checkpoint/create.go new file mode 100644 index 0000000000..42b316fe2a --- /dev/null +++ b/components/cli/command/checkpoint/create.go @@ -0,0 +1,54 @@ +// +build experimental + +package checkpoint + +import ( + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +type createOptions struct { + container string + checkpoint string + leaveRunning bool +} + +func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts createOptions + + cmd := &cobra.Command{ + Use: "create CONTAINER CHECKPOINT", + Short: "Create a checkpoint from a running container", + Args: cli.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + opts.container = args[0] + opts.checkpoint = args[1] + return runCreate(dockerCli, opts) + }, + } + + flags := cmd.Flags() + flags.BoolVar(&opts.leaveRunning, "leave-running", false, "leave the container running after checkpoing") + + return cmd +} + +func runCreate(dockerCli *command.DockerCli, opts createOptions) error { + client := dockerCli.Client() + + checkpointOpts := types.CheckpointCreateOptions{ + CheckpointID: opts.checkpoint, + Exit: !opts.leaveRunning, + } + + err := client.CheckpointCreate(context.Background(), opts.container, checkpointOpts) + if err != nil { + return err + } + + return nil +} diff --git a/components/cli/command/checkpoint/list.go b/components/cli/command/checkpoint/list.go new file mode 100644 index 0000000000..6d22531d45 --- /dev/null +++ b/components/cli/command/checkpoint/list.go @@ -0,0 +1,47 @@ +// +build experimental + +package checkpoint + +import ( + "fmt" + "text/tabwriter" + + "golang.org/x/net/context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +func newListCommand(dockerCli *command.DockerCli) *cobra.Command { + return &cobra.Command{ + Use: "ls CONTAINER", + Aliases: []string{"list"}, + Short: "List checkpoints for a container", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runList(dockerCli, args[0]) + }, + } +} + +func runList(dockerCli *command.DockerCli, container string) error { + client := dockerCli.Client() + + checkpoints, err := client.CheckpointList(context.Background(), container) + if err != nil { + return err + } + + w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0) + fmt.Fprintf(w, "CHECKPOINT NAME") + fmt.Fprintf(w, "\n") + + for _, checkpoint := range checkpoints { + fmt.Fprintf(w, "%s\t", checkpoint.Name) + fmt.Fprint(w, "\n") + } + + w.Flush() + return nil +} diff --git a/components/cli/command/checkpoint/remove.go b/components/cli/command/checkpoint/remove.go new file mode 100644 index 0000000000..6605c5e472 --- /dev/null +++ b/components/cli/command/checkpoint/remove.go @@ -0,0 +1,28 @@ +// +build experimental + +package checkpoint + +import ( + "golang.org/x/net/context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { + return &cobra.Command{ + Use: "rm CONTAINER CHECKPOINT", + Aliases: []string{"remove"}, + Short: "Remove a checkpoint", + Args: cli.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + return runRemove(dockerCli, args[0], args[1]) + }, + } +} + +func runRemove(dockerCli *command.DockerCli, container string, checkpoint string) error { + client := dockerCli.Client() + return client.CheckpointDelete(context.Background(), container, checkpoint) +} diff --git a/components/cli/command/commands/commands.go b/components/cli/command/commands/commands.go index 35fd6860b0..0adf8e3f3e 100644 --- a/components/cli/command/commands/commands.go +++ b/components/cli/command/commands/commands.go @@ -2,6 +2,7 @@ package commands import ( "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/checkpoint" "github.com/docker/docker/cli/command/container" "github.com/docker/docker/cli/command/image" "github.com/docker/docker/cli/command/network" @@ -67,5 +68,6 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) { volume.NewVolumeCommand(dockerCli), system.NewInfoCommand(dockerCli), ) + checkpoint.NewCheckpointCommand(cmd, dockerCli) plugin.NewPluginCommand(cmd, dockerCli) } diff --git a/components/cli/command/container/start.go b/components/cli/command/container/start.go index e72369177a..9f414a7c66 100644 --- a/components/cli/command/container/start.go +++ b/components/cli/command/container/start.go @@ -20,6 +20,7 @@ type startOptions struct { attach bool openStdin bool detachKeys string + checkpoint string containers []string } @@ -42,6 +43,9 @@ func NewStartCommand(dockerCli *command.DockerCli) *cobra.Command { flags.BoolVarP(&opts.attach, "attach", "a", false, "Attach STDOUT/STDERR and forward signals") flags.BoolVarP(&opts.openStdin, "interactive", "i", false, "Attach container's STDIN") flags.StringVar(&opts.detachKeys, "detach-keys", "", "Override the key sequence for detaching a container") + + addExperimentalStartFlags(flags, &opts) + return cmd } @@ -105,9 +109,12 @@ func runStart(dockerCli *command.DockerCli, opts *startOptions) error { // 3. We should open a channel for receiving status code of the container // no matter it's detached, removed on daemon side(--rm) or exit normally. statusChan, statusErr := waitExitOrRemoved(dockerCli, context.Background(), c.ID, c.HostConfig.AutoRemove) + startOptions := types.ContainerStartOptions{ + CheckpointID: opts.checkpoint, + } // 4. Start the container. - if err := dockerCli.Client().ContainerStart(ctx, c.ID, types.ContainerStartOptions{}); err != nil { + if err := dockerCli.Client().ContainerStart(ctx, c.ID, startOptions); err != nil { cancelFun() <-cErr if c.HostConfig.AutoRemove && statusErr == nil { @@ -134,6 +141,16 @@ func runStart(dockerCli *command.DockerCli, opts *startOptions) error { if status := <-statusChan; status != 0 { return cli.StatusError{StatusCode: status} } + } else if opts.checkpoint != "" { + if len(opts.containers) > 1 { + return fmt.Errorf("You cannot restore multiple containers at once.") + } + container := opts.containers[0] + startOptions := types.ContainerStartOptions{ + CheckpointID: opts.checkpoint, + } + return dockerCli.Client().ContainerStart(ctx, container, startOptions) + } else { // We're not going to attach to anything. // Start as many containers as we want. diff --git a/components/cli/command/container/start_utils.go b/components/cli/command/container/start_utils.go new file mode 100644 index 0000000000..689d742f06 --- /dev/null +++ b/components/cli/command/container/start_utils.go @@ -0,0 +1,8 @@ +// +build !experimental + +package container + +import "github.com/spf13/pflag" + +func addExperimentalStartFlags(flags *pflag.FlagSet, opts *startOptions) { +} diff --git a/components/cli/command/container/start_utils_experimental.go b/components/cli/command/container/start_utils_experimental.go new file mode 100644 index 0000000000..43c64f431c --- /dev/null +++ b/components/cli/command/container/start_utils_experimental.go @@ -0,0 +1,9 @@ +// +build experimental + +package container + +import "github.com/spf13/pflag" + +func addExperimentalStartFlags(flags *pflag.FlagSet, opts *startOptions) { + flags.StringVar(&opts.checkpoint, "checkpoint", "", "Restore from this checkpoint") +} From 46699a2a41e62580e70d10a14f719cbe7a7dd46d Mon Sep 17 00:00:00 2001 From: boucher Date: Tue, 30 Aug 2016 10:10:09 -0400 Subject: [PATCH 121/978] Fix typo Signed-off-by: boucher Upstream-commit: 9524caa317df82c812a278393e9e5e1b6440e1ec Component: cli --- components/cli/command/checkpoint/create.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/checkpoint/create.go b/components/cli/command/checkpoint/create.go index 42b316fe2a..f214574556 100644 --- a/components/cli/command/checkpoint/create.go +++ b/components/cli/command/checkpoint/create.go @@ -32,7 +32,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { } flags := cmd.Flags() - flags.BoolVar(&opts.leaveRunning, "leave-running", false, "leave the container running after checkpoing") + flags.BoolVar(&opts.leaveRunning, "leave-running", false, "leave the container running after checkpoint") return cmd } From bea65674777daef1fe69fbe0c7e0611c0f104918 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 29 Aug 2016 14:45:29 -0400 Subject: [PATCH 122/978] Move image trust related cli methods into the image package. Signed-off-by: Daniel Nephin Upstream-commit: 0cf85349f3810de9feafe615208621aa1d89178f Component: cli --- components/cli/command/cli.go | 12 +- components/cli/command/container/create.go | 5 +- components/cli/command/container/hijack.go | 11 +- components/cli/command/image/build.go | 7 +- components/cli/command/image/pull.go | 4 +- components/cli/command/image/push.go | 4 +- components/cli/command/image/trust.go | 576 ++++++++++++++++++ .../cli/command/{ => image}/trust_test.go | 2 +- components/cli/command/trust.go | 563 +---------------- 9 files changed, 604 insertions(+), 580 deletions(-) create mode 100644 components/cli/command/image/trust.go rename components/cli/command/{ => image}/trust_test.go (99%) diff --git a/components/cli/command/cli.go b/components/cli/command/cli.go index 6194c7fe93..63397bf920 100644 --- a/components/cli/command/cli.go +++ b/components/cli/command/cli.go @@ -21,6 +21,13 @@ import ( "github.com/docker/go-connections/tlsconfig" ) +// Streams is an interface which exposes the standard input and output streams +type Streams interface { + In() *InStream + Out() *OutStream + Err() io.Writer +} + // DockerCli represents the docker command line client. // Instances of the client can be returned from NewDockerCli. type DockerCli struct { @@ -105,7 +112,7 @@ func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile. if customHeaders == nil { customHeaders = map[string]string{} } - customHeaders["User-Agent"] = clientUserAgent() + customHeaders["User-Agent"] = UserAgent() verStr := api.DefaultVersion if tmpStr := os.Getenv("DOCKER_API_VERSION"); tmpStr != "" { @@ -159,6 +166,7 @@ func newHTTPClient(host string, tlsOptions *tlsconfig.Options) (*http.Client, er }, nil } -func clientUserAgent() string { +// UserAgent returns the user agent string used for making API requests +func UserAgent() string { return "Docker-Client/" + dockerversion.Version + " (" + runtime.GOOS + ")" } diff --git a/components/cli/command/container/create.go b/components/cli/command/container/create.go index 95e8d95ed9..b80b6e1e5a 100644 --- a/components/cli/command/container/create.go +++ b/components/cli/command/container/create.go @@ -9,6 +9,7 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/image" "github.com/docker/docker/pkg/jsonmessage" // FIXME migrate to docker/distribution/reference "github.com/docker/docker/api/types" @@ -169,7 +170,7 @@ func createContainer(ctx context.Context, dockerCli *command.DockerCli, config * if ref, ok := ref.(reference.NamedTagged); ok && command.IsTrusted() { var err error - trustedRef, err = dockerCli.TrustedReference(ctx, ref) + trustedRef, err = image.TrustedReference(ctx, dockerCli, ref) if err != nil { return nil, err } @@ -190,7 +191,7 @@ func createContainer(ctx context.Context, dockerCli *command.DockerCli, config * return nil, err } if ref, ok := ref.(reference.NamedTagged); ok && trustedRef != nil { - if err := dockerCli.TagTrusted(ctx, trustedRef, ref); err != nil { + if err := image.TagTrusted(ctx, dockerCli, trustedRef, ref); err != nil { return nil, err } } diff --git a/components/cli/command/container/hijack.go b/components/cli/command/container/hijack.go index ea429245cf..ca136f0e43 100644 --- a/components/cli/command/container/hijack.go +++ b/components/cli/command/container/hijack.go @@ -12,14 +12,9 @@ import ( "golang.org/x/net/context" ) -type streams interface { - In() *command.InStream - Out() *command.OutStream -} - // holdHijackedConnection handles copying input to and output from streams to the // connection -func holdHijackedConnection(ctx context.Context, streams streams, tty bool, inputStream io.ReadCloser, outputStream, errorStream io.Writer, resp types.HijackedResponse) error { +func holdHijackedConnection(ctx context.Context, streams command.Streams, tty bool, inputStream io.ReadCloser, outputStream, errorStream io.Writer, resp types.HijackedResponse) error { var ( err error restoreOnce sync.Once @@ -100,14 +95,14 @@ func holdHijackedConnection(ctx context.Context, streams streams, tty bool, inpu return nil } -func setRawTerminal(streams streams) error { +func setRawTerminal(streams command.Streams) error { if err := streams.In().SetRawTerminal(); err != nil { return err } return streams.Out().SetRawTerminal() } -func restoreTerminal(streams streams, in io.Closer) error { +func restoreTerminal(streams command.Streams, in io.Closer) error { streams.In().RestoreTerminal() streams.Out().RestoreTerminal() // WARNING: DO NOT REMOVE THE OS CHECK !!! diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go index 10ad413f25..06ee32ba83 100644 --- a/components/cli/command/image/build.go +++ b/components/cli/command/image/build.go @@ -220,9 +220,12 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { var resolvedTags []*resolvedTag if command.IsTrusted() { + translator := func(ctx context.Context, ref reference.NamedTagged) (reference.Canonical, error) { + return TrustedReference(ctx, dockerCli, ref) + } // Wrap the tar archive to replace the Dockerfile entry with the rewritten // Dockerfile which uses trusted pulls. - buildCtx = replaceDockerfileTarWrapper(ctx, buildCtx, relDockerfile, dockerCli.TrustedReference, &resolvedTags) + buildCtx = replaceDockerfileTarWrapper(ctx, buildCtx, relDockerfile, translator, &resolvedTags) } // Setup an upload progress bar @@ -323,7 +326,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { // Since the build was successful, now we must tag any of the resolved // images from the above Dockerfile rewrite. for _, resolved := range resolvedTags { - if err := dockerCli.TagTrusted(ctx, resolved.digestRef, resolved.tagRef); err != nil { + if err := TagTrusted(ctx, dockerCli, resolved.digestRef, resolved.tagRef); err != nil { return err } } diff --git a/components/cli/command/image/pull.go b/components/cli/command/image/pull.go index 88ccb47342..3f3093a5d8 100644 --- a/components/cli/command/image/pull.go +++ b/components/cli/command/image/pull.go @@ -78,9 +78,9 @@ func runPull(dockerCli *command.DockerCli, opts pullOptions) error { if command.IsTrusted() && !registryRef.HasDigest() { // Check if tag is digest - err = dockerCli.TrustedPull(ctx, repoInfo, registryRef, authConfig, requestPrivilege) + err = trustedPull(ctx, dockerCli, repoInfo, registryRef, authConfig, requestPrivilege) } else { - err = dockerCli.ImagePullPrivileged(ctx, authConfig, distributionRef.String(), requestPrivilege, opts.all) + err = imagePullPrivileged(ctx, dockerCli, authConfig, distributionRef.String(), requestPrivilege, opts.all) } if err != nil { if strings.Contains(err.Error(), "target is a plugin") { diff --git a/components/cli/command/image/push.go b/components/cli/command/image/push.go index 62b637f6ee..a98de9e707 100644 --- a/components/cli/command/image/push.go +++ b/components/cli/command/image/push.go @@ -48,10 +48,10 @@ func runPush(dockerCli *command.DockerCli, remote string) error { requestPrivilege := dockerCli.RegistryAuthenticationPrivilegedFunc(repoInfo.Index, "push") if command.IsTrusted() { - return dockerCli.TrustedPush(ctx, repoInfo, ref, authConfig, requestPrivilege) + return trustedPush(ctx, dockerCli, repoInfo, ref, authConfig, requestPrivilege) } - responseBody, err := dockerCli.ImagePushPrivileged(ctx, authConfig, ref.String(), requestPrivilege) + responseBody, err := imagePushPrivileged(ctx, dockerCli, authConfig, ref.String(), requestPrivilege) if err != nil { return err } diff --git a/components/cli/command/image/trust.go b/components/cli/command/image/trust.go new file mode 100644 index 0000000000..f0948cc808 --- /dev/null +++ b/components/cli/command/image/trust.go @@ -0,0 +1,576 @@ +package image + +import ( + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/url" + "os" + "path" + "path/filepath" + "sort" + "time" + + "golang.org/x/net/context" + + "github.com/Sirupsen/logrus" + "github.com/docker/distribution/digest" + "github.com/docker/distribution/registry/client/auth" + "github.com/docker/distribution/registry/client/transport" + "github.com/docker/docker/api/types" + registrytypes "github.com/docker/docker/api/types/registry" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cliconfig" + "github.com/docker/docker/distribution" + "github.com/docker/docker/pkg/jsonmessage" + "github.com/docker/docker/reference" + "github.com/docker/docker/registry" + "github.com/docker/go-connections/tlsconfig" + "github.com/docker/notary/client" + "github.com/docker/notary/passphrase" + "github.com/docker/notary/trustmanager" + "github.com/docker/notary/trustpinning" + "github.com/docker/notary/tuf/data" + "github.com/docker/notary/tuf/signed" + "github.com/docker/notary/tuf/store" +) + +var ( + releasesRole = path.Join(data.CanonicalTargetsRole, "releases") +) + +type target struct { + reference registry.Reference + digest digest.Digest + size int64 +} + +// trustedPush handles content trust pushing of an image +func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error { + responseBody, err := imagePushPrivileged(ctx, cli, authConfig, ref.String(), requestPrivilege) + if err != nil { + return err + } + + defer responseBody.Close() + + // If it is a trusted push we would like to find the target entry which match the + // tag provided in the function and then do an AddTarget later. + target := &client.Target{} + // Count the times of calling for handleTarget, + // if it is called more that once, that should be considered an error in a trusted push. + cnt := 0 + handleTarget := func(aux *json.RawMessage) { + cnt++ + if cnt > 1 { + // handleTarget should only be called one. This will be treated as an error. + return + } + + var pushResult distribution.PushResult + err := json.Unmarshal(*aux, &pushResult) + if err == nil && pushResult.Tag != "" && pushResult.Digest.Validate() == nil { + h, err := hex.DecodeString(pushResult.Digest.Hex()) + if err != nil { + target = nil + return + } + target.Name = registry.ParseReference(pushResult.Tag).String() + target.Hashes = data.Hashes{string(pushResult.Digest.Algorithm()): h} + target.Length = int64(pushResult.Size) + } + } + + var tag string + switch x := ref.(type) { + case reference.Canonical: + return errors.New("cannot push a digest reference") + case reference.NamedTagged: + tag = x.Tag() + } + + // We want trust signatures to always take an explicit tag, + // otherwise it will act as an untrusted push. + if tag == "" { + if err = jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), nil); err != nil { + return err + } + fmt.Fprintln(cli.Out(), "No tag specified, skipping trust metadata push") + return nil + } + + if err = jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), handleTarget); err != nil { + return err + } + + if cnt > 1 { + return fmt.Errorf("internal error: only one call to handleTarget expected") + } + + if target == nil { + fmt.Fprintln(cli.Out(), "No targets found, please provide a specific tag in order to sign it") + return nil + } + + fmt.Fprintln(cli.Out(), "Signing and pushing trust metadata") + + repo, err := GetNotaryRepository(cli, repoInfo, authConfig, "push", "pull") + if err != nil { + fmt.Fprintf(cli.Out(), "Error establishing connection to notary repository: %s\n", err) + return err + } + + // get the latest repository metadata so we can figure out which roles to sign + err = repo.Update(false) + + switch err.(type) { + case client.ErrRepoNotInitialized, client.ErrRepositoryNotExist: + keys := repo.CryptoService.ListKeys(data.CanonicalRootRole) + var rootKeyID string + // always select the first root key + if len(keys) > 0 { + sort.Strings(keys) + rootKeyID = keys[0] + } else { + rootPublicKey, err := repo.CryptoService.Create(data.CanonicalRootRole, "", data.ECDSAKey) + if err != nil { + return err + } + rootKeyID = rootPublicKey.ID() + } + + // Initialize the notary repository with a remotely managed snapshot key + if err := repo.Initialize(rootKeyID, data.CanonicalSnapshotRole); err != nil { + return notaryError(repoInfo.FullName(), err) + } + fmt.Fprintf(cli.Out(), "Finished initializing %q\n", repoInfo.FullName()) + err = repo.AddTarget(target, data.CanonicalTargetsRole) + case nil: + // already initialized and we have successfully downloaded the latest metadata + err = addTargetToAllSignableRoles(repo, target) + default: + return notaryError(repoInfo.FullName(), err) + } + + if err == nil { + err = repo.Publish() + } + + if err != nil { + fmt.Fprintf(cli.Out(), "Failed to sign %q:%s - %s\n", repoInfo.FullName(), tag, err.Error()) + return notaryError(repoInfo.FullName(), err) + } + + fmt.Fprintf(cli.Out(), "Successfully signed %q:%s\n", repoInfo.FullName(), tag) + return nil +} + +// Attempt to add the image target to all the top level delegation roles we can +// (based on whether we have the signing key and whether the role's path allows +// us to). +// If there are no delegation roles, we add to the targets role. +func addTargetToAllSignableRoles(repo *client.NotaryRepository, target *client.Target) error { + var signableRoles []string + + // translate the full key names, which includes the GUN, into just the key IDs + allCanonicalKeyIDs := make(map[string]struct{}) + for fullKeyID := range repo.CryptoService.ListAllKeys() { + allCanonicalKeyIDs[path.Base(fullKeyID)] = struct{}{} + } + + allDelegationRoles, err := repo.GetDelegationRoles() + if err != nil { + return err + } + + // if there are no delegation roles, then just try to sign it into the targets role + if len(allDelegationRoles) == 0 { + return repo.AddTarget(target, data.CanonicalTargetsRole) + } + + // there are delegation roles, find every delegation role we have a key for, and + // attempt to sign into into all those roles. + for _, delegationRole := range allDelegationRoles { + // We do not support signing any delegation role that isn't a direct child of the targets role. + // Also don't bother checking the keys if we can't add the target + // to this role due to path restrictions + if path.Dir(delegationRole.Name) != data.CanonicalTargetsRole || !delegationRole.CheckPaths(target.Name) { + continue + } + + for _, canonicalKeyID := range delegationRole.KeyIDs { + if _, ok := allCanonicalKeyIDs[canonicalKeyID]; ok { + signableRoles = append(signableRoles, delegationRole.Name) + break + } + } + } + + if len(signableRoles) == 0 { + return fmt.Errorf("no valid signing keys for delegation roles") + } + + return repo.AddTarget(target, signableRoles...) +} + +// imagePushPrivileged push the image +func imagePushPrivileged(ctx context.Context, cli *command.DockerCli, authConfig types.AuthConfig, ref string, requestPrivilege types.RequestPrivilegeFunc) (io.ReadCloser, error) { + encodedAuth, err := command.EncodeAuthToBase64(authConfig) + if err != nil { + return nil, err + } + options := types.ImagePushOptions{ + RegistryAuth: encodedAuth, + PrivilegeFunc: requestPrivilege, + } + + return cli.Client().ImagePush(ctx, ref, options) +} + +// trustedPull handles content trust pulling of an image +func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry.RepositoryInfo, ref registry.Reference, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error { + var refs []target + + notaryRepo, err := GetNotaryRepository(cli, repoInfo, authConfig, "pull") + if err != nil { + fmt.Fprintf(cli.Out(), "Error establishing connection to trust repository: %s\n", err) + return err + } + + if ref.String() == "" { + // List all targets + targets, err := notaryRepo.ListTargets(releasesRole, data.CanonicalTargetsRole) + if err != nil { + return notaryError(repoInfo.FullName(), err) + } + for _, tgt := range targets { + t, err := convertTarget(tgt.Target) + if err != nil { + fmt.Fprintf(cli.Out(), "Skipping target for %q\n", repoInfo.Name()) + continue + } + // Only list tags in the top level targets role or the releases delegation role - ignore + // all other delegation roles + if tgt.Role != releasesRole && tgt.Role != data.CanonicalTargetsRole { + continue + } + refs = append(refs, t) + } + if len(refs) == 0 { + return notaryError(repoInfo.FullName(), fmt.Errorf("No trusted tags for %s", repoInfo.FullName())) + } + } else { + t, err := notaryRepo.GetTargetByName(ref.String(), releasesRole, data.CanonicalTargetsRole) + if err != nil { + return notaryError(repoInfo.FullName(), err) + } + // Only get the tag if it's in the top level targets role or the releases delegation role + // ignore it if it's in any other delegation roles + if t.Role != releasesRole && t.Role != data.CanonicalTargetsRole { + return notaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", ref.String())) + } + + logrus.Debugf("retrieving target for %s role\n", t.Role) + r, err := convertTarget(t.Target) + if err != nil { + return err + + } + refs = append(refs, r) + } + + for i, r := range refs { + displayTag := r.reference.String() + if displayTag != "" { + displayTag = ":" + displayTag + } + fmt.Fprintf(cli.Out(), "Pull (%d of %d): %s%s@%s\n", i+1, len(refs), repoInfo.Name(), displayTag, r.digest) + + ref, err := reference.WithDigest(repoInfo, r.digest) + if err != nil { + return err + } + if err := imagePullPrivileged(ctx, cli, authConfig, ref.String(), requestPrivilege, false); err != nil { + return err + } + + // If reference is not trusted, tag by trusted reference + if !r.reference.HasDigest() { + tagged, err := reference.WithTag(repoInfo, r.reference.String()) + if err != nil { + return err + } + trustedRef, err := reference.WithDigest(repoInfo, r.digest) + if err != nil { + return err + } + if err := TagTrusted(ctx, cli, trustedRef, tagged); err != nil { + return err + } + } + } + return nil +} + +// imagePullPrivileged pulls the image and displays it to the output +func imagePullPrivileged(ctx context.Context, cli *command.DockerCli, authConfig types.AuthConfig, ref string, requestPrivilege types.RequestPrivilegeFunc, all bool) error { + + encodedAuth, err := command.EncodeAuthToBase64(authConfig) + if err != nil { + return err + } + options := types.ImagePullOptions{ + RegistryAuth: encodedAuth, + PrivilegeFunc: requestPrivilege, + All: all, + } + + responseBody, err := cli.Client().ImagePull(ctx, ref, options) + if err != nil { + return err + } + defer responseBody.Close() + + return jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), nil) +} + +func trustDirectory() string { + return filepath.Join(cliconfig.ConfigDir(), "trust") +} + +// certificateDirectory returns the directory containing +// TLS certificates for the given server. An error is +// returned if there was an error parsing the server string. +func certificateDirectory(server string) (string, error) { + u, err := url.Parse(server) + if err != nil { + return "", err + } + + return filepath.Join(cliconfig.ConfigDir(), "tls", u.Host), nil +} + +func trustServer(index *registrytypes.IndexInfo) (string, error) { + if s := os.Getenv("DOCKER_CONTENT_TRUST_SERVER"); s != "" { + urlObj, err := url.Parse(s) + if err != nil || urlObj.Scheme != "https" { + return "", fmt.Errorf("valid https URL required for trust server, got %s", s) + } + + return s, nil + } + if index.Official { + return registry.NotaryServer, nil + } + return "https://" + index.Name, nil +} + +type simpleCredentialStore struct { + auth types.AuthConfig +} + +func (scs simpleCredentialStore) Basic(u *url.URL) (string, string) { + return scs.auth.Username, scs.auth.Password +} + +func (scs simpleCredentialStore) RefreshToken(u *url.URL, service string) string { + return scs.auth.IdentityToken +} + +func (scs simpleCredentialStore) SetRefreshToken(*url.URL, string, string) { +} + +// GetNotaryRepository returns a NotaryRepository which stores all the +// information needed to operate on a notary repository. +// It creates an HTTP transport providing authentication support. +// TODO: move this too +func GetNotaryRepository(streams command.Streams, repoInfo *registry.RepositoryInfo, authConfig types.AuthConfig, actions ...string) (*client.NotaryRepository, error) { + server, err := trustServer(repoInfo.Index) + if err != nil { + return nil, err + } + + var cfg = tlsconfig.ClientDefault() + cfg.InsecureSkipVerify = !repoInfo.Index.Secure + + // Get certificate base directory + certDir, err := certificateDirectory(server) + if err != nil { + return nil, err + } + logrus.Debugf("reading certificate directory: %s", certDir) + + if err := registry.ReadCertsDirectory(cfg, certDir); err != nil { + return nil, err + } + + base := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + Dial: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + }).Dial, + TLSHandshakeTimeout: 10 * time.Second, + TLSClientConfig: cfg, + DisableKeepAlives: true, + } + + // Skip configuration headers since request is not going to Docker daemon + modifiers := registry.DockerHeaders(command.UserAgent(), http.Header{}) + authTransport := transport.NewTransport(base, modifiers...) + pingClient := &http.Client{ + Transport: authTransport, + Timeout: 5 * time.Second, + } + endpointStr := server + "/v2/" + req, err := http.NewRequest("GET", endpointStr, nil) + if err != nil { + return nil, err + } + + challengeManager := auth.NewSimpleChallengeManager() + + resp, err := pingClient.Do(req) + if err != nil { + // Ignore error on ping to operate in offline mode + logrus.Debugf("Error pinging notary server %q: %s", endpointStr, err) + } else { + defer resp.Body.Close() + + // Add response to the challenge manager to parse out + // authentication header and register authentication method + if err := challengeManager.AddResponse(resp); err != nil { + return nil, err + } + } + + creds := simpleCredentialStore{auth: authConfig} + tokenHandler := auth.NewTokenHandler(authTransport, creds, repoInfo.FullName(), actions...) + basicHandler := auth.NewBasicHandler(creds) + modifiers = append(modifiers, transport.RequestModifier(auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))) + tr := transport.NewTransport(base, modifiers...) + + return client.NewNotaryRepository( + trustDirectory(), + repoInfo.FullName(), + server, + tr, + getPassphraseRetriever(streams), + trustpinning.TrustPinConfig{}) +} + +func getPassphraseRetriever(streams command.Streams) passphrase.Retriever { + aliasMap := map[string]string{ + "root": "root", + "snapshot": "repository", + "targets": "repository", + "default": "repository", + } + baseRetriever := passphrase.PromptRetrieverWithInOut(streams.In(), streams.Out(), aliasMap) + env := map[string]string{ + "root": os.Getenv("DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE"), + "snapshot": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), + "targets": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), + "default": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), + } + + return func(keyName string, alias string, createNew bool, numAttempts int) (string, bool, error) { + if v := env[alias]; v != "" { + return v, numAttempts > 1, nil + } + // For non-root roles, we can also try the "default" alias if it is specified + if v := env["default"]; v != "" && alias != data.CanonicalRootRole { + return v, numAttempts > 1, nil + } + return baseRetriever(keyName, alias, createNew, numAttempts) + } +} + +// TrustedReference returns the canonical trusted reference for an image reference +func TrustedReference(ctx context.Context, cli *command.DockerCli, ref reference.NamedTagged) (reference.Canonical, error) { + repoInfo, err := registry.ParseRepositoryInfo(ref) + if err != nil { + return nil, err + } + + // Resolve the Auth config relevant for this server + authConfig := cli.ResolveAuthConfig(ctx, repoInfo.Index) + + notaryRepo, err := GetNotaryRepository(cli, repoInfo, authConfig, "pull") + if err != nil { + fmt.Fprintf(cli.Out(), "Error establishing connection to trust repository: %s\n", err) + return nil, err + } + + t, err := notaryRepo.GetTargetByName(ref.Tag(), releasesRole, data.CanonicalTargetsRole) + if err != nil { + return nil, err + } + // Only list tags in the top level targets role or the releases delegation role - ignore + // all other delegation roles + if t.Role != releasesRole && t.Role != data.CanonicalTargetsRole { + return nil, notaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", ref.Tag())) + } + r, err := convertTarget(t.Target) + if err != nil { + return nil, err + + } + + return reference.WithDigest(ref, r.digest) +} + +func convertTarget(t client.Target) (target, error) { + h, ok := t.Hashes["sha256"] + if !ok { + return target{}, errors.New("no valid hash, expecting sha256") + } + return target{ + reference: registry.ParseReference(t.Name), + digest: digest.NewDigestFromHex("sha256", hex.EncodeToString(h)), + size: t.Length, + }, nil +} + +// TagTrusted tags a trusted ref +func TagTrusted(ctx context.Context, cli *command.DockerCli, trustedRef reference.Canonical, ref reference.NamedTagged) error { + fmt.Fprintf(cli.Out(), "Tagging %s as %s\n", trustedRef.String(), ref.String()) + + return cli.Client().ImageTag(ctx, trustedRef.String(), ref.String()) +} + +// notaryError formats an error message received from the notary service +func notaryError(repoName string, err error) error { + switch err.(type) { + case *json.SyntaxError: + logrus.Debugf("Notary syntax error: %s", err) + return fmt.Errorf("Error: no trust data available for remote repository %s. Try running notary server and setting DOCKER_CONTENT_TRUST_SERVER to its HTTPS address?", repoName) + case signed.ErrExpired: + return fmt.Errorf("Error: remote repository %s out-of-date: %v", repoName, err) + case trustmanager.ErrKeyNotFound: + return fmt.Errorf("Error: signing keys for remote repository %s not found: %v", repoName, err) + case *net.OpError: + return fmt.Errorf("Error: error contacting notary server: %v", err) + case store.ErrMetaNotFound: + return fmt.Errorf("Error: trust data missing for remote repository %s or remote repository not found: %v", repoName, err) + case signed.ErrInvalidKeyType: + return fmt.Errorf("Warning: potential malicious behavior - trust data mismatch for remote repository %s: %v", repoName, err) + case signed.ErrNoKeys: + return fmt.Errorf("Error: could not find signing keys for remote repository %s, or could not decrypt signing key: %v", repoName, err) + case signed.ErrLowVersion: + return fmt.Errorf("Warning: potential malicious behavior - trust data version is lower than expected for remote repository %s: %v", repoName, err) + case signed.ErrRoleThreshold: + return fmt.Errorf("Warning: potential malicious behavior - trust data has insufficient signatures for remote repository %s: %v", repoName, err) + case client.ErrRepositoryNotExist: + return fmt.Errorf("Error: remote trust data does not exist for %s: %v", repoName, err) + case signed.ErrInsufficientSignatures: + return fmt.Errorf("Error: could not produce valid signature for %s. If Yubikey was used, was touch input provided?: %v", repoName, err) + } + + return err +} diff --git a/components/cli/command/trust_test.go b/components/cli/command/image/trust_test.go similarity index 99% rename from components/cli/command/trust_test.go rename to components/cli/command/image/trust_test.go index 534815f379..ba6373f2da 100644 --- a/components/cli/command/trust_test.go +++ b/components/cli/command/image/trust_test.go @@ -1,4 +1,4 @@ -package command +package image import ( "os" diff --git a/components/cli/command/trust.go b/components/cli/command/trust.go index 329da52515..b4c8a84ee5 100644 --- a/components/cli/command/trust.go +++ b/components/cli/command/trust.go @@ -1,48 +1,15 @@ package command import ( - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "io" - "net" - "net/http" - "net/url" "os" - "path" - "path/filepath" - "sort" "strconv" - "time" - "golang.org/x/net/context" - - "github.com/Sirupsen/logrus" - "github.com/docker/distribution/digest" - "github.com/docker/distribution/registry/client/auth" - "github.com/docker/distribution/registry/client/transport" - "github.com/docker/docker/api/types" - registrytypes "github.com/docker/docker/api/types/registry" - "github.com/docker/docker/cliconfig" - "github.com/docker/docker/distribution" - "github.com/docker/docker/pkg/jsonmessage" - "github.com/docker/docker/reference" - "github.com/docker/docker/registry" - "github.com/docker/go-connections/tlsconfig" - "github.com/docker/notary/client" - "github.com/docker/notary/passphrase" - "github.com/docker/notary/trustmanager" - "github.com/docker/notary/trustpinning" - "github.com/docker/notary/tuf/data" - "github.com/docker/notary/tuf/signed" - "github.com/docker/notary/tuf/store" "github.com/spf13/pflag" ) var ( - releasesRole = path.Join(data.CanonicalTargetsRole, "releases") - untrusted bool + // TODO: make this not global + untrusted bool ) // AddTrustedFlags adds content trust flags to the current command flagset @@ -70,529 +37,3 @@ func setupTrustedFlag(verify bool) (bool, string) { func IsTrusted() bool { return !untrusted } - -type target struct { - reference registry.Reference - digest digest.Digest - size int64 -} - -func (cli *DockerCli) trustDirectory() string { - return filepath.Join(cliconfig.ConfigDir(), "trust") -} - -// certificateDirectory returns the directory containing -// TLS certificates for the given server. An error is -// returned if there was an error parsing the server string. -func (cli *DockerCli) certificateDirectory(server string) (string, error) { - u, err := url.Parse(server) - if err != nil { - return "", err - } - - return filepath.Join(cliconfig.ConfigDir(), "tls", u.Host), nil -} - -func trustServer(index *registrytypes.IndexInfo) (string, error) { - if s := os.Getenv("DOCKER_CONTENT_TRUST_SERVER"); s != "" { - urlObj, err := url.Parse(s) - if err != nil || urlObj.Scheme != "https" { - return "", fmt.Errorf("valid https URL required for trust server, got %s", s) - } - - return s, nil - } - if index.Official { - return registry.NotaryServer, nil - } - return "https://" + index.Name, nil -} - -type simpleCredentialStore struct { - auth types.AuthConfig -} - -func (scs simpleCredentialStore) Basic(u *url.URL) (string, string) { - return scs.auth.Username, scs.auth.Password -} - -func (scs simpleCredentialStore) RefreshToken(u *url.URL, service string) string { - return scs.auth.IdentityToken -} - -func (scs simpleCredentialStore) SetRefreshToken(*url.URL, string, string) { -} - -// getNotaryRepository returns a NotaryRepository which stores all the -// information needed to operate on a notary repository. -// It creates an HTTP transport providing authentication support. -func (cli *DockerCli) getNotaryRepository(repoInfo *registry.RepositoryInfo, authConfig types.AuthConfig, actions ...string) (*client.NotaryRepository, error) { - server, err := trustServer(repoInfo.Index) - if err != nil { - return nil, err - } - - var cfg = tlsconfig.ClientDefault() - cfg.InsecureSkipVerify = !repoInfo.Index.Secure - - // Get certificate base directory - certDir, err := cli.certificateDirectory(server) - if err != nil { - return nil, err - } - logrus.Debugf("reading certificate directory: %s", certDir) - - if err := registry.ReadCertsDirectory(cfg, certDir); err != nil { - return nil, err - } - - base := &http.Transport{ - Proxy: http.ProxyFromEnvironment, - Dial: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - DualStack: true, - }).Dial, - TLSHandshakeTimeout: 10 * time.Second, - TLSClientConfig: cfg, - DisableKeepAlives: true, - } - - // Skip configuration headers since request is not going to Docker daemon - modifiers := registry.DockerHeaders(clientUserAgent(), http.Header{}) - authTransport := transport.NewTransport(base, modifiers...) - pingClient := &http.Client{ - Transport: authTransport, - Timeout: 5 * time.Second, - } - endpointStr := server + "/v2/" - req, err := http.NewRequest("GET", endpointStr, nil) - if err != nil { - return nil, err - } - - challengeManager := auth.NewSimpleChallengeManager() - - resp, err := pingClient.Do(req) - if err != nil { - // Ignore error on ping to operate in offline mode - logrus.Debugf("Error pinging notary server %q: %s", endpointStr, err) - } else { - defer resp.Body.Close() - - // Add response to the challenge manager to parse out - // authentication header and register authentication method - if err := challengeManager.AddResponse(resp); err != nil { - return nil, err - } - } - - creds := simpleCredentialStore{auth: authConfig} - tokenHandler := auth.NewTokenHandler(authTransport, creds, repoInfo.FullName(), actions...) - basicHandler := auth.NewBasicHandler(creds) - modifiers = append(modifiers, transport.RequestModifier(auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))) - tr := transport.NewTransport(base, modifiers...) - - return client.NewNotaryRepository( - cli.trustDirectory(), repoInfo.FullName(), server, tr, cli.getPassphraseRetriever(), - trustpinning.TrustPinConfig{}) -} - -func convertTarget(t client.Target) (target, error) { - h, ok := t.Hashes["sha256"] - if !ok { - return target{}, errors.New("no valid hash, expecting sha256") - } - return target{ - reference: registry.ParseReference(t.Name), - digest: digest.NewDigestFromHex("sha256", hex.EncodeToString(h)), - size: t.Length, - }, nil -} - -func (cli *DockerCli) getPassphraseRetriever() passphrase.Retriever { - aliasMap := map[string]string{ - "root": "root", - "snapshot": "repository", - "targets": "repository", - "default": "repository", - } - baseRetriever := passphrase.PromptRetrieverWithInOut(cli.in, cli.out, aliasMap) - env := map[string]string{ - "root": os.Getenv("DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE"), - "snapshot": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), - "targets": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), - "default": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), - } - - return func(keyName string, alias string, createNew bool, numAttempts int) (string, bool, error) { - if v := env[alias]; v != "" { - return v, numAttempts > 1, nil - } - // For non-root roles, we can also try the "default" alias if it is specified - if v := env["default"]; v != "" && alias != data.CanonicalRootRole { - return v, numAttempts > 1, nil - } - return baseRetriever(keyName, alias, createNew, numAttempts) - } -} - -// TrustedReference returns the canonical trusted reference for an image reference -func (cli *DockerCli) TrustedReference(ctx context.Context, ref reference.NamedTagged) (reference.Canonical, error) { - repoInfo, err := registry.ParseRepositoryInfo(ref) - if err != nil { - return nil, err - } - - // Resolve the Auth config relevant for this server - authConfig := cli.ResolveAuthConfig(ctx, repoInfo.Index) - - notaryRepo, err := cli.getNotaryRepository(repoInfo, authConfig, "pull") - if err != nil { - fmt.Fprintf(cli.out, "Error establishing connection to trust repository: %s\n", err) - return nil, err - } - - t, err := notaryRepo.GetTargetByName(ref.Tag(), releasesRole, data.CanonicalTargetsRole) - if err != nil { - return nil, err - } - // Only list tags in the top level targets role or the releases delegation role - ignore - // all other delegation roles - if t.Role != releasesRole && t.Role != data.CanonicalTargetsRole { - return nil, notaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", ref.Tag())) - } - r, err := convertTarget(t.Target) - if err != nil { - return nil, err - - } - - return reference.WithDigest(ref, r.digest) -} - -// TagTrusted tags a trusted ref -func (cli *DockerCli) TagTrusted(ctx context.Context, trustedRef reference.Canonical, ref reference.NamedTagged) error { - fmt.Fprintf(cli.out, "Tagging %s as %s\n", trustedRef.String(), ref.String()) - - return cli.client.ImageTag(ctx, trustedRef.String(), ref.String()) -} - -func notaryError(repoName string, err error) error { - switch err.(type) { - case *json.SyntaxError: - logrus.Debugf("Notary syntax error: %s", err) - return fmt.Errorf("Error: no trust data available for remote repository %s. Try running notary server and setting DOCKER_CONTENT_TRUST_SERVER to its HTTPS address?", repoName) - case signed.ErrExpired: - return fmt.Errorf("Error: remote repository %s out-of-date: %v", repoName, err) - case trustmanager.ErrKeyNotFound: - return fmt.Errorf("Error: signing keys for remote repository %s not found: %v", repoName, err) - case *net.OpError: - return fmt.Errorf("Error: error contacting notary server: %v", err) - case store.ErrMetaNotFound: - return fmt.Errorf("Error: trust data missing for remote repository %s or remote repository not found: %v", repoName, err) - case signed.ErrInvalidKeyType: - return fmt.Errorf("Warning: potential malicious behavior - trust data mismatch for remote repository %s: %v", repoName, err) - case signed.ErrNoKeys: - return fmt.Errorf("Error: could not find signing keys for remote repository %s, or could not decrypt signing key: %v", repoName, err) - case signed.ErrLowVersion: - return fmt.Errorf("Warning: potential malicious behavior - trust data version is lower than expected for remote repository %s: %v", repoName, err) - case signed.ErrRoleThreshold: - return fmt.Errorf("Warning: potential malicious behavior - trust data has insufficient signatures for remote repository %s: %v", repoName, err) - case client.ErrRepositoryNotExist: - return fmt.Errorf("Error: remote trust data does not exist for %s: %v", repoName, err) - case signed.ErrInsufficientSignatures: - return fmt.Errorf("Error: could not produce valid signature for %s. If Yubikey was used, was touch input provided?: %v", repoName, err) - } - - return err -} - -// TrustedPull handles content trust pulling of an image -func (cli *DockerCli) TrustedPull(ctx context.Context, repoInfo *registry.RepositoryInfo, ref registry.Reference, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error { - var refs []target - - notaryRepo, err := cli.getNotaryRepository(repoInfo, authConfig, "pull") - if err != nil { - fmt.Fprintf(cli.out, "Error establishing connection to trust repository: %s\n", err) - return err - } - - if ref.String() == "" { - // List all targets - targets, err := notaryRepo.ListTargets(releasesRole, data.CanonicalTargetsRole) - if err != nil { - return notaryError(repoInfo.FullName(), err) - } - for _, tgt := range targets { - t, err := convertTarget(tgt.Target) - if err != nil { - fmt.Fprintf(cli.out, "Skipping target for %q\n", repoInfo.Name()) - continue - } - // Only list tags in the top level targets role or the releases delegation role - ignore - // all other delegation roles - if tgt.Role != releasesRole && tgt.Role != data.CanonicalTargetsRole { - continue - } - refs = append(refs, t) - } - if len(refs) == 0 { - return notaryError(repoInfo.FullName(), fmt.Errorf("No trusted tags for %s", repoInfo.FullName())) - } - } else { - t, err := notaryRepo.GetTargetByName(ref.String(), releasesRole, data.CanonicalTargetsRole) - if err != nil { - return notaryError(repoInfo.FullName(), err) - } - // Only get the tag if it's in the top level targets role or the releases delegation role - // ignore it if it's in any other delegation roles - if t.Role != releasesRole && t.Role != data.CanonicalTargetsRole { - return notaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", ref.String())) - } - - logrus.Debugf("retrieving target for %s role\n", t.Role) - r, err := convertTarget(t.Target) - if err != nil { - return err - - } - refs = append(refs, r) - } - - for i, r := range refs { - displayTag := r.reference.String() - if displayTag != "" { - displayTag = ":" + displayTag - } - fmt.Fprintf(cli.out, "Pull (%d of %d): %s%s@%s\n", i+1, len(refs), repoInfo.Name(), displayTag, r.digest) - - ref, err := reference.WithDigest(repoInfo, r.digest) - if err != nil { - return err - } - if err := cli.ImagePullPrivileged(ctx, authConfig, ref.String(), requestPrivilege, false); err != nil { - return err - } - - // If reference is not trusted, tag by trusted reference - if !r.reference.HasDigest() { - tagged, err := reference.WithTag(repoInfo, r.reference.String()) - if err != nil { - return err - } - trustedRef, err := reference.WithDigest(repoInfo, r.digest) - if err != nil { - return err - } - if err := cli.TagTrusted(ctx, trustedRef, tagged); err != nil { - return err - } - } - } - return nil -} - -// TrustedPush handles content trust pushing of an image -func (cli *DockerCli) TrustedPush(ctx context.Context, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error { - responseBody, err := cli.ImagePushPrivileged(ctx, authConfig, ref.String(), requestPrivilege) - if err != nil { - return err - } - - defer responseBody.Close() - - // If it is a trusted push we would like to find the target entry which match the - // tag provided in the function and then do an AddTarget later. - target := &client.Target{} - // Count the times of calling for handleTarget, - // if it is called more that once, that should be considered an error in a trusted push. - cnt := 0 - handleTarget := func(aux *json.RawMessage) { - cnt++ - if cnt > 1 { - // handleTarget should only be called one. This will be treated as an error. - return - } - - var pushResult distribution.PushResult - err := json.Unmarshal(*aux, &pushResult) - if err == nil && pushResult.Tag != "" && pushResult.Digest.Validate() == nil { - h, err := hex.DecodeString(pushResult.Digest.Hex()) - if err != nil { - target = nil - return - } - target.Name = registry.ParseReference(pushResult.Tag).String() - target.Hashes = data.Hashes{string(pushResult.Digest.Algorithm()): h} - target.Length = int64(pushResult.Size) - } - } - - var tag string - switch x := ref.(type) { - case reference.Canonical: - return errors.New("cannot push a digest reference") - case reference.NamedTagged: - tag = x.Tag() - } - - // We want trust signatures to always take an explicit tag, - // otherwise it will act as an untrusted push. - if tag == "" { - if err = jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), nil); err != nil { - return err - } - fmt.Fprintln(cli.out, "No tag specified, skipping trust metadata push") - return nil - } - - if err = jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), handleTarget); err != nil { - return err - } - - if cnt > 1 { - return fmt.Errorf("internal error: only one call to handleTarget expected") - } - - if target == nil { - fmt.Fprintln(cli.out, "No targets found, please provide a specific tag in order to sign it") - return nil - } - - fmt.Fprintln(cli.out, "Signing and pushing trust metadata") - - repo, err := cli.getNotaryRepository(repoInfo, authConfig, "push", "pull") - if err != nil { - fmt.Fprintf(cli.out, "Error establishing connection to notary repository: %s\n", err) - return err - } - - // get the latest repository metadata so we can figure out which roles to sign - err = repo.Update(false) - - switch err.(type) { - case client.ErrRepoNotInitialized, client.ErrRepositoryNotExist: - keys := repo.CryptoService.ListKeys(data.CanonicalRootRole) - var rootKeyID string - // always select the first root key - if len(keys) > 0 { - sort.Strings(keys) - rootKeyID = keys[0] - } else { - rootPublicKey, err := repo.CryptoService.Create(data.CanonicalRootRole, "", data.ECDSAKey) - if err != nil { - return err - } - rootKeyID = rootPublicKey.ID() - } - - // Initialize the notary repository with a remotely managed snapshot key - if err := repo.Initialize(rootKeyID, data.CanonicalSnapshotRole); err != nil { - return notaryError(repoInfo.FullName(), err) - } - fmt.Fprintf(cli.out, "Finished initializing %q\n", repoInfo.FullName()) - err = repo.AddTarget(target, data.CanonicalTargetsRole) - case nil: - // already initialized and we have successfully downloaded the latest metadata - err = cli.addTargetToAllSignableRoles(repo, target) - default: - return notaryError(repoInfo.FullName(), err) - } - - if err == nil { - err = repo.Publish() - } - - if err != nil { - fmt.Fprintf(cli.out, "Failed to sign %q:%s - %s\n", repoInfo.FullName(), tag, err.Error()) - return notaryError(repoInfo.FullName(), err) - } - - fmt.Fprintf(cli.out, "Successfully signed %q:%s\n", repoInfo.FullName(), tag) - return nil -} - -// Attempt to add the image target to all the top level delegation roles we can -// (based on whether we have the signing key and whether the role's path allows -// us to). -// If there are no delegation roles, we add to the targets role. -func (cli *DockerCli) addTargetToAllSignableRoles(repo *client.NotaryRepository, target *client.Target) error { - var signableRoles []string - - // translate the full key names, which includes the GUN, into just the key IDs - allCanonicalKeyIDs := make(map[string]struct{}) - for fullKeyID := range repo.CryptoService.ListAllKeys() { - allCanonicalKeyIDs[path.Base(fullKeyID)] = struct{}{} - } - - allDelegationRoles, err := repo.GetDelegationRoles() - if err != nil { - return err - } - - // if there are no delegation roles, then just try to sign it into the targets role - if len(allDelegationRoles) == 0 { - return repo.AddTarget(target, data.CanonicalTargetsRole) - } - - // there are delegation roles, find every delegation role we have a key for, and - // attempt to sign into into all those roles. - for _, delegationRole := range allDelegationRoles { - // We do not support signing any delegation role that isn't a direct child of the targets role. - // Also don't bother checking the keys if we can't add the target - // to this role due to path restrictions - if path.Dir(delegationRole.Name) != data.CanonicalTargetsRole || !delegationRole.CheckPaths(target.Name) { - continue - } - - for _, canonicalKeyID := range delegationRole.KeyIDs { - if _, ok := allCanonicalKeyIDs[canonicalKeyID]; ok { - signableRoles = append(signableRoles, delegationRole.Name) - break - } - } - } - - if len(signableRoles) == 0 { - return fmt.Errorf("no valid signing keys for delegation roles") - } - - return repo.AddTarget(target, signableRoles...) -} - -// ImagePullPrivileged pulls the image and displays it to the output -func (cli *DockerCli) ImagePullPrivileged(ctx context.Context, authConfig types.AuthConfig, ref string, requestPrivilege types.RequestPrivilegeFunc, all bool) error { - - encodedAuth, err := EncodeAuthToBase64(authConfig) - if err != nil { - return err - } - options := types.ImagePullOptions{ - RegistryAuth: encodedAuth, - PrivilegeFunc: requestPrivilege, - All: all, - } - - responseBody, err := cli.client.ImagePull(ctx, ref, options) - if err != nil { - return err - } - defer responseBody.Close() - - return jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), nil) -} - -// ImagePushPrivileged push the image -func (cli *DockerCli) ImagePushPrivileged(ctx context.Context, authConfig types.AuthConfig, ref string, requestPrivilege types.RequestPrivilegeFunc) (io.ReadCloser, error) { - encodedAuth, err := EncodeAuthToBase64(authConfig) - if err != nil { - return nil, err - } - options := types.ImagePushOptions{ - RegistryAuth: encodedAuth, - PrivilegeFunc: requestPrivilege, - } - - return cli.client.ImagePush(ctx, ref, options) -} From e0e6966e2ddcfe1a919dce9563843842524b2539 Mon Sep 17 00:00:00 2001 From: boucher Date: Fri, 9 Sep 2016 12:13:46 -0400 Subject: [PATCH 123/978] Update checkpoint comments to be more accurate Signed-off-by: boucher Upstream-commit: 272868566bb0bfaa5adcdba2b280cc89cb692fcb Component: cli --- components/cli/command/checkpoint/cmd.go | 2 +- components/cli/command/checkpoint/cmd_experimental.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/command/checkpoint/cmd.go b/components/cli/command/checkpoint/cmd.go index cbeb951793..bc8224a2ff 100644 --- a/components/cli/command/checkpoint/cmd.go +++ b/components/cli/command/checkpoint/cmd.go @@ -7,6 +7,6 @@ import ( "github.com/spf13/cobra" ) -// NewCheckpointCommand returns a cobra command for `checkpoint` subcommands +// NewCheckpointCommand appends the `checkpoint` subcommands to rootCmd (only in experimental) func NewCheckpointCommand(rootCmd *cobra.Command, dockerCli *command.DockerCli) { } diff --git a/components/cli/command/checkpoint/cmd_experimental.go b/components/cli/command/checkpoint/cmd_experimental.go index b7e614ca6f..7939678cd5 100644 --- a/components/cli/command/checkpoint/cmd_experimental.go +++ b/components/cli/command/checkpoint/cmd_experimental.go @@ -11,7 +11,7 @@ import ( "github.com/docker/docker/cli/command" ) -// NewCheckpointCommand returns a cobra command for `checkpoint` subcommands +// NewCheckpointCommand appends the `checkpoint` subcommands to rootCmd func NewCheckpointCommand(rootCmd *cobra.Command, dockerCli *command.DockerCli) { cmd := &cobra.Command{ Use: "checkpoint", From d47523d4c59b9bf8c6bea9a464ed9e409dee8cfe Mon Sep 17 00:00:00 2001 From: allencloud Date: Sun, 4 Sep 2016 15:38:50 +0800 Subject: [PATCH 124/978] support docker node ps multiNodes Signed-off-by: allencloud Upstream-commit: 6df46463a9bcb850be5bb5f8ba6546e8a7105f80 Component: cli --- components/cli/command/node/ps.go | 66 +++++++++++++++++++++---------- 1 file changed, 45 insertions(+), 21 deletions(-) diff --git a/components/cli/command/node/ps.go b/components/cli/command/node/ps.go index 84d4b375ac..607488f35e 100644 --- a/components/cli/command/node/ps.go +++ b/components/cli/command/node/ps.go @@ -1,7 +1,11 @@ package node import ( + "fmt" + "strings" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/idresolver" @@ -12,7 +16,7 @@ import ( ) type psOptions struct { - nodeID string + nodeIDs []string noResolve bool noTrunc bool filter opts.FilterOpt @@ -22,14 +26,14 @@ func newPsCommand(dockerCli *command.DockerCli) *cobra.Command { opts := psOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ - Use: "ps [OPTIONS] [NODE]", - Short: "List tasks running on a node, defaults to current node", - Args: cli.RequiresRangeArgs(0, 1), + Use: "ps [OPTIONS] [NODE...]", + Short: "List tasks running on one or more nodes, defaults to current node", + Args: cli.RequiresMinArgs(0), RunE: func(cmd *cobra.Command, args []string) error { - opts.nodeID = "self" + opts.nodeIDs = []string{"self"} if len(args) != 0 { - opts.nodeID = args[0] + opts.nodeIDs = args } return runPs(dockerCli, opts) @@ -47,23 +51,43 @@ func runPs(dockerCli *command.DockerCli, opts psOptions) error { client := dockerCli.Client() ctx := context.Background() - nodeRef, err := Reference(ctx, client, opts.nodeID) - if err != nil { - return nil - } - node, _, err := client.NodeInspectWithRaw(ctx, nodeRef) - if err != nil { - return err + var ( + errs []string + tasks []swarm.Task + ) + + for _, nodeID := range opts.nodeIDs { + nodeRef, err := Reference(ctx, client, nodeID) + if err != nil { + errs = append(errs, err.Error()) + continue + } + + node, _, err := client.NodeInspectWithRaw(ctx, nodeRef) + if err != nil { + errs = append(errs, err.Error()) + continue + } + + filter := opts.filter.Value() + filter.Add("node", node.ID) + + nodeTasks, err := client.TaskList(ctx, types.TaskListOptions{Filter: filter}) + if err != nil { + errs = append(errs, err.Error()) + continue + } + + tasks = append(tasks, nodeTasks...) } - filter := opts.filter.Value() - filter.Add("node", node.ID) - tasks, err := client.TaskList( - ctx, - types.TaskListOptions{Filter: filter}) - if err != nil { - return err + if err := task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), opts.noTrunc); err != nil { + errs = append(errs, err.Error()) } - return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), opts.noTrunc) + if len(errs) > 0 { + return fmt.Errorf("%s", strings.Join(errs, "\n")) + } + + return nil } From 291889bdacfa511f2d416e323fb5c2fcf6f0aadf Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Thu, 8 Sep 2016 20:21:27 -0700 Subject: [PATCH 125/978] tlsconfig: move Clone into proper package Signed-off-by: Stephen J Day Upstream-commit: c6f96cb8b40f2f78fef78b23675623df11f8d9d6 Component: cli --- components/cli/hijack.go | 4 +-- components/cli/transport/tlsconfig_clone.go | 11 ------- .../cli/transport/tlsconfig_clone_go16.go | 31 ----------------- .../cli/transport/tlsconfig_clone_go17.go | 33 ------------------- 4 files changed, 2 insertions(+), 77 deletions(-) delete mode 100644 components/cli/transport/tlsconfig_clone.go delete mode 100644 components/cli/transport/tlsconfig_clone_go16.go delete mode 100644 components/cli/transport/tlsconfig_clone_go17.go diff --git a/components/cli/hijack.go b/components/cli/hijack.go index 9376d21b97..e3f63e20c2 100644 --- a/components/cli/hijack.go +++ b/components/cli/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/components/cli/transport/tlsconfig_clone.go b/components/cli/transport/tlsconfig_clone.go deleted file mode 100644 index 034bc01d33..0000000000 --- a/components/cli/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/components/cli/transport/tlsconfig_clone_go16.go b/components/cli/transport/tlsconfig_clone_go16.go deleted file mode 100644 index 12f13e4694..0000000000 --- a/components/cli/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/components/cli/transport/tlsconfig_clone_go17.go b/components/cli/transport/tlsconfig_clone_go17.go deleted file mode 100644 index 50bf389e43..0000000000 --- a/components/cli/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 b9ca23a5985a089c5f2eb36b06659d89d144aefb Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 9 Sep 2016 10:49:52 -0400 Subject: [PATCH 126/978] Remove RetrieveAuthConfigs Signed-off-by: Daniel Nephin Upstream-commit: ed55f00674b7ccfc6b0b03d60a88ad07f07144bf Component: cli --- components/cli/command/cli.go | 9 +++++ components/cli/command/credentials.go | 7 ---- components/cli/command/image/build.go | 3 +- components/cli/command/registry.go | 58 +++++++++++++-------------- 4 files changed, 40 insertions(+), 37 deletions(-) diff --git a/components/cli/command/cli.go b/components/cli/command/cli.go index 63397bf920..9ca28765cc 100644 --- a/components/cli/command/cli.go +++ b/components/cli/command/cli.go @@ -64,6 +64,15 @@ func (cli *DockerCli) ConfigFile() *configfile.ConfigFile { return cli.configFile } +// CredentialsStore returns a new credentials store based +// on the settings provided in the configuration file. +func (cli *DockerCli) CredentialsStore() credentials.Store { + if cli.configFile.CredentialsStore != "" { + return credentials.NewNativeStore(cli.configFile) + } + return credentials.NewFileStore(cli.configFile) +} + // Initialize the dockerCli runs initialization that must happen after command // line flags are parsed. func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error { diff --git a/components/cli/command/credentials.go b/components/cli/command/credentials.go index 06e9d1de20..e4a4981458 100644 --- a/components/cli/command/credentials.go +++ b/components/cli/command/credentials.go @@ -13,13 +13,6 @@ func GetCredentials(c *configfile.ConfigFile, serverAddress string) (types.AuthC return s.Get(serverAddress) } -// GetAllCredentials loads all credentials from a credentials store. -// The store is determined by the config file settings. -func GetAllCredentials(c *configfile.ConfigFile) (map[string]types.AuthConfig, error) { - s := LoadCredentialsStore(c) - return s.GetAll() -} - // StoreCredentials saves the user credentials in a credentials store. // The store is determined by the config file settings. func StoreCredentials(c *configfile.ConfigFile, auth types.AuthConfig) error { diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go index 06ee32ba83..7f59b54136 100644 --- a/components/cli/command/image/build.go +++ b/components/cli/command/image/build.go @@ -266,6 +266,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { } } + authConfig, _ := dockerCli.CredentialsStore().GetAll() buildOptions := types.ImageBuildOptions{ Memory: memory, MemorySwap: memorySwap, @@ -286,7 +287,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { ShmSize: shmSize, Ulimits: options.ulimits.GetList(), BuildArgs: runconfigopts.ConvertKVStringsToMap(options.buildArgs.GetAll()), - AuthConfigs: dockerCli.RetrieveAuthConfigs(), + AuthConfigs: authConfig, Labels: runconfigopts.ConvertKVStringsToMap(options.labels), } diff --git a/components/cli/command/registry.go b/components/cli/command/registry.go index 4f72afa4a1..65d8f3d8b9 100644 --- a/components/cli/command/registry.go +++ b/components/cli/command/registry.go @@ -20,6 +20,7 @@ import ( ) // ElectAuthServer returns the default registry to use (by asking the daemon) +// TODO: only used in registry package and from ResolveAuthConfig func (cli *DockerCli) ElectAuthServer(ctx context.Context) string { // The daemon `/info` endpoint informs us of the default registry being // used. This is essential in cross-platforms environment, where for @@ -35,6 +36,7 @@ func (cli *DockerCli) ElectAuthServer(ctx context.Context) string { } // EncodeAuthToBase64 serializes the auth configuration as JSON base64 payload +// TODO: move to client/encode ? func EncodeAuthToBase64(authConfig types.AuthConfig) (string, error) { buf, err := json.Marshal(authConfig) if err != nil { @@ -45,6 +47,7 @@ func EncodeAuthToBase64(authConfig types.AuthConfig) (string, error) { // RegistryAuthenticationPrivilegedFunc returns a RequestPrivilegeFunc from the specified registry index info // for the given command. +// TODO: image/plugin func (cli *DockerCli) RegistryAuthenticationPrivilegedFunc(index *registrytypes.IndexInfo, cmdName string) types.RequestPrivilegeFunc { return func() (string, error) { fmt.Fprintf(cli.out, "\nPlease login prior to %s:\n", cmdName) @@ -58,17 +61,10 @@ func (cli *DockerCli) RegistryAuthenticationPrivilegedFunc(index *registrytypes. } } -func (cli *DockerCli) promptWithDefault(prompt string, configDefault string) { - if configDefault == "" { - fmt.Fprintf(cli.out, "%s: ", prompt) - } else { - fmt.Fprintf(cli.out, "%s (%s): ", prompt, configDefault) - } -} - // ResolveAuthConfig is like registry.ResolveAuthConfig, but if using the // default index, it uses the default index name for the daemon's platform, // not the client's platform. +// TODO: plugin/image/container and from RetrieveAuthTokenFromImage func (cli *DockerCli) ResolveAuthConfig(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig { configKey := index.Name if index.Official { @@ -79,13 +75,8 @@ func (cli *DockerCli) ResolveAuthConfig(ctx context.Context, index *registrytype return a } -// RetrieveAuthConfigs return all credentials. -func (cli *DockerCli) RetrieveAuthConfigs() map[string]types.AuthConfig { - acs, _ := GetAllCredentials(cli.configFile) - return acs -} - // ConfigureAuth returns an AuthConfig from the specified user, password and server. +// TODO: only used in registry package func (cli *DockerCli) ConfigureAuth(flUser, flPassword, serverAddress string, isDefaultRegistry bool) (types.AuthConfig, error) { // On Windows, force the use of the regular OS stdin stream. Fixes #14336/#14210 if runtime.GOOS == "windows" { @@ -154,21 +145,26 @@ func (cli *DockerCli) ConfigureAuth(flUser, flPassword, serverAddress string, is return authconfig, nil } -// resolveAuthConfigFromImage retrieves that AuthConfig using the image string -func (cli *DockerCli) resolveAuthConfigFromImage(ctx context.Context, image string) (types.AuthConfig, error) { - registryRef, err := reference.ParseNamed(image) +func readInput(in io.Reader, out io.Writer) string { + reader := bufio.NewReader(in) + line, _, err := reader.ReadLine() if err != nil { - return types.AuthConfig{}, err + fmt.Fprintln(out, err.Error()) + os.Exit(1) } - repoInfo, err := registry.ParseRepositoryInfo(registryRef) - if err != nil { - return types.AuthConfig{}, err + return string(line) +} + +func (cli *DockerCli) promptWithDefault(prompt string, configDefault string) { + if configDefault == "" { + fmt.Fprintf(cli.out, "%s: ", prompt) + } else { + fmt.Fprintf(cli.out, "%s (%s): ", prompt, configDefault) } - authConfig := cli.ResolveAuthConfig(ctx, repoInfo.Index) - return authConfig, nil } // RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete image +// TODO: used in service/stack packages func (cli *DockerCli) RetrieveAuthTokenFromImage(ctx context.Context, image string) (string, error) { // Retrieve encoded auth token from the image reference authConfig, err := cli.resolveAuthConfigFromImage(ctx, image) @@ -182,12 +178,16 @@ func (cli *DockerCli) RetrieveAuthTokenFromImage(ctx context.Context, image stri return encodedAuth, nil } -func readInput(in io.Reader, out io.Writer) string { - reader := bufio.NewReader(in) - line, _, err := reader.ReadLine() +// resolveAuthConfigFromImage retrieves that AuthConfig using the image string +func (cli *DockerCli) resolveAuthConfigFromImage(ctx context.Context, image string) (types.AuthConfig, error) { + registryRef, err := reference.ParseNamed(image) if err != nil { - fmt.Fprintln(out, err.Error()) - os.Exit(1) + return types.AuthConfig{}, err } - return string(line) + repoInfo, err := registry.ParseRepositoryInfo(registryRef) + if err != nil { + return types.AuthConfig{}, err + } + authConfig := cli.ResolveAuthConfig(ctx, repoInfo.Index) + return authConfig, nil } From 5a9c81567274aa685abcfb5e813031fbe6f55468 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 9 Sep 2016 15:24:10 -0400 Subject: [PATCH 127/978] Remove cli/command/credentials Signed-off-by: Daniel Nephin Upstream-commit: 4ae4e66e3cfed8e8dfb11e97c2ae32b914b700c0 Component: cli --- components/cli/command/credentials.go | 37 ----------------------- components/cli/command/registry.go | 5 ++- components/cli/command/registry/login.go | 2 +- components/cli/command/registry/logout.go | 2 +- 4 files changed, 4 insertions(+), 42 deletions(-) delete mode 100644 components/cli/command/credentials.go diff --git a/components/cli/command/credentials.go b/components/cli/command/credentials.go deleted file mode 100644 index e4a4981458..0000000000 --- a/components/cli/command/credentials.go +++ /dev/null @@ -1,37 +0,0 @@ -package command - -import ( - "github.com/docker/docker/api/types" - "github.com/docker/docker/cliconfig/configfile" - "github.com/docker/docker/cliconfig/credentials" -) - -// GetCredentials loads the user credentials from a credentials store. -// The store is determined by the config file settings. -func GetCredentials(c *configfile.ConfigFile, serverAddress string) (types.AuthConfig, error) { - s := LoadCredentialsStore(c) - return s.Get(serverAddress) -} - -// StoreCredentials saves the user credentials in a credentials store. -// The store is determined by the config file settings. -func StoreCredentials(c *configfile.ConfigFile, auth types.AuthConfig) error { - s := LoadCredentialsStore(c) - return s.Store(auth) -} - -// EraseCredentials removes the user credentials from a credentials store. -// The store is determined by the config file settings. -func EraseCredentials(c *configfile.ConfigFile, serverAddress string) error { - s := LoadCredentialsStore(c) - return s.Erase(serverAddress) -} - -// LoadCredentialsStore initializes a new credentials store based -// in the settings provided in the configuration file. -func LoadCredentialsStore(c *configfile.ConfigFile) credentials.Store { - if c.CredentialsStore != "" { - return credentials.NewNativeStore(c) - } - return credentials.NewFileStore(c) -} diff --git a/components/cli/command/registry.go b/components/cli/command/registry.go index 65d8f3d8b9..3c78d47d8c 100644 --- a/components/cli/command/registry.go +++ b/components/cli/command/registry.go @@ -36,7 +36,6 @@ func (cli *DockerCli) ElectAuthServer(ctx context.Context) string { } // EncodeAuthToBase64 serializes the auth configuration as JSON base64 payload -// TODO: move to client/encode ? func EncodeAuthToBase64(authConfig types.AuthConfig) (string, error) { buf, err := json.Marshal(authConfig) if err != nil { @@ -71,7 +70,7 @@ func (cli *DockerCli) ResolveAuthConfig(ctx context.Context, index *registrytype configKey = cli.ElectAuthServer(ctx) } - a, _ := GetCredentials(cli.configFile, configKey) + a, _ := cli.CredentialsStore().Get(configKey) return a } @@ -87,7 +86,7 @@ func (cli *DockerCli) ConfigureAuth(flUser, flPassword, serverAddress string, is serverAddress = registry.ConvertToHostname(serverAddress) } - authconfig, err := GetCredentials(cli.configFile, serverAddress) + authconfig, err := cli.CredentialsStore().Get(serverAddress) if err != nil { return authconfig, err } diff --git a/components/cli/command/registry/login.go b/components/cli/command/registry/login.go index dccf538474..f97bb557f2 100644 --- a/components/cli/command/registry/login.go +++ b/components/cli/command/registry/login.go @@ -74,7 +74,7 @@ func runLogin(dockerCli *command.DockerCli, opts loginOptions) error { authConfig.Password = "" authConfig.IdentityToken = response.IdentityToken } - if err := command.StoreCredentials(dockerCli.ConfigFile(), authConfig); err != nil { + if err := dockerCli.CredentialsStore().Store(authConfig); err != nil { return fmt.Errorf("Error saving credentials: %v", err) } diff --git a/components/cli/command/registry/logout.go b/components/cli/command/registry/logout.go index 1e0c5170a6..3fc59dea7c 100644 --- a/components/cli/command/registry/logout.go +++ b/components/cli/command/registry/logout.go @@ -68,7 +68,7 @@ func runLogout(dockerCli *command.DockerCli, serverAddress string) error { fmt.Fprintf(dockerCli.Out(), "Removing login credentials for %s\n", hostnameAddress) for _, r := range regsToLogout { - if err := command.EraseCredentials(dockerCli.ConfigFile(), r); err != nil { + if err := dockerCli.CredentialsStore().Erase(r); err != nil { fmt.Fprintf(dockerCli.Err(), "WARNING: could not erase credentials: %v\n", err) } } From 3c8785422e0c2a69c3e4a48050df0b0d5fe415d5 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 9 Sep 2016 15:38:00 -0400 Subject: [PATCH 128/978] Remove remaining registry methods from DockerCLI. Signed-off-by: Daniel Nephin Upstream-commit: a26ba0e7023040f6e94dc34d68e0280cc4669561 Component: cli --- components/cli/command/container/create.go | 2 +- components/cli/command/image/pull.go | 4 +- components/cli/command/image/push.go | 4 +- components/cli/command/image/search.go | 4 +- components/cli/command/image/trust.go | 2 +- components/cli/command/plugin/install.go | 4 +- components/cli/command/plugin/push.go | 2 +- components/cli/command/registry.go | 52 ++++++++++------------ components/cli/command/registry/login.go | 4 +- components/cli/command/registry/logout.go | 2 +- components/cli/command/service/create.go | 2 +- components/cli/command/service/update.go | 2 +- components/cli/command/stack/deploy.go | 2 +- 13 files changed, 40 insertions(+), 46 deletions(-) diff --git a/components/cli/command/container/create.go b/components/cli/command/container/create.go index b80b6e1e5a..7bd3856971 100644 --- a/components/cli/command/container/create.go +++ b/components/cli/command/container/create.go @@ -85,7 +85,7 @@ func pullImage(ctx context.Context, dockerCli *command.DockerCli, image string, return err } - authConfig := dockerCli.ResolveAuthConfig(ctx, repoInfo.Index) + authConfig := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index) encodedAuth, err := command.EncodeAuthToBase64(authConfig) if err != nil { return err diff --git a/components/cli/command/image/pull.go b/components/cli/command/image/pull.go index 3f3093a5d8..9116d45840 100644 --- a/components/cli/command/image/pull.go +++ b/components/cli/command/image/pull.go @@ -73,8 +73,8 @@ func runPull(dockerCli *command.DockerCli, opts pullOptions) error { ctx := context.Background() - authConfig := dockerCli.ResolveAuthConfig(ctx, repoInfo.Index) - requestPrivilege := dockerCli.RegistryAuthenticationPrivilegedFunc(repoInfo.Index, "pull") + authConfig := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index) + requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, "pull") if command.IsTrusted() && !registryRef.HasDigest() { // Check if tag is digest diff --git a/components/cli/command/image/push.go b/components/cli/command/image/push.go index a98de9e707..a8ce4945ec 100644 --- a/components/cli/command/image/push.go +++ b/components/cli/command/image/push.go @@ -44,8 +44,8 @@ func runPush(dockerCli *command.DockerCli, remote string) error { ctx := context.Background() // Resolve the Auth config relevant for this server - authConfig := dockerCli.ResolveAuthConfig(ctx, repoInfo.Index) - requestPrivilege := dockerCli.RegistryAuthenticationPrivilegedFunc(repoInfo.Index, "push") + authConfig := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index) + requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, "push") if command.IsTrusted() { return trustedPush(ctx, dockerCli, repoInfo, ref, authConfig, requestPrivilege) diff --git a/components/cli/command/image/search.go b/components/cli/command/image/search.go index 7c4ad03b96..6f8308af80 100644 --- a/components/cli/command/image/search.go +++ b/components/cli/command/image/search.go @@ -66,8 +66,8 @@ func runSearch(dockerCli *command.DockerCli, opts searchOptions) error { ctx := context.Background() - authConfig := dockerCli.ResolveAuthConfig(ctx, indexInfo) - requestPrivilege := dockerCli.RegistryAuthenticationPrivilegedFunc(indexInfo, "search") + authConfig := command.ResolveAuthConfig(ctx, dockerCli, indexInfo) + requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(dockerCli, indexInfo, "search") encodedAuth, err := command.EncodeAuthToBase64(authConfig) if err != nil { diff --git a/components/cli/command/image/trust.go b/components/cli/command/image/trust.go index f0948cc808..b08bd490cb 100644 --- a/components/cli/command/image/trust.go +++ b/components/cli/command/image/trust.go @@ -499,7 +499,7 @@ func TrustedReference(ctx context.Context, cli *command.DockerCli, ref reference } // Resolve the Auth config relevant for this server - authConfig := cli.ResolveAuthConfig(ctx, repoInfo.Index) + authConfig := command.ResolveAuthConfig(ctx, cli, repoInfo.Index) notaryRepo, err := GetNotaryRepository(cli, repoInfo, authConfig, "pull") if err != nil { diff --git a/components/cli/command/plugin/install.go b/components/cli/command/plugin/install.go index 2867247a84..e90e8d1224 100644 --- a/components/cli/command/plugin/install.go +++ b/components/cli/command/plugin/install.go @@ -61,14 +61,14 @@ func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error { return err } - authConfig := dockerCli.ResolveAuthConfig(ctx, repoInfo.Index) + authConfig := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index) encodedAuth, err := command.EncodeAuthToBase64(authConfig) if err != nil { return err } - registryAuthFunc := dockerCli.RegistryAuthenticationPrivilegedFunc(repoInfo.Index, "plugin install") + registryAuthFunc := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, "plugin install") options := types.PluginInstallOptions{ RegistryAuth: encodedAuth, diff --git a/components/cli/command/plugin/push.go b/components/cli/command/plugin/push.go index 5174828ea8..360830902e 100644 --- a/components/cli/command/plugin/push.go +++ b/components/cli/command/plugin/push.go @@ -45,7 +45,7 @@ func runPush(dockerCli *command.DockerCli, name string) error { if err != nil { return err } - authConfig := dockerCli.ResolveAuthConfig(ctx, repoInfo.Index) + authConfig := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index) encodedAuth, err := command.EncodeAuthToBase64(authConfig) if err != nil { diff --git a/components/cli/command/registry.go b/components/cli/command/registry.go index 3c78d47d8c..b70d6f444c 100644 --- a/components/cli/command/registry.go +++ b/components/cli/command/registry.go @@ -20,15 +20,14 @@ import ( ) // ElectAuthServer returns the default registry to use (by asking the daemon) -// TODO: only used in registry package and from ResolveAuthConfig -func (cli *DockerCli) ElectAuthServer(ctx context.Context) string { +func ElectAuthServer(ctx context.Context, cli *DockerCli) string { // The daemon `/info` endpoint informs us of the default registry being // used. This is essential in cross-platforms environment, where for // example a Linux client might be interacting with a Windows daemon, hence // the default registry URL might be Windows specific. serverAddress := registry.IndexServer - if info, err := cli.client.Info(ctx); err != nil { - fmt.Fprintf(cli.out, "Warning: failed to get default registry endpoint from daemon (%v). Using system default: %s\n", err, serverAddress) + if info, err := cli.Client().Info(ctx); err != nil { + fmt.Fprintf(cli.Out(), "Warning: failed to get default registry endpoint from daemon (%v). Using system default: %s\n", err, serverAddress) } else { serverAddress = info.IndexServerAddress } @@ -46,13 +45,12 @@ func EncodeAuthToBase64(authConfig types.AuthConfig) (string, error) { // RegistryAuthenticationPrivilegedFunc returns a RequestPrivilegeFunc from the specified registry index info // for the given command. -// TODO: image/plugin -func (cli *DockerCli) RegistryAuthenticationPrivilegedFunc(index *registrytypes.IndexInfo, cmdName string) types.RequestPrivilegeFunc { +func RegistryAuthenticationPrivilegedFunc(cli *DockerCli, index *registrytypes.IndexInfo, cmdName string) types.RequestPrivilegeFunc { return func() (string, error) { - fmt.Fprintf(cli.out, "\nPlease login prior to %s:\n", cmdName) + fmt.Fprintf(cli.Out(), "\nPlease login prior to %s:\n", cmdName) indexServer := registry.GetAuthConfigKey(index) - isDefaultRegistry := indexServer == cli.ElectAuthServer(context.Background()) - authConfig, err := cli.ConfigureAuth("", "", indexServer, isDefaultRegistry) + isDefaultRegistry := indexServer == ElectAuthServer(context.Background(), cli) + authConfig, err := ConfigureAuth(cli, "", "", indexServer, isDefaultRegistry) if err != nil { return "", err } @@ -63,11 +61,10 @@ func (cli *DockerCli) RegistryAuthenticationPrivilegedFunc(index *registrytypes. // ResolveAuthConfig is like registry.ResolveAuthConfig, but if using the // default index, it uses the default index name for the daemon's platform, // not the client's platform. -// TODO: plugin/image/container and from RetrieveAuthTokenFromImage -func (cli *DockerCli) ResolveAuthConfig(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig { +func ResolveAuthConfig(ctx context.Context, cli *DockerCli, index *registrytypes.IndexInfo) types.AuthConfig { configKey := index.Name if index.Official { - configKey = cli.ElectAuthServer(ctx) + configKey = ElectAuthServer(ctx, cli) } a, _ := cli.CredentialsStore().Get(configKey) @@ -75,8 +72,7 @@ func (cli *DockerCli) ResolveAuthConfig(ctx context.Context, index *registrytype } // ConfigureAuth returns an AuthConfig from the specified user, password and server. -// TODO: only used in registry package -func (cli *DockerCli) ConfigureAuth(flUser, flPassword, serverAddress string, isDefaultRegistry bool) (types.AuthConfig, error) { +func ConfigureAuth(cli *DockerCli, flUser, flPassword, serverAddress string, isDefaultRegistry bool) (types.AuthConfig, error) { // On Windows, force the use of the regular OS stdin stream. Fixes #14336/#14210 if runtime.GOOS == "windows" { cli.in = NewInStream(os.Stdin) @@ -107,10 +103,10 @@ func (cli *DockerCli) ConfigureAuth(flUser, flPassword, serverAddress string, is if flUser = strings.TrimSpace(flUser); flUser == "" { if isDefaultRegistry { // if this is a default registry (docker hub), then display the following message. - fmt.Fprintln(cli.out, "Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.") + fmt.Fprintln(cli.Out(), "Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.") } - cli.promptWithDefault("Username", authconfig.Username) - flUser = readInput(cli.in, cli.out) + promptWithDefault(cli.Out(), "Username", authconfig.Username) + flUser = readInput(cli.In(), cli.Out()) flUser = strings.TrimSpace(flUser) if flUser == "" { flUser = authconfig.Username @@ -124,11 +120,11 @@ func (cli *DockerCli) ConfigureAuth(flUser, flPassword, serverAddress string, is if err != nil { return authconfig, err } - fmt.Fprintf(cli.out, "Password: ") + fmt.Fprintf(cli.Out(), "Password: ") term.DisableEcho(cli.In().FD(), oldState) - flPassword = readInput(cli.in, cli.out) - fmt.Fprint(cli.out, "\n") + flPassword = readInput(cli.In(), cli.Out()) + fmt.Fprint(cli.Out(), "\n") term.RestoreTerminal(cli.In().FD(), oldState) if flPassword == "" { @@ -154,19 +150,18 @@ func readInput(in io.Reader, out io.Writer) string { return string(line) } -func (cli *DockerCli) promptWithDefault(prompt string, configDefault string) { +func promptWithDefault(out io.Writer, prompt string, configDefault string) { if configDefault == "" { - fmt.Fprintf(cli.out, "%s: ", prompt) + fmt.Fprintf(out, "%s: ", prompt) } else { - fmt.Fprintf(cli.out, "%s (%s): ", prompt, configDefault) + fmt.Fprintf(out, "%s (%s): ", prompt, configDefault) } } // RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete image -// TODO: used in service/stack packages -func (cli *DockerCli) RetrieveAuthTokenFromImage(ctx context.Context, image string) (string, error) { +func RetrieveAuthTokenFromImage(ctx context.Context, cli *DockerCli, image string) (string, error) { // Retrieve encoded auth token from the image reference - authConfig, err := cli.resolveAuthConfigFromImage(ctx, image) + authConfig, err := resolveAuthConfigFromImage(ctx, cli, image) if err != nil { return "", err } @@ -178,7 +173,7 @@ func (cli *DockerCli) RetrieveAuthTokenFromImage(ctx context.Context, image stri } // resolveAuthConfigFromImage retrieves that AuthConfig using the image string -func (cli *DockerCli) resolveAuthConfigFromImage(ctx context.Context, image string) (types.AuthConfig, error) { +func resolveAuthConfigFromImage(ctx context.Context, cli *DockerCli, image string) (types.AuthConfig, error) { registryRef, err := reference.ParseNamed(image) if err != nil { return types.AuthConfig{}, err @@ -187,6 +182,5 @@ func (cli *DockerCli) resolveAuthConfigFromImage(ctx context.Context, image stri if err != nil { return types.AuthConfig{}, err } - authConfig := cli.ResolveAuthConfig(ctx, repoInfo.Index) - return authConfig, nil + return ResolveAuthConfig(ctx, cli, repoInfo.Index), nil } diff --git a/components/cli/command/registry/login.go b/components/cli/command/registry/login.go index f97bb557f2..d6f7f8f1d1 100644 --- a/components/cli/command/registry/login.go +++ b/components/cli/command/registry/login.go @@ -52,7 +52,7 @@ func runLogin(dockerCli *command.DockerCli, opts loginOptions) error { var ( serverAddress string - authServer = dockerCli.ElectAuthServer(ctx) + authServer = command.ElectAuthServer(ctx, dockerCli) ) if opts.serverAddress != "" { serverAddress = opts.serverAddress @@ -62,7 +62,7 @@ func runLogin(dockerCli *command.DockerCli, opts loginOptions) error { isDefaultRegistry := serverAddress == authServer - authConfig, err := dockerCli.ConfigureAuth(opts.user, opts.password, serverAddress, isDefaultRegistry) + authConfig, err := command.ConfigureAuth(dockerCli, opts.user, opts.password, serverAddress, isDefaultRegistry) if err != nil { return err } diff --git a/components/cli/command/registry/logout.go b/components/cli/command/registry/logout.go index 3fc59dea7c..5d80595ff0 100644 --- a/components/cli/command/registry/logout.go +++ b/components/cli/command/registry/logout.go @@ -35,7 +35,7 @@ func runLogout(dockerCli *command.DockerCli, serverAddress string) error { var isDefaultRegistry bool if serverAddress == "" { - serverAddress = dockerCli.ElectAuthServer(ctx) + serverAddress = command.ElectAuthServer(ctx, dockerCli) isDefaultRegistry = true } diff --git a/components/cli/command/service/create.go b/components/cli/command/service/create.go index 4ec8835b37..bc5576b1ad 100644 --- a/components/cli/command/service/create.go +++ b/components/cli/command/service/create.go @@ -55,7 +55,7 @@ func runCreate(dockerCli *command.DockerCli, opts *serviceOptions) error { // only send auth if flag was set if opts.registryAuth { // Retrieve encoded auth token from the image reference - encodedAuth, err := dockerCli.RetrieveAuthTokenFromImage(ctx, opts.image) + encodedAuth, err := command.RetrieveAuthTokenFromImage(ctx, dockerCli, opts.image) if err != nil { return err } diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index a86f20e585..be3218ed60 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -82,7 +82,7 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID str // Retrieve encoded auth token from the image reference // This would be the old image if it didn't change in this update image := service.Spec.TaskTemplate.ContainerSpec.Image - encodedAuth, err := dockerCli.RetrieveAuthTokenFromImage(ctx, image) + encodedAuth, err := command.RetrieveAuthTokenFromImage(ctx, dockerCli, image) if err != nil { return err } diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index d72c2bd08f..6daf9500f0 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -197,7 +197,7 @@ func deployServices( if sendAuth { // Retrieve encoded auth token from the image reference image := serviceSpec.TaskTemplate.ContainerSpec.Image - encodedAuth, err = dockerCli.RetrieveAuthTokenFromImage(ctx, image) + encodedAuth, err = command.RetrieveAuthTokenFromImage(ctx, dockerCli, image) if err != nil { return err } From b87d5047a3b5907eeaaac24ff1b11c50fb058ac9 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 12 Sep 2016 11:41:11 +0200 Subject: [PATCH 129/978] =?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 Upstream-commit: 62e14c713b444f2566b1dffc79f68718608011ff Component: cli --- components/cli/README.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 components/cli/README.md diff --git a/components/cli/README.md b/components/cli/README.md new file mode 100644 index 0000000000..7872d94a53 --- /dev/null +++ b/components/cli/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 4f7aafa4084988ef74da1f0e89d3ae033836706c Mon Sep 17 00:00:00 2001 From: sakeven Date: Mon, 29 Aug 2016 21:10:32 +0800 Subject: [PATCH 130/978] validate build-arg Signed-off-by: sakeven Upstream-commit: f39b39cccbb9b883567eda1c190b711703dd50b4 Component: cli --- components/cli/command/image/build.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go index 06ee32ba83..9ea7c23e48 100644 --- a/components/cli/command/image/build.go +++ b/components/cli/command/image/build.go @@ -62,7 +62,7 @@ func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command { ulimits := make(map[string]*units.Ulimit) options := buildOptions{ tags: opts.NewListOpts(validateTag), - buildArgs: opts.NewListOpts(runconfigopts.ValidateEnv), + buildArgs: opts.NewListOpts(runconfigopts.ValidateArg), ulimits: runconfigopts.NewUlimitOpt(&ulimits), } From 45c490c19f2dbe3e844160e70524a5c7f0added0 Mon Sep 17 00:00:00 2001 From: Zhang Wei Date: Tue, 19 Jul 2016 00:02:41 +0800 Subject: [PATCH 131/978] Enhancement: allow parallel stop Stop multiple containers in parallel to speed up stop process, allow maximum 50 parallel stops. Signed-off-by: Abhinav Dahiya Signed-off-by: Zhang Wei Upstream-commit: 285fef282f3e27295a1a1abadaf1674fea46766a Component: cli --- components/cli/command/container/stop.go | 8 ++++-- components/cli/command/container/utils.go | 32 +++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/components/cli/command/container/stop.go b/components/cli/command/container/stop.go index dddb7efa22..2f22fd09a4 100644 --- a/components/cli/command/container/stop.go +++ b/components/cli/command/container/stop.go @@ -39,11 +39,15 @@ func NewStopCommand(dockerCli *command.DockerCli) *cobra.Command { func runStop(dockerCli *command.DockerCli, opts *stopOptions) error { ctx := context.Background() + timeout := time.Duration(opts.time) * time.Second var errs []string + + errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, id string) error { + return dockerCli.Client().ContainerStop(ctx, id, &timeout) + }) for _, container := range opts.containers { - timeout := time.Duration(opts.time) * time.Second - if err := dockerCli.Client().ContainerStop(ctx, container, &timeout); err != nil { + if err := <-errChan; err != nil { errs = append(errs, err.Error()) } else { fmt.Fprintf(dockerCli.Out(), "%s\n", container) diff --git a/components/cli/command/container/utils.go b/components/cli/command/container/utils.go index 8c993dcce5..7e895834f9 100644 --- a/components/cli/command/container/utils.go +++ b/components/cli/command/container/utils.go @@ -90,3 +90,35 @@ func getExitCode(dockerCli *command.DockerCli, ctx context.Context, containerID } return c.State.Running, c.State.ExitCode, nil } + +func parallelOperation(ctx context.Context, cids []string, op func(ctx context.Context, id string) error) chan error { + if len(cids) == 0 { + return nil + } + const defaultParallel int = 50 + sem := make(chan struct{}, defaultParallel) + errChan := make(chan error) + + // make sure result is printed in correct order + output := map[string]chan error{} + for _, c := range cids { + output[c] = make(chan error, 1) + } + go func() { + for _, c := range cids { + err := <-output[c] + errChan <- err + } + }() + + go func() { + for _, c := range cids { + sem <- struct{}{} // Wait for active queue sem to drain. + go func(container string) { + output[container] <- op(ctx, container) + <-sem + }(c) + } + }() + return errChan +} From 24c19a8d4735f3b034e241b59e56852f9b33b00b Mon Sep 17 00:00:00 2001 From: Zhang Wei Date: Mon, 12 Sep 2016 17:21:08 +0800 Subject: [PATCH 132/978] Add parallel operation support for pause/unpause Support parallel pause/unpause Signed-off-by: Zhang Wei Upstream-commit: 4570bfe8de34dda519567efcf28cabaf098ba2c8 Component: cli --- components/cli/command/container/pause.go | 3 ++- components/cli/command/container/unpause.go | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/components/cli/command/container/pause.go b/components/cli/command/container/pause.go index 0cc5b351ba..6817cf60eb 100644 --- a/components/cli/command/container/pause.go +++ b/components/cli/command/container/pause.go @@ -34,8 +34,9 @@ func runPause(dockerCli *command.DockerCli, opts *pauseOptions) error { ctx := context.Background() var errs []string + errChan := parallelOperation(ctx, opts.containers, dockerCli.Client().ContainerPause) for _, container := range opts.containers { - if err := dockerCli.Client().ContainerPause(ctx, container); err != nil { + if err := <-errChan; err != nil { errs = append(errs, err.Error()) } else { fmt.Fprintf(dockerCli.Out(), "%s\n", container) diff --git a/components/cli/command/container/unpause.go b/components/cli/command/container/unpause.go index c3635db555..c4d8d4841e 100644 --- a/components/cli/command/container/unpause.go +++ b/components/cli/command/container/unpause.go @@ -35,8 +35,9 @@ func runUnpause(dockerCli *command.DockerCli, opts *unpauseOptions) error { ctx := context.Background() var errs []string + errChan := parallelOperation(ctx, opts.containers, dockerCli.Client().ContainerUnpause) for _, container := range opts.containers { - if err := dockerCli.Client().ContainerUnpause(ctx, container); err != nil { + if err := <-errChan; err != nil { errs = append(errs, err.Error()) } else { fmt.Fprintf(dockerCli.Out(), "%s\n", container) From 7fb726de4d2a5320c1e60ee68b293643e1cf31ee Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Mon, 12 Sep 2016 23:08:19 -0700 Subject: [PATCH 133/978] Fix issue of `WARNING: --size ignored for volume` for `docker inspect` When `docker inspect` is invoked, it is possible to pass a flag of `-s` for container types to display size information. If `-s` is used for non-container types then a warning `WARNING: --size ignored for volume` will show up. However, currently `WARNING: --size ignored for volume` will show up even when `-s` is not passed to `docker inspect` for non-container types. This fix fixes this issue by checking if `-s` has been passed or not (`getSize`). Also, since image inspect does not support `-s`, `IsSizeSupported` has been changed to false for images. This fix is tested manually. Signed-off-by: Yong Tang Upstream-commit: 9aba07679ff36312d1e91c137687f42704ae1a39 Component: cli --- components/cli/command/system/inspect.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/command/system/inspect.go b/components/cli/command/system/inspect.go index e4f67cf643..015c1b5c6d 100644 --- a/components/cli/command/system/inspect.go +++ b/components/cli/command/system/inspect.go @@ -102,7 +102,7 @@ func inspectAll(ctx context.Context, dockerCli *command.DockerCli, getSize bool, ObjectInspector func(string) (interface{}, []byte, error) }{ {"container", true, inspectContainers(ctx, dockerCli, getSize)}, - {"image", true, inspectImages(ctx, dockerCli)}, + {"image", false, inspectImages(ctx, dockerCli)}, {"network", false, inspectNetwork(ctx, dockerCli)}, {"volume", false, inspectVolume(ctx, dockerCli)}, {"service", false, inspectService(ctx, dockerCli)}, @@ -126,7 +126,7 @@ func inspectAll(ctx context.Context, dockerCli *command.DockerCli, getSize bool, } return v, raw, err } - if !inspectData.IsSizeSupported { + if getSize && !inspectData.IsSizeSupported { fmt.Fprintf(dockerCli.Err(), "WARNING: --size ignored for %s\n", inspectData.ObjectType) } return v, raw, err From d9c3f096cfd89249bb4a1e2b4a61fe3eccebf161 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 13 Sep 2016 14:53:11 -0400 Subject: [PATCH 134/978] Use opts.FilterOpt for filter flags. Signed-off-by: Daniel Nephin Upstream-commit: d9cb421d69c79d6e97cfbea72e8e18228a3885e7 Component: cli --- components/cli/command/container/ps.go | 24 ++++--------- components/cli/command/container/ps_test.go | 37 +++++++++------------ components/cli/command/image/images.go | 25 ++++---------- components/cli/command/image/search.go | 19 +++-------- components/cli/command/network/list.go | 22 +++--------- components/cli/command/system/events.go | 22 +++--------- components/cli/command/volume/list.go | 20 +++-------- 7 files changed, 48 insertions(+), 121 deletions(-) diff --git a/components/cli/command/container/ps.go b/components/cli/command/container/ps.go index d7ae675f5b..3583ee1092 100644 --- a/components/cli/command/container/ps.go +++ b/components/cli/command/container/ps.go @@ -1,16 +1,15 @@ package container import ( + "io/ioutil" + "golang.org/x/net/context" "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/formatter" - - "io/ioutil" - + "github.com/docker/docker/opts" "github.com/docker/docker/utils/templates" "github.com/spf13/cobra" ) @@ -23,12 +22,12 @@ type psOptions struct { nLatest bool last int format string - filter []string + filter opts.FilterOpt } // NewPsCommand creates a new cobra.Command for `docker ps` func NewPsCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts psOptions + opts := psOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ Use: "ps [OPTIONS]", @@ -48,7 +47,7 @@ func NewPsCommand(dockerCli *command.DockerCli) *cobra.Command { flags.BoolVarP(&opts.nLatest, "latest", "l", false, "Show the latest created container (includes all states)") flags.IntVarP(&opts.last, "last", "n", -1, "Show n last created containers (includes all states)") flags.StringVarP(&opts.format, "format", "", "", "Pretty-print containers using a Go template") - flags.StringSliceVarP(&opts.filter, "filter", "f", []string{}, "Filter output based on conditions provided") + flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") return cmd } @@ -65,26 +64,17 @@ func (p *preProcessor) Size() bool { } func buildContainerListOptions(opts *psOptions) (*types.ContainerListOptions, error) { - options := &types.ContainerListOptions{ All: opts.all, Limit: opts.last, Size: opts.size, - Filter: filters.NewArgs(), + Filter: opts.filter.Value(), } if opts.nLatest && opts.last == -1 { options.Limit = 1 } - for _, f := range opts.filter { - var err error - options.Filter, err = filters.ParseFlag(f, options.Filter) - if err != nil { - return nil, err - } - } - // Currently only used with Size, so we can determine if the user // put {{.Size}} in their format. pre := &preProcessor{opts: options} diff --git a/components/cli/command/container/ps_test.go b/components/cli/command/container/ps_test.go index 2af183cce1..dafdcdf905 100644 --- a/components/cli/command/container/ps_test.go +++ b/components/cli/command/container/ps_test.go @@ -1,8 +1,16 @@ package container -import "testing" +import ( + "testing" + + "github.com/docker/docker/opts" + "github.com/docker/docker/pkg/testutil/assert" +) func TestBuildContainerListOptions(t *testing.T) { + filters := opts.NewFilterOpt() + assert.NilError(t, filters.Set("foo=bar")) + assert.NilError(t, filters.Set("baz=foo")) contexts := []struct { psOpts *psOptions @@ -16,7 +24,7 @@ func TestBuildContainerListOptions(t *testing.T) { all: true, size: true, last: 5, - filter: []string{"foo=bar", "baz=foo"}, + filter: filters, }, expectedAll: true, expectedSize: true, @@ -42,27 +50,12 @@ func TestBuildContainerListOptions(t *testing.T) { for _, c := range contexts { options, err := buildContainerListOptions(c.psOpts) - if err != nil { - t.Fatal(err) - } + assert.NilError(t, err) - if c.expectedAll != options.All { - t.Fatalf("Expected All to be %t but got %t", c.expectedAll, options.All) - } - - if c.expectedSize != options.Size { - t.Fatalf("Expected Size to be %t but got %t", c.expectedSize, options.Size) - } - - if c.expectedLimit != options.Limit { - t.Fatalf("Expected Limit to be %d but got %d", c.expectedLimit, options.Limit) - } - - f := options.Filter - - if f.Len() != len(c.expectedFilters) { - t.Fatalf("Expected %d filters but got %d", len(c.expectedFilters), f.Len()) - } + assert.Equal(t, c.expectedAll, options.All) + assert.Equal(t, c.expectedSize, options.Size) + assert.Equal(t, c.expectedLimit, options.Limit) + assert.Equal(t, options.Filter.Len(), len(c.expectedFilters)) for k, v := range c.expectedFilters { f := options.Filter diff --git a/components/cli/command/image/images.go b/components/cli/command/image/images.go index f00fecf672..648236dfe5 100644 --- a/components/cli/command/image/images.go +++ b/components/cli/command/image/images.go @@ -4,10 +4,10 @@ import ( "golang.org/x/net/context" "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/formatter" + "github.com/docker/docker/opts" "github.com/spf13/cobra" ) @@ -19,12 +19,12 @@ type imagesOptions struct { noTrunc bool showDigests bool format string - filter []string + filter opts.FilterOpt } // NewImagesCommand creates a new `docker images` command func NewImagesCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts imagesOptions + opts := imagesOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ Use: "images [OPTIONS] [REPOSITORY[:TAG]]", @@ -45,7 +45,7 @@ func NewImagesCommand(dockerCli *command.DockerCli) *cobra.Command { flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output") flags.BoolVar(&opts.showDigests, "digests", false, "Show digests") flags.StringVar(&opts.format, "format", "", "Pretty-print images using a Go template") - flags.StringSliceVarP(&opts.filter, "filter", "f", []string{}, "Filter output based on conditions provided") + flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") return cmd } @@ -53,23 +53,10 @@ func NewImagesCommand(dockerCli *command.DockerCli) *cobra.Command { func runImages(dockerCli *command.DockerCli, opts imagesOptions) error { ctx := context.Background() - // Consolidate all filter flags, and sanity check them early. - // They'll get process in the daemon/server. - imageFilterArgs := filters.NewArgs() - for _, f := range opts.filter { - var err error - imageFilterArgs, err = filters.ParseFlag(f, imageFilterArgs) - if err != nil { - return err - } - } - - matchName := opts.matchName - options := types.ImageListOptions{ - MatchName: matchName, + MatchName: opts.matchName, All: opts.all, - Filters: imageFilterArgs, + Filters: opts.filter.Value(), } images, err := dockerCli.Client().ImageList(ctx, options) diff --git a/components/cli/command/image/search.go b/components/cli/command/image/search.go index 6f8308af80..93db7006ac 100644 --- a/components/cli/command/image/search.go +++ b/components/cli/command/image/search.go @@ -9,10 +9,10 @@ import ( "golang.org/x/net/context" "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" registrytypes "github.com/docker/docker/api/types/registry" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/opts" "github.com/docker/docker/pkg/stringutils" "github.com/docker/docker/registry" "github.com/spf13/cobra" @@ -22,7 +22,7 @@ type searchOptions struct { term string noTrunc bool limit int - filter []string + filter opts.FilterOpt // Deprecated stars uint @@ -31,7 +31,7 @@ type searchOptions struct { // NewSearchCommand creates a new `docker search` command func NewSearchCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts searchOptions + opts := searchOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ Use: "search [OPTIONS] TERM", @@ -46,7 +46,7 @@ func NewSearchCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output") - flags.StringSliceVarP(&opts.filter, "filter", "f", []string{}, "Filter output based on conditions provided") + flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") flags.IntVar(&opts.limit, "limit", registry.DefaultSearchLimit, "Max number of search results") flags.BoolVar(&opts.automated, "automated", false, "Only show automated builds") @@ -74,19 +74,10 @@ func runSearch(dockerCli *command.DockerCli, opts searchOptions) error { return err } - searchFilters := filters.NewArgs() - for _, f := range opts.filter { - var err error - searchFilters, err = filters.ParseFlag(f, searchFilters) - if err != nil { - return err - } - } - options := types.ImageSearchOptions{ RegistryAuth: encodedAuth, PrivilegeFunc: requestPrivilege, - Filters: searchFilters, + Filters: opts.filter.Value(), Limit: opts.limit, } diff --git a/components/cli/command/network/list.go b/components/cli/command/network/list.go index 19013a3b8e..a0f2e7f4f0 100644 --- a/components/cli/command/network/list.go +++ b/components/cli/command/network/list.go @@ -6,10 +6,10 @@ import ( "golang.org/x/net/context" "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/formatter" + "github.com/docker/docker/opts" "github.com/spf13/cobra" ) @@ -23,11 +23,11 @@ type listOptions struct { quiet bool noTrunc bool format string - filter []string + filter opts.FilterOpt } func newListCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts listOptions + opts := listOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ Use: "ls [OPTIONS]", @@ -43,7 +43,7 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command { flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display network IDs") flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate the output") flags.StringVar(&opts.format, "format", "", "Pretty-print networks using a Go template") - flags.StringSliceVarP(&opts.filter, "filter", "f", []string{}, "Provide filter values (i.e. 'dangling=true')") + flags.VarP(&opts.filter, "filter", "f", "Provide filter values (i.e. 'dangling=true')") return cmd } @@ -51,19 +51,7 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command { func runList(dockerCli *command.DockerCli, opts listOptions) error { client := dockerCli.Client() - netFilterArgs := filters.NewArgs() - for _, f := range opts.filter { - var err error - netFilterArgs, err = filters.ParseFlag(f, netFilterArgs) - if err != nil { - return err - } - } - - options := types.NetworkListOptions{ - Filters: netFilterArgs, - } - + options := types.NetworkListOptions{Filters: opts.filter.Value()} networkResources, err := client.NetworkList(context.Background(), options) if err != nil { return err diff --git a/components/cli/command/system/events.go b/components/cli/command/system/events.go index 456e81b4ce..b9d740f356 100644 --- a/components/cli/command/system/events.go +++ b/components/cli/command/system/events.go @@ -11,9 +11,9 @@ import ( "github.com/docker/docker/api/types" eventtypes "github.com/docker/docker/api/types/events" - "github.com/docker/docker/api/types/filters" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/opts" "github.com/docker/docker/pkg/jsonlog" "github.com/spf13/cobra" ) @@ -21,12 +21,12 @@ import ( type eventsOptions struct { since string until string - filter []string + filter opts.FilterOpt } // NewEventsCommand creates a new cobra.Command for `docker events` func NewEventsCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts eventsOptions + opts := eventsOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ Use: "events [OPTIONS]", @@ -40,28 +40,16 @@ func NewEventsCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() flags.StringVar(&opts.since, "since", "", "Show all events created since timestamp") flags.StringVar(&opts.until, "until", "", "Stream events until this timestamp") - flags.StringSliceVarP(&opts.filter, "filter", "f", []string{}, "Filter output based on conditions provided") + flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") return cmd } func runEvents(dockerCli *command.DockerCli, opts *eventsOptions) error { - eventFilterArgs := filters.NewArgs() - - // Consolidate all filter flags, and sanity check them early. - // They'll get process in the daemon/server. - for _, f := range opts.filter { - var err error - eventFilterArgs, err = filters.ParseFlag(f, eventFilterArgs) - if err != nil { - return err - } - } - options := types.EventsOptions{ Since: opts.since, Until: opts.until, - Filters: eventFilterArgs, + Filters: opts.filter.Value(), } responseBody, err := dockerCli.Client().Events(context.Background(), options) diff --git a/components/cli/command/volume/list.go b/components/cli/command/volume/list.go index 75e77f828f..6d32d2cbfb 100644 --- a/components/cli/command/volume/list.go +++ b/components/cli/command/volume/list.go @@ -6,10 +6,10 @@ import ( "golang.org/x/net/context" "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/formatter" + "github.com/docker/docker/opts" "github.com/spf13/cobra" ) @@ -24,11 +24,11 @@ func (r byVolumeName) Less(i, j int) bool { type listOptions struct { quiet bool format string - filter []string + filter opts.FilterOpt } func newListCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts listOptions + opts := listOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ Use: "ls [OPTIONS]", @@ -44,24 +44,14 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display volume names") flags.StringVar(&opts.format, "format", "", "Pretty-print volumes using a Go template") - flags.StringSliceVarP(&opts.filter, "filter", "f", []string{}, "Provide filter values (e.g. 'dangling=true')") + flags.VarP(&opts.filter, "filter", "f", "Provide filter values (e.g. 'dangling=true')") return cmd } func runList(dockerCli *command.DockerCli, opts listOptions) error { client := dockerCli.Client() - - volFilterArgs := filters.NewArgs() - for _, f := range opts.filter { - var err error - volFilterArgs, err = filters.ParseFlag(f, volFilterArgs) - if err != nil { - return err - } - } - - volumes, err := client.VolumeList(context.Background(), volFilterArgs) + volumes, err := client.VolumeList(context.Background(), opts.filter.Value()) if err != nil { return err } From 91cf73d8193b69087af4abbd30058987cde4e11a Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 12 Sep 2016 16:59:18 -0400 Subject: [PATCH 135/978] Refactor formatter. Signed-off-by: Daniel Nephin Upstream-commit: db0952ad22e9fc8e3241f0a2ed15e4f32cc70e15 Component: cli --- components/cli/command/container/ps.go | 26 +-- components/cli/command/formatter/container.go | 107 ++++------ .../cli/command/formatter/container_test.go | 200 ++++++------------ components/cli/command/formatter/custom.go | 15 +- components/cli/command/formatter/formatter.go | 75 +++++-- components/cli/command/formatter/image.go | 88 ++++---- .../cli/command/formatter/image_test.go | 68 +++--- components/cli/command/formatter/network.go | 84 +++----- .../cli/command/formatter/network_test.go | 81 ++----- components/cli/command/formatter/volume.go | 78 +++---- .../cli/command/formatter/volume_test.go | 81 ++----- components/cli/command/image/images.go | 19 +- components/cli/command/network/list.go | 26 +-- components/cli/command/volume/list.go | 23 +- 14 files changed, 381 insertions(+), 590 deletions(-) diff --git a/components/cli/command/container/ps.go b/components/cli/command/container/ps.go index 3583ee1092..9d015fd707 100644 --- a/components/cli/command/container/ps.go +++ b/components/cli/command/container/ps.go @@ -106,27 +106,19 @@ func runPs(dockerCli *command.DockerCli, opts *psOptions) error { return err } - f := opts.format - if len(f) == 0 { + format := opts.format + if len(format) == 0 { if len(dockerCli.ConfigFile().PsFormat) > 0 && !opts.quiet { - f = dockerCli.ConfigFile().PsFormat + format = dockerCli.ConfigFile().PsFormat } else { - f = "table" + format = "table" } } - psCtx := formatter.ContainerContext{ - Context: formatter.Context{ - Output: dockerCli.Out(), - Format: f, - Quiet: opts.quiet, - Trunc: !opts.noTrunc, - }, - Size: listOptions.Size, - Containers: containers, + containerCtx := formatter.Context{ + Output: dockerCli.Out(), + Format: formatter.NewContainerFormat(format, opts.quiet, opts.size), + Trunc: !opts.noTrunc, } - - psCtx.Write() - - return nil + return formatter.ContainerWrite(containerCtx, containers) } diff --git a/components/cli/command/formatter/container.go b/components/cli/command/formatter/container.go index 6f519e4493..6f3a162fe3 100644 --- a/components/cli/command/formatter/container.go +++ b/components/cli/command/formatter/container.go @@ -1,7 +1,6 @@ package formatter import ( - "bytes" "fmt" "strconv" "strings" @@ -11,7 +10,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/pkg/stringid" "github.com/docker/docker/pkg/stringutils" - "github.com/docker/go-units" + units "github.com/docker/go-units" ) const ( @@ -26,67 +25,53 @@ const ( mountsHeader = "MOUNTS" ) -// ContainerContext contains container specific information required by the formater, encapsulate a Context struct. -type ContainerContext struct { - Context - // Size when set to true will display the size of the output. - Size bool - // Containers - Containers []types.Container +// NewContainerFormat returns a Format for rendering using a Context +func NewContainerFormat(source string, quiet bool, size bool) Format { + switch source { + case TableFormatKey: + if quiet { + return defaultQuietFormat + } + format := defaultContainerTableFormat + if size { + format += `\t{{.Size}}` + } + return Format(format) + case RawFormatKey: + if quiet { + return `container_id: {{.ID}}` + } + format := `container_id: {{.ID}}\nimage: {{.Image}}\ncommand: {{.Command}}\ncreated_at: {{.CreatedAt}}\nstatus: {{.Status}}\nnames: {{.Names}}\nlabels: {{.Labels}}\nports: {{.Ports}}\n` + if size { + format += `size: {{.Size}}\n` + } + return Format(format) + } + return Format(source) } -func (ctx ContainerContext) Write() { - switch ctx.Format { - case tableFormatKey: - if ctx.Quiet { - ctx.Format = defaultQuietFormat - } else { - ctx.Format = defaultContainerTableFormat - if ctx.Size { - ctx.Format += `\t{{.Size}}` - } - } - case rawFormatKey: - if ctx.Quiet { - ctx.Format = `container_id: {{.ID}}` - } else { - ctx.Format = `container_id: {{.ID}}\nimage: {{.Image}}\ncommand: {{.Command}}\ncreated_at: {{.CreatedAt}}\nstatus: {{.Status}}\nnames: {{.Names}}\nlabels: {{.Labels}}\nports: {{.Ports}}\n` - if ctx.Size { - ctx.Format += `size: {{.Size}}\n` +// ContainerWrite renders the context for a list of containers +func ContainerWrite(ctx Context, containers []types.Container) error { + render := func(format func(subContext subContext) error) error { + for _, container := range containers { + err := format(&containerContext{trunc: ctx.Trunc, c: container}) + if err != nil { + return err } } + return nil } - - ctx.buffer = bytes.NewBufferString("") - ctx.preformat() - - tmpl, err := ctx.parseFormat() - if err != nil { - return - } - - for _, container := range ctx.Containers { - containerCtx := &containerContext{ - trunc: ctx.Trunc, - c: container, - } - err = ctx.contextFormat(tmpl, containerCtx) - if err != nil { - return - } - } - - ctx.postformat(tmpl, &containerContext{}) + return ctx.Write(&containerContext{}, render) } type containerContext struct { - baseSubContext + HeaderContext trunc bool c types.Container } func (c *containerContext) ID() string { - c.addHeader(containerIDHeader) + c.AddHeader(containerIDHeader) if c.trunc { return stringid.TruncateID(c.c.ID) } @@ -94,7 +79,7 @@ func (c *containerContext) ID() string { } func (c *containerContext) Names() string { - c.addHeader(namesHeader) + c.AddHeader(namesHeader) names := stripNamePrefix(c.c.Names) if c.trunc { for _, name := range names { @@ -108,7 +93,7 @@ func (c *containerContext) Names() string { } func (c *containerContext) Image() string { - c.addHeader(imageHeader) + c.AddHeader(imageHeader) if c.c.Image == "" { return "" } @@ -121,7 +106,7 @@ func (c *containerContext) Image() string { } func (c *containerContext) Command() string { - c.addHeader(commandHeader) + c.AddHeader(commandHeader) command := c.c.Command if c.trunc { command = stringutils.Ellipsis(command, 20) @@ -130,28 +115,28 @@ func (c *containerContext) Command() string { } func (c *containerContext) CreatedAt() string { - c.addHeader(createdAtHeader) + c.AddHeader(createdAtHeader) return time.Unix(int64(c.c.Created), 0).String() } func (c *containerContext) RunningFor() string { - c.addHeader(runningForHeader) + c.AddHeader(runningForHeader) createdAt := time.Unix(int64(c.c.Created), 0) return units.HumanDuration(time.Now().UTC().Sub(createdAt)) } func (c *containerContext) Ports() string { - c.addHeader(portsHeader) + c.AddHeader(portsHeader) return api.DisplayablePorts(c.c.Ports) } func (c *containerContext) Status() string { - c.addHeader(statusHeader) + c.AddHeader(statusHeader) return c.c.Status } func (c *containerContext) Size() string { - c.addHeader(sizeHeader) + c.AddHeader(sizeHeader) srw := units.HumanSizeWithPrecision(float64(c.c.SizeRw), 3) sv := units.HumanSizeWithPrecision(float64(c.c.SizeRootFs), 3) @@ -163,7 +148,7 @@ func (c *containerContext) Size() string { } func (c *containerContext) Labels() string { - c.addHeader(labelsHeader) + c.AddHeader(labelsHeader) if c.c.Labels == nil { return "" } @@ -180,7 +165,7 @@ func (c *containerContext) Label(name string) string { r := strings.NewReplacer("-", " ", "_", " ") h := r.Replace(n[len(n)-1]) - c.addHeader(h) + c.AddHeader(h) if c.c.Labels == nil { return "" @@ -189,7 +174,7 @@ func (c *containerContext) Label(name string) string { } func (c *containerContext) Mounts() string { - c.addHeader(mountsHeader) + c.AddHeader(mountsHeader) var name string var mounts []string diff --git a/components/cli/command/formatter/container_test.go b/components/cli/command/formatter/container_test.go index 29b8450db9..1ef48ae2d2 100644 --- a/components/cli/command/formatter/container_test.go +++ b/components/cli/command/formatter/container_test.go @@ -95,7 +95,7 @@ func TestContainerPsContext(t *testing.T) { t.Fatalf("Expected %s, was %s\n", c.expValue, v) } - h := ctx.fullHeader() + h := ctx.FullHeader() if h != c.expHeader { t.Fatalf("Expected %s, was %s\n", c.expHeader, h) } @@ -114,7 +114,7 @@ func TestContainerPsContext(t *testing.T) { t.Fatalf("Expected ubuntu, was %s\n", node) } - h := ctx.fullHeader() + h := ctx.FullHeader() if h != "SWARM ID\tNODE NAME" { t.Fatalf("Expected %s, was %s\n", "SWARM ID\tNODE NAME", h) @@ -129,9 +129,9 @@ func TestContainerPsContext(t *testing.T) { } ctx = containerContext{c: c2, trunc: true} - fullHeader := ctx.fullHeader() - if fullHeader != "" { - t.Fatalf("Expected fullHeader to be empty, was %s", fullHeader) + FullHeader := ctx.FullHeader() + if FullHeader != "" { + t.Fatalf("Expected FullHeader to be empty, was %s", FullHeader) } } @@ -140,186 +140,127 @@ func TestContainerContextWrite(t *testing.T) { unixTime := time.Now().AddDate(0, 0, -1).Unix() expectedTime := time.Unix(unixTime, 0).String() - contexts := []struct { - context ContainerContext + cases := []struct { + context Context expected string }{ // Errors { - ContainerContext{ - Context: Context{ - Format: "{{InvalidFunction}}", - }, - }, + Context{Format: "{{InvalidFunction}}"}, `Template parsing error: template: :1: function "InvalidFunction" not defined `, }, { - ContainerContext{ - Context: Context{ - Format: "{{nil}}", - }, - }, + Context{Format: "{{nil}}"}, `Template parsing error: template: :1:2: executing "" at : nil is not a command `, }, // Table Format { - ContainerContext{ - Context: Context{ - Format: "table", - }, - Size: true, - }, + Context{Format: NewContainerFormat("table", false, true)}, `CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES SIZE containerID1 ubuntu "" 24 hours ago foobar_baz 0 B containerID2 ubuntu "" 24 hours ago foobar_bar 0 B `, }, { - ContainerContext{ - Context: Context{ - Format: "table", - }, - }, + Context{Format: NewContainerFormat("table", false, false)}, `CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES containerID1 ubuntu "" 24 hours ago foobar_baz containerID2 ubuntu "" 24 hours ago foobar_bar `, }, { - ContainerContext{ - Context: Context{ - Format: "table {{.Image}}", - }, - }, + Context{Format: NewContainerFormat("table {{.Image}}", false, false)}, "IMAGE\nubuntu\nubuntu\n", }, { - ContainerContext{ - Context: Context{ - Format: "table {{.Image}}", - }, - Size: true, - }, + Context{Format: NewContainerFormat("table {{.Image}}", false, true)}, "IMAGE\nubuntu\nubuntu\n", }, { - ContainerContext{ - Context: Context{ - Format: "table {{.Image}}", - Quiet: true, - }, - }, + Context{Format: NewContainerFormat("table {{.Image}}", true, false)}, "IMAGE\nubuntu\nubuntu\n", }, { - ContainerContext{ - Context: Context{ - Format: "table", - Quiet: true, - }, - }, + Context{Format: NewContainerFormat("table", true, false)}, "containerID1\ncontainerID2\n", }, // Raw Format { - ContainerContext{ - Context: Context{ - Format: "raw", - }, - }, + Context{Format: NewContainerFormat("raw", false, false)}, fmt.Sprintf(`container_id: containerID1 image: ubuntu command: "" created_at: %s -status: +status: names: foobar_baz -labels: -ports: +labels: +ports: container_id: containerID2 image: ubuntu command: "" created_at: %s -status: +status: names: foobar_bar -labels: -ports: +labels: +ports: `, expectedTime, expectedTime), }, { - ContainerContext{ - Context: Context{ - Format: "raw", - }, - Size: true, - }, + Context{Format: NewContainerFormat("raw", false, true)}, fmt.Sprintf(`container_id: containerID1 image: ubuntu command: "" created_at: %s -status: +status: names: foobar_baz -labels: -ports: +labels: +ports: size: 0 B container_id: containerID2 image: ubuntu command: "" created_at: %s -status: +status: names: foobar_bar -labels: -ports: +labels: +ports: size: 0 B `, expectedTime, expectedTime), }, { - ContainerContext{ - Context: Context{ - Format: "raw", - Quiet: true, - }, - }, + Context{Format: NewContainerFormat("raw", true, false)}, "container_id: containerID1\ncontainer_id: containerID2\n", }, // Custom Format { - ContainerContext{ - Context: Context{ - Format: "{{.Image}}", - }, - }, + Context{Format: "{{.Image}}"}, "ubuntu\nubuntu\n", }, { - ContainerContext{ - Context: Context{ - Format: "{{.Image}}", - }, - Size: true, - }, + Context{Format: NewContainerFormat("{{.Image}}", false, true)}, "ubuntu\nubuntu\n", }, } - for _, context := range contexts { + for _, testcase := range cases { containers := []types.Container{ {ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unixTime}, {ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unixTime}, } out := bytes.NewBufferString("") - context.context.Output = out - context.context.Containers = containers - context.context.Write() - actual := out.String() - assert.Equal(t, actual, context.expected) - // Clean buffer - out.Reset() + testcase.context.Output = out + err := ContainerWrite(testcase.context, containers) + if err != nil { + assert.Error(t, err, testcase.expected) + } else { + assert.Equal(t, out.String(), testcase.expected) + } } } @@ -328,75 +269,56 @@ func TestContainerContextWriteWithNoContainers(t *testing.T) { containers := []types.Container{} contexts := []struct { - context ContainerContext + context Context expected string }{ { - ContainerContext{ - Context: Context{ - Format: "{{.Image}}", - Output: out, - }, + Context{ + Format: "{{.Image}}", + Output: out, }, "", }, { - ContainerContext{ - Context: Context{ - Format: "table {{.Image}}", - Output: out, - }, + Context{ + Format: "table {{.Image}}", + Output: out, }, "IMAGE\n", }, { - ContainerContext{ - Context: Context{ - Format: "{{.Image}}", - Output: out, - }, - Size: true, + Context{ + Format: NewContainerFormat("{{.Image}}", false, true), + Output: out, }, "", }, { - ContainerContext{ - Context: Context{ - Format: "table {{.Image}}", - Output: out, - }, - Size: true, + Context{ + Format: NewContainerFormat("table {{.Image}}", false, true), + Output: out, }, "IMAGE\n", }, { - ContainerContext{ - Context: Context{ - Format: "table {{.Image}}\t{{.Size}}", - Output: out, - }, + Context{ + Format: "table {{.Image}}\t{{.Size}}", + Output: out, }, "IMAGE SIZE\n", }, { - ContainerContext{ - Context: Context{ - Format: "table {{.Image}}\t{{.Size}}", - Output: out, - }, - Size: true, + Context{ + Format: NewContainerFormat("table {{.Image}}\t{{.Size}}", false, true), + Output: out, }, "IMAGE SIZE\n", }, } for _, context := range contexts { - context.context.Containers = containers - context.context.Write() - actual := out.String() - if actual != context.expected { - t.Fatalf("Expected \n%s, got \n%s", context.expected, actual) - } + ContainerWrite(context.context, containers) + assert.Equal(t, context.expected, out.String()) // Clean buffer out.Reset() } diff --git a/components/cli/command/formatter/custom.go b/components/cli/command/formatter/custom.go index 2aa2e7b554..df32684429 100644 --- a/components/cli/command/formatter/custom.go +++ b/components/cli/command/formatter/custom.go @@ -5,8 +5,6 @@ import ( ) const ( - tableKey = "table" - imageHeader = "IMAGE" createdSinceHeader = "CREATED" createdAtHeader = "CREATED AT" @@ -18,22 +16,25 @@ const ( ) type subContext interface { - fullHeader() string - addHeader(header string) + FullHeader() string + AddHeader(header string) } -type baseSubContext struct { +// HeaderContext provides the subContext interface for managing headers +type HeaderContext struct { header []string } -func (c *baseSubContext) fullHeader() string { +// FullHeader returns the header as a string +func (c *HeaderContext) FullHeader() string { if c.header == nil { return "" } return strings.Join(c.header, "\t") } -func (c *baseSubContext) addHeader(header string) { +// AddHeader adds another column to the header +func (c *HeaderContext) AddHeader(header string) { if c.header == nil { c.header = []string{} } diff --git a/components/cli/command/formatter/formatter.go b/components/cli/command/formatter/formatter.go index de71c3cdd4..32f9a4d359 100644 --- a/components/cli/command/formatter/formatter.go +++ b/components/cli/command/formatter/formatter.go @@ -12,36 +12,48 @@ import ( ) const ( - tableFormatKey = "table" - rawFormatKey = "raw" + // TableFormatKey is the key used to format as a table + TableFormatKey = "table" + // RawFormatKey is the key used to format as raw JSON + RawFormatKey = "raw" defaultQuietFormat = "{{.ID}}" ) +// Format is the format string rendered using the Context +type Format string + +// IsTable returns true if the format is a table-type format +func (f Format) IsTable() bool { + return strings.HasPrefix(string(f), TableFormatKey) +} + +// Contains returns true if the format contains the substring +func (f Format) Contains(sub string) bool { + return strings.Contains(string(f), sub) +} + // Context contains information required by the formatter to print the output as desired. type Context struct { // Output is the output stream to which the formatted string is written. Output io.Writer // Format is used to choose raw, table or custom format for the output. - Format string - // Quiet when set to true will simply print minimal information. - Quiet bool + Format Format // Trunc when set to true will truncate the output of certain fields such as Container ID. Trunc bool // internal element - table bool finalFormat string header string buffer *bytes.Buffer } -func (c *Context) preformat() { - c.finalFormat = c.Format +func (c *Context) preFormat() { + c.finalFormat = string(c.Format) - if strings.HasPrefix(c.Format, tableKey) { - c.table = true - c.finalFormat = c.finalFormat[len(tableKey):] + // TODO: handle this in the Format type + if c.Format.IsTable() { + c.finalFormat = c.finalFormat[len(TableFormatKey):] } c.finalFormat = strings.Trim(c.finalFormat, " ") @@ -52,18 +64,17 @@ func (c *Context) preformat() { func (c *Context) parseFormat() (*template.Template, error) { tmpl, err := templates.Parse(c.finalFormat) if err != nil { - c.buffer.WriteString(fmt.Sprintf("Template parsing error: %v\n", err)) - c.buffer.WriteTo(c.Output) + return tmpl, fmt.Errorf("Template parsing error: %v\n", err) } return tmpl, err } -func (c *Context) postformat(tmpl *template.Template, subContext subContext) { - if c.table { +func (c *Context) postFormat(tmpl *template.Template, subContext subContext) { + if c.Format.IsTable() { if len(c.header) == 0 { // if we still don't have a header, we didn't have any containers so we need to fake it to get the right headers from the template tmpl.Execute(bytes.NewBufferString(""), subContext) - c.header = subContext.fullHeader() + c.header = subContext.FullHeader() } t := tabwriter.NewWriter(c.Output, 20, 1, 3, ' ', 0) @@ -78,13 +89,35 @@ func (c *Context) postformat(tmpl *template.Template, subContext subContext) { func (c *Context) contextFormat(tmpl *template.Template, subContext subContext) error { if err := tmpl.Execute(c.buffer, subContext); err != nil { - c.buffer = bytes.NewBufferString(fmt.Sprintf("Template parsing error: %v\n", err)) - c.buffer.WriteTo(c.Output) - return err + return fmt.Errorf("Template parsing error: %v\n", err) } - if c.table && len(c.header) == 0 { - c.header = subContext.fullHeader() + if c.Format.IsTable() && len(c.header) == 0 { + c.header = subContext.FullHeader() } c.buffer.WriteString("\n") return nil } + +// SubFormat is a function type accepted by Write() +type SubFormat func(func(subContext) error) error + +// Write the template to the buffer using this Context +func (c *Context) Write(sub subContext, f SubFormat) error { + c.buffer = bytes.NewBufferString("") + c.preFormat() + + tmpl, err := c.parseFormat() + if err != nil { + return err + } + + subFormat := func(subContext subContext) error { + return c.contextFormat(tmpl, subContext) + } + if err := f(subFormat); err != nil { + return err + } + + c.postFormat(tmpl, sub) + return nil +} diff --git a/components/cli/command/formatter/image.go b/components/cli/command/formatter/image.go index 012860e04e..54cb7b62fa 100644 --- a/components/cli/command/formatter/image.go +++ b/components/cli/command/formatter/image.go @@ -1,14 +1,12 @@ package formatter import ( - "bytes" - "strings" "time" "github.com/docker/docker/api/types" "github.com/docker/docker/pkg/stringid" "github.com/docker/docker/reference" - "github.com/docker/go-units" + units "github.com/docker/go-units" ) const ( @@ -25,59 +23,63 @@ const ( type ImageContext struct { Context Digest bool - // Images - Images []types.Image } func isDangling(image types.Image) bool { return len(image.RepoTags) == 1 && image.RepoTags[0] == ":" && len(image.RepoDigests) == 1 && image.RepoDigests[0] == "@" } -func (ctx ImageContext) Write() { - switch ctx.Format { - case tableFormatKey: - ctx.Format = defaultImageTableFormat - if ctx.Digest { - ctx.Format = defaultImageTableFormatWithDigest +// NewImageFormat returns a format for rendering an ImageContext +func NewImageFormat(source string, quiet bool, digest bool) Format { + switch source { + case TableFormatKey: + switch { + case quiet: + return defaultQuietFormat + case digest: + return defaultImageTableFormatWithDigest + default: + return defaultImageTableFormat } - if ctx.Quiet { - ctx.Format = defaultQuietFormat - } - case rawFormatKey: - if ctx.Quiet { - ctx.Format = `image_id: {{.ID}}` - } else { - if ctx.Digest { - ctx.Format = `repository: {{ .Repository }} + case RawFormatKey: + switch { + case quiet: + return `image_id: {{.ID}}` + case digest: + return `repository: {{ .Repository }} tag: {{.Tag}} digest: {{.Digest}} image_id: {{.ID}} created_at: {{.CreatedAt}} virtual_size: {{.Size}} ` - } else { - ctx.Format = `repository: {{ .Repository }} + default: + return `repository: {{ .Repository }} tag: {{.Tag}} image_id: {{.ID}} created_at: {{.CreatedAt}} virtual_size: {{.Size}} ` - } } } - ctx.buffer = bytes.NewBufferString("") - ctx.preformat() - if ctx.table && ctx.Digest && !strings.Contains(ctx.Format, "{{.Digest}}") { - ctx.finalFormat += "\t{{.Digest}}" + format := Format(source) + if format.IsTable() && digest && !format.Contains("{{.Digest}}") { + format += "\t{{.Digest}}" } + return format +} - tmpl, err := ctx.parseFormat() - if err != nil { - return +// ImageWrite writes the formatter images using the ImageContext +func ImageWrite(ctx ImageContext, images []types.Image) error { + render := func(format func(subContext subContext) error) error { + return imageFormat(ctx, images, format) } + return ctx.Write(&imageContext{}, render) +} - for _, image := range ctx.Images { +func imageFormat(ctx ImageContext, images []types.Image, format func(subContext subContext) error) error { + for _, image := range images { images := []*imageContext{} if isDangling(image) { images = append(images, &imageContext{ @@ -170,18 +172,16 @@ virtual_size: {{.Size}} } } for _, imageCtx := range images { - err = ctx.contextFormat(tmpl, imageCtx) - if err != nil { - return + if err := format(imageCtx); err != nil { + return err } } } - - ctx.postformat(tmpl, &imageContext{}) + return nil } type imageContext struct { - baseSubContext + HeaderContext trunc bool i types.Image repo string @@ -190,7 +190,7 @@ type imageContext struct { } func (c *imageContext) ID() string { - c.addHeader(imageIDHeader) + c.AddHeader(imageIDHeader) if c.trunc { return stringid.TruncateID(c.i.ID) } @@ -198,32 +198,32 @@ func (c *imageContext) ID() string { } func (c *imageContext) Repository() string { - c.addHeader(repositoryHeader) + c.AddHeader(repositoryHeader) return c.repo } func (c *imageContext) Tag() string { - c.addHeader(tagHeader) + c.AddHeader(tagHeader) return c.tag } func (c *imageContext) Digest() string { - c.addHeader(digestHeader) + c.AddHeader(digestHeader) return c.digest } func (c *imageContext) CreatedSince() string { - c.addHeader(createdSinceHeader) + c.AddHeader(createdSinceHeader) createdAt := time.Unix(int64(c.i.Created), 0) return units.HumanDuration(time.Now().UTC().Sub(createdAt)) } func (c *imageContext) CreatedAt() string { - c.addHeader(createdAtHeader) + c.AddHeader(createdAtHeader) return time.Unix(int64(c.i.Created), 0).String() } func (c *imageContext) Size() string { - c.addHeader(sizeHeader) + c.AddHeader(sizeHeader) return units.HumanSizeWithPrecision(float64(c.i.Size), 3) } diff --git a/components/cli/command/formatter/image_test.go b/components/cli/command/formatter/image_test.go index 7c87f393fc..6dc7f73db3 100644 --- a/components/cli/command/formatter/image_test.go +++ b/components/cli/command/formatter/image_test.go @@ -9,6 +9,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/testutil/assert" ) func TestImageContext(t *testing.T) { @@ -66,7 +67,7 @@ func TestImageContext(t *testing.T) { t.Fatalf("Expected %s, was %s\n", c.expValue, v) } - h := ctx.fullHeader() + h := ctx.FullHeader() if h != c.expHeader { t.Fatalf("Expected %s, was %s\n", c.expHeader, h) } @@ -77,7 +78,7 @@ func TestImageContextWrite(t *testing.T) { unixTime := time.Now().AddDate(0, 0, -1).Unix() expectedTime := time.Unix(unixTime, 0).String() - contexts := []struct { + cases := []struct { context ImageContext expected string }{ @@ -104,7 +105,7 @@ func TestImageContextWrite(t *testing.T) { { ImageContext{ Context: Context{ - Format: "table", + Format: NewImageFormat("table", false, false), }, }, `REPOSITORY TAG IMAGE ID CREATED SIZE @@ -116,7 +117,7 @@ image tag2 imageID2 24 hours ago { ImageContext{ Context: Context{ - Format: "table {{.Repository}}", + Format: NewImageFormat("table {{.Repository}}", false, false), }, }, "REPOSITORY\nimage\nimage\n\n", @@ -124,7 +125,7 @@ image tag2 imageID2 24 hours ago { ImageContext{ Context: Context{ - Format: "table {{.Repository}}", + Format: NewImageFormat("table {{.Repository}}", false, true), }, Digest: true, }, @@ -137,8 +138,7 @@ image { ImageContext{ Context: Context{ - Format: "table {{.Repository}}", - Quiet: true, + Format: NewImageFormat("table {{.Repository}}", true, false), }, }, "REPOSITORY\nimage\nimage\n\n", @@ -146,8 +146,7 @@ image { ImageContext{ Context: Context{ - Format: "table", - Quiet: true, + Format: NewImageFormat("table", true, false), }, }, "imageID1\nimageID2\nimageID3\n", @@ -155,8 +154,7 @@ image { ImageContext{ Context: Context{ - Format: "table", - Quiet: false, + Format: NewImageFormat("table", false, true), }, Digest: true, }, @@ -169,8 +167,7 @@ image tag2 { ImageContext{ Context: Context{ - Format: "table", - Quiet: true, + Format: NewImageFormat("table", true, true), }, Digest: true, }, @@ -180,7 +177,7 @@ image tag2 { ImageContext{ Context: Context{ - Format: "raw", + Format: NewImageFormat("raw", false, false), }, }, fmt.Sprintf(`repository: image @@ -206,7 +203,7 @@ virtual_size: 0 B { ImageContext{ Context: Context{ - Format: "raw", + Format: NewImageFormat("raw", false, true), }, Digest: true, }, @@ -236,8 +233,7 @@ virtual_size: 0 B { ImageContext{ Context: Context{ - Format: "raw", - Quiet: true, + Format: NewImageFormat("raw", true, false), }, }, `image_id: imageID1 @@ -249,7 +245,7 @@ image_id: imageID3 { ImageContext{ Context: Context{ - Format: "{{.Repository}}", + Format: NewImageFormat("{{.Repository}}", false, false), }, }, "image\nimage\n\n", @@ -257,7 +253,7 @@ image_id: imageID3 { ImageContext{ Context: Context{ - Format: "{{.Repository}}", + Format: NewImageFormat("{{.Repository}}", false, true), }, Digest: true, }, @@ -265,22 +261,20 @@ image_id: imageID3 }, } - for _, context := range contexts { + for _, testcase := range cases { images := []types.Image{ {ID: "imageID1", RepoTags: []string{"image:tag1"}, RepoDigests: []string{"image@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf"}, Created: unixTime}, {ID: "imageID2", RepoTags: []string{"image:tag2"}, Created: unixTime}, {ID: "imageID3", RepoTags: []string{":"}, RepoDigests: []string{"@"}, Created: unixTime}, } out := bytes.NewBufferString("") - context.context.Output = out - context.context.Images = images - context.context.Write() - actual := out.String() - if actual != context.expected { - t.Fatalf("Expected \n%s, got \n%s", context.expected, actual) + testcase.context.Output = out + err := ImageWrite(testcase.context, images) + if err != nil { + assert.Error(t, err, testcase.expected) + } else { + assert.Equal(t, out.String(), testcase.expected) } - // Clean buffer - out.Reset() } } @@ -295,7 +289,7 @@ func TestImageContextWriteWithNoImage(t *testing.T) { { ImageContext{ Context: Context{ - Format: "{{.Repository}}", + Format: NewImageFormat("{{.Repository}}", false, false), Output: out, }, }, @@ -304,7 +298,7 @@ func TestImageContextWriteWithNoImage(t *testing.T) { { ImageContext{ Context: Context{ - Format: "table {{.Repository}}", + Format: NewImageFormat("table {{.Repository}}", false, false), Output: out, }, }, @@ -313,32 +307,26 @@ func TestImageContextWriteWithNoImage(t *testing.T) { { ImageContext{ Context: Context{ - Format: "{{.Repository}}", + Format: NewImageFormat("{{.Repository}}", false, true), Output: out, }, - Digest: true, }, "", }, { ImageContext{ Context: Context{ - Format: "table {{.Repository}}", + Format: NewImageFormat("table {{.Repository}}", false, true), Output: out, }, - Digest: true, }, "REPOSITORY DIGEST\n", }, } for _, context := range contexts { - context.context.Images = images - context.context.Write() - actual := out.String() - if actual != context.expected { - t.Fatalf("Expected \n%s, got \n%s", context.expected, actual) - } + ImageWrite(context.context, images) + assert.Equal(t, out.String(), context.expected) // Clean buffer out.Reset() } diff --git a/components/cli/command/formatter/network.go b/components/cli/command/formatter/network.go index 6eb820879e..d808fdc22d 100644 --- a/components/cli/command/formatter/network.go +++ b/components/cli/command/formatter/network.go @@ -1,7 +1,6 @@ package formatter import ( - "bytes" "fmt" "strings" @@ -17,60 +16,45 @@ const ( internalHeader = "INTERNAL" ) -// NetworkContext contains network specific information required by the formatter, -// encapsulate a Context struct. -type NetworkContext struct { - Context - // Networks - Networks []types.NetworkResource +// NewNetworkFormat returns a Format for rendering using a network Context +func NewNetworkFormat(source string, quiet bool) Format { + switch source { + case TableFormatKey: + if quiet { + return defaultQuietFormat + } + return defaultNetworkTableFormat + case RawFormatKey: + if quiet { + return `network_id: {{.ID}}` + } + return `network_id: {{.ID}}\nname: {{.Name}}\ndriver: {{.Driver}}\nscope: {{.Scope}}\n` + } + return Format(source) } -func (ctx NetworkContext) Write() { - switch ctx.Format { - case tableFormatKey: - if ctx.Quiet { - ctx.Format = defaultQuietFormat - } else { - ctx.Format = defaultNetworkTableFormat - } - case rawFormatKey: - if ctx.Quiet { - ctx.Format = `network_id: {{.ID}}` - } else { - ctx.Format = `network_id: {{.ID}}\nname: {{.Name}}\ndriver: {{.Driver}}\nscope: {{.Scope}}\n` +// NetworkWrite writes the context +func NetworkWrite(ctx Context, networks []types.NetworkResource) error { + render := func(format func(subContext subContext) error) error { + for _, network := range networks { + networkCtx := &networkContext{trunc: ctx.Trunc, n: network} + if err := format(networkCtx); err != nil { + return err + } } + return nil } - - ctx.buffer = bytes.NewBufferString("") - ctx.preformat() - - tmpl, err := ctx.parseFormat() - if err != nil { - return - } - - for _, network := range ctx.Networks { - networkCtx := &networkContext{ - trunc: ctx.Trunc, - n: network, - } - err = ctx.contextFormat(tmpl, networkCtx) - if err != nil { - return - } - } - - ctx.postformat(tmpl, &networkContext{}) + return ctx.Write(&networkContext{}, render) } type networkContext struct { - baseSubContext + HeaderContext trunc bool n types.NetworkResource } func (c *networkContext) ID() string { - c.addHeader(networkIDHeader) + c.AddHeader(networkIDHeader) if c.trunc { return stringid.TruncateID(c.n.ID) } @@ -78,32 +62,32 @@ func (c *networkContext) ID() string { } func (c *networkContext) Name() string { - c.addHeader(nameHeader) + c.AddHeader(nameHeader) return c.n.Name } func (c *networkContext) Driver() string { - c.addHeader(driverHeader) + c.AddHeader(driverHeader) return c.n.Driver } func (c *networkContext) Scope() string { - c.addHeader(scopeHeader) + c.AddHeader(scopeHeader) return c.n.Scope } func (c *networkContext) IPv6() string { - c.addHeader(ipv6Header) + c.AddHeader(ipv6Header) return fmt.Sprintf("%v", c.n.EnableIPv6) } func (c *networkContext) Internal() string { - c.addHeader(internalHeader) + c.AddHeader(internalHeader) return fmt.Sprintf("%v", c.n.Internal) } func (c *networkContext) Labels() string { - c.addHeader(labelsHeader) + c.AddHeader(labelsHeader) if c.n.Labels == nil { return "" } @@ -120,7 +104,7 @@ func (c *networkContext) Label(name string) string { r := strings.NewReplacer("-", " ", "_", " ") h := r.Replace(n[len(n)-1]) - c.addHeader(h) + c.AddHeader(h) if c.n.Labels == nil { return "" diff --git a/components/cli/command/formatter/network_test.go b/components/cli/command/formatter/network_test.go index b5f826af6d..28f078548f 100644 --- a/components/cli/command/formatter/network_test.go +++ b/components/cli/command/formatter/network_test.go @@ -7,6 +7,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/testutil/assert" ) func TestNetworkContext(t *testing.T) { @@ -62,7 +63,7 @@ func TestNetworkContext(t *testing.T) { t.Fatalf("Expected %s, was %s\n", c.expValue, v) } - h := ctx.fullHeader() + h := ctx.FullHeader() if h != c.expHeader { t.Fatalf("Expected %s, was %s\n", c.expHeader, h) } @@ -70,71 +71,45 @@ func TestNetworkContext(t *testing.T) { } func TestNetworkContextWrite(t *testing.T) { - contexts := []struct { - context NetworkContext + cases := []struct { + context Context expected string }{ // Errors { - NetworkContext{ - Context: Context{ - Format: "{{InvalidFunction}}", - }, - }, + Context{Format: "{{InvalidFunction}}"}, `Template parsing error: template: :1: function "InvalidFunction" not defined `, }, { - NetworkContext{ - Context: Context{ - Format: "{{nil}}", - }, - }, + Context{Format: "{{nil}}"}, `Template parsing error: template: :1:2: executing "" at : nil is not a command `, }, // Table format { - NetworkContext{ - Context: Context{ - Format: "table", - }, - }, + Context{Format: NewNetworkFormat("table", false)}, `NETWORK ID NAME DRIVER SCOPE networkID1 foobar_baz foo local networkID2 foobar_bar bar local `, }, { - NetworkContext{ - Context: Context{ - Format: "table", - Quiet: true, - }, - }, + Context{Format: NewNetworkFormat("table", true)}, `networkID1 networkID2 `, }, { - NetworkContext{ - Context: Context{ - Format: "table {{.Name}}", - }, - }, + Context{Format: NewNetworkFormat("table {{.Name}}", false)}, `NAME foobar_baz foobar_bar `, }, { - NetworkContext{ - Context: Context{ - Format: "table {{.Name}}", - Quiet: true, - }, - }, + Context{Format: NewNetworkFormat("table {{.Name}}", true)}, `NAME foobar_baz foobar_bar @@ -142,11 +117,8 @@ foobar_bar }, // Raw Format { - NetworkContext{ - Context: Context{ - Format: "raw", - }, - }, `network_id: networkID1 + Context{Format: NewNetworkFormat("raw", false)}, + `network_id: networkID1 name: foobar_baz driver: foo scope: local @@ -159,43 +131,32 @@ scope: local `, }, { - NetworkContext{ - Context: Context{ - Format: "raw", - Quiet: true, - }, - }, + Context{Format: NewNetworkFormat("raw", true)}, `network_id: networkID1 network_id: networkID2 `, }, // Custom Format { - NetworkContext{ - Context: Context{ - Format: "{{.Name}}", - }, - }, + Context{Format: NewNetworkFormat("{{.Name}}", false)}, `foobar_baz foobar_bar `, }, } - for _, context := range contexts { + for _, testcase := range cases { networks := []types.NetworkResource{ {ID: "networkID1", Name: "foobar_baz", Driver: "foo", Scope: "local"}, {ID: "networkID2", Name: "foobar_bar", Driver: "bar", Scope: "local"}, } out := bytes.NewBufferString("") - context.context.Output = out - context.context.Networks = networks - context.context.Write() - actual := out.String() - if actual != context.expected { - t.Fatalf("Expected \n%s, got \n%s", context.expected, actual) + testcase.context.Output = out + err := NetworkWrite(testcase.context, networks) + if err != nil { + assert.Error(t, err, testcase.expected) + } else { + assert.Equal(t, out.String(), testcase.expected) } - // Clean buffer - out.Reset() } } diff --git a/components/cli/command/formatter/volume.go b/components/cli/command/formatter/volume.go index ba24b06a4f..2fec59d8fb 100644 --- a/components/cli/command/formatter/volume.go +++ b/components/cli/command/formatter/volume.go @@ -1,7 +1,6 @@ package formatter import ( - "bytes" "fmt" "strings" @@ -16,78 +15,63 @@ const ( // Status header ? ) -// VolumeContext contains volume specific information required by the formatter, -// encapsulate a Context struct. -type VolumeContext struct { - Context - // Volumes - Volumes []*types.Volume +// NewVolumeFormat returns a format for use with a volume Context +func NewVolumeFormat(source string, quiet bool) Format { + switch source { + case TableFormatKey: + if quiet { + return defaultVolumeQuietFormat + } + return defaultVolumeTableFormat + case RawFormatKey: + if quiet { + return `name: {{.Name}}` + } + return `name: {{.Name}}\ndriver: {{.Driver}}\n` + } + return Format(source) } -func (ctx VolumeContext) Write() { - switch ctx.Format { - case tableFormatKey: - if ctx.Quiet { - ctx.Format = defaultVolumeQuietFormat - } else { - ctx.Format = defaultVolumeTableFormat - } - case rawFormatKey: - if ctx.Quiet { - ctx.Format = `name: {{.Name}}` - } else { - ctx.Format = `name: {{.Name}}\ndriver: {{.Driver}}\n` +// VolumeWrite writes formatted volumes using the Context +func VolumeWrite(ctx Context, volumes []*types.Volume) error { + render := func(format func(subContext subContext) error) error { + for _, volume := range volumes { + if err := format(&volumeContext{v: volume}); err != nil { + return err + } } + return nil } - - ctx.buffer = bytes.NewBufferString("") - ctx.preformat() - - tmpl, err := ctx.parseFormat() - if err != nil { - return - } - - for _, volume := range ctx.Volumes { - volumeCtx := &volumeContext{ - v: volume, - } - err = ctx.contextFormat(tmpl, volumeCtx) - if err != nil { - return - } - } - - ctx.postformat(tmpl, &networkContext{}) + return ctx.Write(&volumeContext{}, render) } type volumeContext struct { - baseSubContext + HeaderContext v *types.Volume } func (c *volumeContext) Name() string { - c.addHeader(nameHeader) + c.AddHeader(nameHeader) return c.v.Name } func (c *volumeContext) Driver() string { - c.addHeader(driverHeader) + c.AddHeader(driverHeader) return c.v.Driver } func (c *volumeContext) Scope() string { - c.addHeader(scopeHeader) + c.AddHeader(scopeHeader) return c.v.Scope } func (c *volumeContext) Mountpoint() string { - c.addHeader(mountpointHeader) + c.AddHeader(mountpointHeader) return c.v.Mountpoint } func (c *volumeContext) Labels() string { - c.addHeader(labelsHeader) + c.AddHeader(labelsHeader) if c.v.Labels == nil { return "" } @@ -105,7 +89,7 @@ func (c *volumeContext) Label(name string) string { r := strings.NewReplacer("-", " ", "_", " ") h := r.Replace(n[len(n)-1]) - c.addHeader(h) + c.AddHeader(h) if c.v.Labels == nil { return "" diff --git a/components/cli/command/formatter/volume_test.go b/components/cli/command/formatter/volume_test.go index 2295eff3ef..1d5f74e42c 100644 --- a/components/cli/command/formatter/volume_test.go +++ b/components/cli/command/formatter/volume_test.go @@ -7,6 +7,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/testutil/assert" ) func TestVolumeContext(t *testing.T) { @@ -48,7 +49,7 @@ func TestVolumeContext(t *testing.T) { t.Fatalf("Expected %s, was %s\n", c.expValue, v) } - h := ctx.fullHeader() + h := ctx.FullHeader() if h != c.expHeader { t.Fatalf("Expected %s, was %s\n", c.expHeader, h) } @@ -56,71 +57,45 @@ func TestVolumeContext(t *testing.T) { } func TestVolumeContextWrite(t *testing.T) { - contexts := []struct { - context VolumeContext + cases := []struct { + context Context expected string }{ // Errors { - VolumeContext{ - Context: Context{ - Format: "{{InvalidFunction}}", - }, - }, + Context{Format: "{{InvalidFunction}}"}, `Template parsing error: template: :1: function "InvalidFunction" not defined `, }, { - VolumeContext{ - Context: Context{ - Format: "{{nil}}", - }, - }, + Context{Format: "{{nil}}"}, `Template parsing error: template: :1:2: executing "" at : nil is not a command `, }, // Table format { - VolumeContext{ - Context: Context{ - Format: "table", - }, - }, + Context{Format: NewVolumeFormat("table", false)}, `DRIVER NAME foo foobar_baz bar foobar_bar `, }, { - VolumeContext{ - Context: Context{ - Format: "table", - Quiet: true, - }, - }, + Context{Format: NewVolumeFormat("table", true)}, `foobar_baz foobar_bar `, }, { - VolumeContext{ - Context: Context{ - Format: "table {{.Name}}", - }, - }, + Context{Format: NewVolumeFormat("table {{.Name}}", false)}, `NAME foobar_baz foobar_bar `, }, { - VolumeContext{ - Context: Context{ - Format: "table {{.Name}}", - Quiet: true, - }, - }, + Context{Format: NewVolumeFormat("table {{.Name}}", true)}, `NAME foobar_baz foobar_bar @@ -128,11 +103,8 @@ foobar_bar }, // Raw Format { - VolumeContext{ - Context: Context{ - Format: "raw", - }, - }, `name: foobar_baz + Context{Format: NewVolumeFormat("raw", false)}, + `name: foobar_baz driver: foo name: foobar_bar @@ -141,43 +113,32 @@ driver: bar `, }, { - VolumeContext{ - Context: Context{ - Format: "raw", - Quiet: true, - }, - }, + Context{Format: NewVolumeFormat("raw", true)}, `name: foobar_baz name: foobar_bar `, }, // Custom Format { - VolumeContext{ - Context: Context{ - Format: "{{.Name}}", - }, - }, + Context{Format: NewVolumeFormat("{{.Name}}", false)}, `foobar_baz foobar_bar `, }, } - for _, context := range contexts { + for _, testcase := range cases { volumes := []*types.Volume{ {Name: "foobar_baz", Driver: "foo"}, {Name: "foobar_bar", Driver: "bar"}, } out := bytes.NewBufferString("") - context.context.Output = out - context.context.Volumes = volumes - context.context.Write() - actual := out.String() - if actual != context.expected { - t.Fatalf("Expected \n%s, got \n%s", context.expected, actual) + testcase.context.Output = out + err := VolumeWrite(testcase.context, volumes) + if err != nil { + assert.Error(t, err, testcase.expected) + } else { + assert.Equal(t, out.String(), testcase.expected) } - // Clean buffer - out.Reset() } } diff --git a/components/cli/command/image/images.go b/components/cli/command/image/images.go index 648236dfe5..b7dbd05671 100644 --- a/components/cli/command/image/images.go +++ b/components/cli/command/image/images.go @@ -64,27 +64,22 @@ func runImages(dockerCli *command.DockerCli, opts imagesOptions) error { return err } - f := opts.format - if len(f) == 0 { + format := opts.format + if len(format) == 0 { if len(dockerCli.ConfigFile().ImagesFormat) > 0 && !opts.quiet { - f = dockerCli.ConfigFile().ImagesFormat + format = dockerCli.ConfigFile().ImagesFormat } else { - f = "table" + format = "table" } } - imagesCtx := formatter.ImageContext{ + imageCtx := formatter.ImageContext{ Context: formatter.Context{ Output: dockerCli.Out(), - Format: f, - Quiet: opts.quiet, + Format: formatter.NewImageFormat(format, opts.quiet, opts.showDigests), Trunc: !opts.noTrunc, }, Digest: opts.showDigests, - Images: images, } - - imagesCtx.Write() - - return nil + return formatter.ImageWrite(imageCtx, images) } diff --git a/components/cli/command/network/list.go b/components/cli/command/network/list.go index a0f2e7f4f0..dd7b72fea7 100644 --- a/components/cli/command/network/list.go +++ b/components/cli/command/network/list.go @@ -50,35 +50,27 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command { func runList(dockerCli *command.DockerCli, opts listOptions) error { client := dockerCli.Client() - options := types.NetworkListOptions{Filters: opts.filter.Value()} networkResources, err := client.NetworkList(context.Background(), options) if err != nil { return err } - f := opts.format - if len(f) == 0 { + format := opts.format + if len(format) == 0 { if len(dockerCli.ConfigFile().NetworksFormat) > 0 && !opts.quiet { - f = dockerCli.ConfigFile().NetworksFormat + format = dockerCli.ConfigFile().NetworksFormat } else { - f = "table" + format = "table" } } sort.Sort(byNetworkName(networkResources)) - networksCtx := formatter.NetworkContext{ - Context: formatter.Context{ - Output: dockerCli.Out(), - Format: f, - Quiet: opts.quiet, - Trunc: !opts.noTrunc, - }, - Networks: networkResources, + networksCtx := formatter.Context{ + Output: dockerCli.Out(), + Format: formatter.NewNetworkFormat(format, opts.quiet), + Trunc: !opts.noTrunc, } - - networksCtx.Write() - - return nil + return formatter.NetworkWrite(networksCtx, networkResources) } diff --git a/components/cli/command/volume/list.go b/components/cli/command/volume/list.go index 6d32d2cbfb..cdbbaafc61 100644 --- a/components/cli/command/volume/list.go +++ b/components/cli/command/volume/list.go @@ -56,29 +56,22 @@ func runList(dockerCli *command.DockerCli, opts listOptions) error { return err } - f := opts.format - if len(f) == 0 { + format := opts.format + if len(format) == 0 { if len(dockerCli.ConfigFile().VolumesFormat) > 0 && !opts.quiet { - f = dockerCli.ConfigFile().VolumesFormat + format = dockerCli.ConfigFile().VolumesFormat } else { - f = "table" + format = "table" } } sort.Sort(byVolumeName(volumes.Volumes)) - volumeCtx := formatter.VolumeContext{ - Context: formatter.Context{ - Output: dockerCli.Out(), - Format: f, - Quiet: opts.quiet, - }, - Volumes: volumes.Volumes, + volumeCtx := formatter.Context{ + Output: dockerCli.Out(), + Format: formatter.NewVolumeFormat(format, opts.quiet), } - - volumeCtx.Write() - - return nil + return formatter.VolumeWrite(volumeCtx, volumes.Volumes) } var listDescription = ` From a43858a79dd167dbf7b46a6b75888c466a3ecc80 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 13 Sep 2016 14:21:07 -0400 Subject: [PATCH 136/978] Fix testcases that expect trailing whitespace and broken integration tests based of nil pointers Signed-off-by: Daniel Nephin Upstream-commit: 2f8c4333fea63e4d5ae9158a2b13905c2e322e81 Component: cli --- components/cli/command/container/ps.go | 4 ++-- components/cli/command/formatter/container.go | 10 +++++++++- components/cli/command/formatter/volume.go | 4 ++-- components/cli/command/formatter/volume_test.go | 12 ++++++------ components/cli/command/image/images.go | 2 +- components/cli/command/network/list.go | 2 +- components/cli/command/volume/list.go | 2 +- 7 files changed, 22 insertions(+), 14 deletions(-) diff --git a/components/cli/command/container/ps.go b/components/cli/command/container/ps.go index 9d015fd707..b5a3be06e9 100644 --- a/components/cli/command/container/ps.go +++ b/components/cli/command/container/ps.go @@ -111,13 +111,13 @@ func runPs(dockerCli *command.DockerCli, opts *psOptions) error { if len(dockerCli.ConfigFile().PsFormat) > 0 && !opts.quiet { format = dockerCli.ConfigFile().PsFormat } else { - format = "table" + format = formatter.TableFormatKey } } containerCtx := formatter.Context{ Output: dockerCli.Out(), - Format: formatter.NewContainerFormat(format, opts.quiet, opts.size), + Format: formatter.NewContainerFormat(format, opts.quiet, listOptions.Size), Trunc: !opts.noTrunc, } return formatter.ContainerWrite(containerCtx, containers) diff --git a/components/cli/command/formatter/container.go b/components/cli/command/formatter/container.go index 6f3a162fe3..30a6492476 100644 --- a/components/cli/command/formatter/container.go +++ b/components/cli/command/formatter/container.go @@ -41,7 +41,15 @@ func NewContainerFormat(source string, quiet bool, size bool) Format { if quiet { return `container_id: {{.ID}}` } - format := `container_id: {{.ID}}\nimage: {{.Image}}\ncommand: {{.Command}}\ncreated_at: {{.CreatedAt}}\nstatus: {{.Status}}\nnames: {{.Names}}\nlabels: {{.Labels}}\nports: {{.Ports}}\n` + format := `container_id: {{.ID}} +image: {{.Image}} +command: {{.Command}} +created_at: {{.CreatedAt}} +status: {{- pad .Status 1 0}} +names: {{.Names}} +labels: {{- pad .Labels 1 0}} +ports: {{- pad .Ports 1 0}} +` if size { format += `size: {{.Size}}\n` } diff --git a/components/cli/command/formatter/volume.go b/components/cli/command/formatter/volume.go index 2fec59d8fb..e41ee266bf 100644 --- a/components/cli/command/formatter/volume.go +++ b/components/cli/command/formatter/volume.go @@ -36,7 +36,7 @@ func NewVolumeFormat(source string, quiet bool) Format { func VolumeWrite(ctx Context, volumes []*types.Volume) error { render := func(format func(subContext subContext) error) error { for _, volume := range volumes { - if err := format(&volumeContext{v: volume}); err != nil { + if err := format(&volumeContext{v: *volume}); err != nil { return err } } @@ -47,7 +47,7 @@ func VolumeWrite(ctx Context, volumes []*types.Volume) error { type volumeContext struct { HeaderContext - v *types.Volume + v types.Volume } func (c *volumeContext) Name() string { diff --git a/components/cli/command/formatter/volume_test.go b/components/cli/command/formatter/volume_test.go index 1d5f74e42c..8c715b3438 100644 --- a/components/cli/command/formatter/volume_test.go +++ b/components/cli/command/formatter/volume_test.go @@ -21,22 +21,22 @@ func TestVolumeContext(t *testing.T) { call func() string }{ {volumeContext{ - v: &types.Volume{Name: volumeName}, + v: types.Volume{Name: volumeName}, }, volumeName, nameHeader, ctx.Name}, {volumeContext{ - v: &types.Volume{Driver: "driver_name"}, + v: types.Volume{Driver: "driver_name"}, }, "driver_name", driverHeader, ctx.Driver}, {volumeContext{ - v: &types.Volume{Scope: "local"}, + v: types.Volume{Scope: "local"}, }, "local", scopeHeader, ctx.Scope}, {volumeContext{ - v: &types.Volume{Mountpoint: "mountpoint"}, + v: types.Volume{Mountpoint: "mountpoint"}, }, "mountpoint", mountpointHeader, ctx.Mountpoint}, {volumeContext{ - v: &types.Volume{}, + v: types.Volume{}, }, "", labelsHeader, ctx.Labels}, {volumeContext{ - v: &types.Volume{Labels: map[string]string{"label1": "value1", "label2": "value2"}}, + v: types.Volume{Labels: map[string]string{"label1": "value1", "label2": "value2"}}, }, "label1=value1,label2=value2", labelsHeader, ctx.Labels}, } diff --git a/components/cli/command/image/images.go b/components/cli/command/image/images.go index b7dbd05671..0229734ce4 100644 --- a/components/cli/command/image/images.go +++ b/components/cli/command/image/images.go @@ -69,7 +69,7 @@ func runImages(dockerCli *command.DockerCli, opts imagesOptions) error { if len(dockerCli.ConfigFile().ImagesFormat) > 0 && !opts.quiet { format = dockerCli.ConfigFile().ImagesFormat } else { - format = "table" + format = formatter.TableFormatKey } } diff --git a/components/cli/command/network/list.go b/components/cli/command/network/list.go index dd7b72fea7..9ba803275b 100644 --- a/components/cli/command/network/list.go +++ b/components/cli/command/network/list.go @@ -61,7 +61,7 @@ func runList(dockerCli *command.DockerCli, opts listOptions) error { if len(dockerCli.ConfigFile().NetworksFormat) > 0 && !opts.quiet { format = dockerCli.ConfigFile().NetworksFormat } else { - format = "table" + format = formatter.TableFormatKey } } diff --git a/components/cli/command/volume/list.go b/components/cli/command/volume/list.go index cdbbaafc61..77ce359771 100644 --- a/components/cli/command/volume/list.go +++ b/components/cli/command/volume/list.go @@ -61,7 +61,7 @@ func runList(dockerCli *command.DockerCli, opts listOptions) error { if len(dockerCli.ConfigFile().VolumesFormat) > 0 && !opts.quiet { format = dockerCli.ConfigFile().VolumesFormat } else { - format = "table" + format = formatter.TableFormatKey } } From 473fb49310a17b122c21b8b9a709bc55bb496697 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Mon, 12 Sep 2016 21:06:04 -0700 Subject: [PATCH 137/978] Check bad syntax on dockerfile before building. This fix tries to address the issue raised in 26453 where bad syntax on dockerfile is not checked before building, thus user has to wait before seeing error in dockerfile. This fix fixes the issue by evaluating all the instructions and check syntax before dockerfile is invoked actually. All existing tests pass. Signed-off-by: Yong Tang Upstream-commit: 824707ea494f279765e8d685f54c9bc0d45f7702 Component: cli --- components/cli/command/image/build.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go index 85f51f14c0..17be405bd5 100644 --- a/components/cli/command/image/build.go +++ b/components/cli/command/image/build.go @@ -293,6 +293,9 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions) if err != nil { + if options.quiet { + fmt.Fprintf(dockerCli.Err(), "%s", progBuff) + } return err } defer response.Body.Close() From b8d8e583b402cb6d84d52a9f5c6369345367757b Mon Sep 17 00:00:00 2001 From: allencloud Date: Sun, 4 Sep 2016 15:17:58 +0800 Subject: [PATCH 138/978] correct some nits in comments Signed-off-by: allencloud Upstream-commit: acb1fc424bd8a0f339be041204a04f7fd9791b52 Component: cli --- components/cli/events_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/events_test.go b/components/cli/events_test.go index f7cb33f611..57689322c3 100644 --- a/components/cli/events_test.go +++ b/components/cli/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 85c46768d00477122d91199b98de56f813a34852 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 2 Sep 2016 07:40:06 +0000 Subject: [PATCH 139/978] add `docker events --format` Signed-off-by: Akihiro Suda Upstream-commit: 0ae2a02ce6e4d3f2e8d3ecf54358414bec7ff62d Component: cli --- components/cli/command/system/events.go | 54 ++++++++++++++++++++----- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/components/cli/command/system/events.go b/components/cli/command/system/events.go index b9d740f356..f2946b8763 100644 --- a/components/cli/command/system/events.go +++ b/components/cli/command/system/events.go @@ -3,8 +3,10 @@ package system import ( "fmt" "io" + "io/ioutil" "sort" "strings" + "text/template" "time" "golang.org/x/net/context" @@ -15,6 +17,7 @@ import ( "github.com/docker/docker/cli/command" "github.com/docker/docker/opts" "github.com/docker/docker/pkg/jsonlog" + "github.com/docker/docker/utils/templates" "github.com/spf13/cobra" ) @@ -22,6 +25,7 @@ type eventsOptions struct { since string until string filter opts.FilterOpt + format string } // NewEventsCommand creates a new cobra.Command for `docker events` @@ -41,11 +45,18 @@ func NewEventsCommand(dockerCli *command.DockerCli) *cobra.Command { flags.StringVar(&opts.since, "since", "", "Show all events created since timestamp") flags.StringVar(&opts.until, "until", "", "Stream events until this timestamp") flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") + flags.StringVar(&opts.format, "format", "", "Format the output using the given go template") return cmd } func runEvents(dockerCli *command.DockerCli, opts *eventsOptions) error { + tmpl, err := makeTemplate(opts.format) + if err != nil { + return cli.StatusError{ + StatusCode: 64, + Status: "Error parsing format: " + err.Error()} + } options := types.EventsOptions{ Since: opts.since, Until: opts.until, @@ -58,33 +69,48 @@ func runEvents(dockerCli *command.DockerCli, opts *eventsOptions) error { } defer responseBody.Close() - return streamEvents(responseBody, dockerCli.Out()) + return streamEvents(dockerCli.Out(), responseBody, tmpl) +} + +func makeTemplate(format string) (*template.Template, error) { + if format == "" { + return nil, nil + } + tmpl, err := templates.Parse(format) + if err != nil { + return tmpl, err + } + // we execute the template for an empty message, so as to validate + // a bad template like "{{.badFieldString}}" + return tmpl, tmpl.Execute(ioutil.Discard, &eventtypes.Message{}) } // streamEvents decodes prints the incoming events in the provided output. -func streamEvents(input io.Reader, output io.Writer) error { +func streamEvents(out io.Writer, input io.Reader, tmpl *template.Template) error { return DecodeEvents(input, func(event eventtypes.Message, err error) error { if err != nil { return err } - printOutput(event, output) - return nil + if tmpl == nil { + return prettyPrintEvent(out, event) + } + return formatEvent(out, event, tmpl) }) } type eventProcessor func(event eventtypes.Message, err error) error -// printOutput prints all types of event information. +// prettyPrintEvent prints all types of event information. // Each output includes the event type, actor id, name and action. // Actor attributes are printed at the end if the actor has any. -func printOutput(event eventtypes.Message, output io.Writer) { +func prettyPrintEvent(out io.Writer, event eventtypes.Message) error { if event.TimeNano != 0 { - fmt.Fprintf(output, "%s ", time.Unix(0, event.TimeNano).Format(jsonlog.RFC3339NanoFixed)) + fmt.Fprintf(out, "%s ", time.Unix(0, event.TimeNano).Format(jsonlog.RFC3339NanoFixed)) } else if event.Time != 0 { - fmt.Fprintf(output, "%s ", time.Unix(event.Time, 0).Format(jsonlog.RFC3339NanoFixed)) + fmt.Fprintf(out, "%s ", time.Unix(event.Time, 0).Format(jsonlog.RFC3339NanoFixed)) } - fmt.Fprintf(output, "%s %s %s", event.Type, event.Action, event.Actor.ID) + fmt.Fprintf(out, "%s %s %s", event.Type, event.Action, event.Actor.ID) if len(event.Actor.Attributes) > 0 { var attrs []string @@ -97,7 +123,13 @@ func printOutput(event eventtypes.Message, output io.Writer) { v := event.Actor.Attributes[k] attrs = append(attrs, fmt.Sprintf("%s=%s", k, v)) } - fmt.Fprintf(output, " (%s)", strings.Join(attrs, ", ")) + fmt.Fprintf(out, " (%s)", strings.Join(attrs, ", ")) } - fmt.Fprint(output, "\n") + fmt.Fprint(out, "\n") + return nil +} + +func formatEvent(out io.Writer, event eventtypes.Message, tmpl *template.Template) error { + defer out.Write([]byte{'\n'}) + return tmpl.Execute(out, event) } From 0e8c716b0824379260b9f84211ef6d482f733b60 Mon Sep 17 00:00:00 2001 From: John Howard Date: Wed, 7 Sep 2016 11:14:49 -0700 Subject: [PATCH 140/978] Only output security options if there are any Signed-off-by: John Howard Upstream-commit: d0e960f3b18331107b8087ba536bb5828bffe97c Component: cli --- components/cli/command/system/info.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/components/cli/command/system/info.go b/components/cli/command/system/info.go index 259b254bd0..a2d0abad23 100644 --- a/components/cli/command/system/info.go +++ b/components/cli/command/system/info.go @@ -135,9 +135,11 @@ func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error { fmt.Fprintf(dockerCli.Out(), "Default Runtime: %s\n", info.DefaultRuntime) } - fmt.Fprintf(dockerCli.Out(), "Security Options:") - ioutils.FprintfIfNotEmpty(dockerCli.Out(), " %s", strings.Join(info.SecurityOptions, " ")) - fmt.Fprintf(dockerCli.Out(), "\n") + if info.OSType == "linux" { + fmt.Fprintf(dockerCli.Out(), "Security Options:") + ioutils.FprintfIfNotEmpty(dockerCli.Out(), " %s", strings.Join(info.SecurityOptions, " ")) + fmt.Fprintf(dockerCli.Out(), "\n") + } ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Kernel Version: %s\n", info.KernelVersion) ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Operating System: %s\n", info.OperatingSystem) From 25db3b26a760a0c9ec7244765314dd293e135c5c Mon Sep 17 00:00:00 2001 From: John Howard Date: Wed, 7 Sep 2016 16:08:51 -0700 Subject: [PATCH 141/978] Windows: stats support Signed-off-by: John Howard Upstream-commit: c3238783319476c4bb26a9f80a67b151ad00ed73 Component: cli --- components/cli/command/container/stats.go | 10 +- .../cli/command/container/stats_helpers.go | 144 ++++++++++++------ 2 files changed, 110 insertions(+), 44 deletions(-) diff --git a/components/cli/command/container/stats.go b/components/cli/command/container/stats.go index ffd3fcae9f..4c97883898 100644 --- a/components/cli/command/container/stats.go +++ b/components/cli/command/container/stats.go @@ -187,7 +187,15 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error { fmt.Fprint(dockerCli.Out(), "\033[2J") fmt.Fprint(dockerCli.Out(), "\033[H") } - io.WriteString(w, "CONTAINER\tCPU %\tMEM USAGE / LIMIT\tMEM %\tNET I/O\tBLOCK I/O\tPIDS\n") + switch daemonOSType { + case "": + // Before we have any stats from the daemon, we don't know the platform... + io.WriteString(w, "Waiting for statistics...\n") + case "windows": + io.WriteString(w, "CONTAINER\tCPU %\tPRIV WORKING SET\tNET I/O\tBLOCK I/O\n") + default: + io.WriteString(w, "CONTAINER\tCPU %\tMEM USAGE / LIMIT\tMEM %\tNET I/O\tBLOCK I/O\tPIDS\n") + } } for range time.Tick(500 * time.Millisecond) { diff --git a/components/cli/command/container/stats_helpers.go b/components/cli/command/container/stats_helpers.go index 54cc5589c1..b48d9c7c60 100644 --- a/components/cli/command/container/stats_helpers.go +++ b/components/cli/command/container/stats_helpers.go @@ -19,23 +19,29 @@ import ( type containerStats struct { Name string CPUPercentage float64 - Memory float64 - MemoryLimit float64 - MemoryPercentage float64 + Memory float64 // On Windows this is the private working set + MemoryLimit float64 // Not used on Windows + MemoryPercentage float64 // Not used on Windows NetworkRx float64 NetworkTx float64 BlockRead float64 BlockWrite float64 - PidsCurrent uint64 + PidsCurrent uint64 // Not used on Windows mu sync.Mutex err error } type stats struct { - mu sync.Mutex - cs []*containerStats + mu sync.Mutex + ostype string + cs []*containerStats } +// daemonOSType is set once we have at least one stat for a container +// from the daemon. It is used to ensure we print the right header based +// on the daemon platform. +var daemonOSType string + func (s *stats) add(cs *containerStats) bool { s.mu.Lock() defer s.mu.Unlock() @@ -80,22 +86,28 @@ func (s *containerStats) Collect(ctx context.Context, cli client.APIClient, stre } }() - responseBody, err := cli.ContainerStats(ctx, s.Name, streamStats) + response, err := cli.ContainerStats(ctx, s.Name, streamStats) if err != nil { s.mu.Lock() s.err = err s.mu.Unlock() return } - defer responseBody.Close() + defer response.Body.Close() - dec := json.NewDecoder(responseBody) + dec := json.NewDecoder(response.Body) go func() { for { - var v *types.StatsJSON + var ( + v *types.StatsJSON + memPercent = 0.0 + cpuPercent = 0.0 + blkRead, blkWrite uint64 // Only used on Linux + mem = 0.0 + ) if err := dec.Decode(&v); err != nil { - dec = json.NewDecoder(io.MultiReader(dec.Buffered(), responseBody)) + dec = json.NewDecoder(io.MultiReader(dec.Buffered(), response.Body)) u <- err if err == io.EOF { break @@ -104,28 +116,38 @@ func (s *containerStats) Collect(ctx context.Context, cli client.APIClient, stre continue } - var memPercent = 0.0 - var cpuPercent = 0.0 + daemonOSType = response.OSType - // MemoryStats.Limit will never be 0 unless the container is not running and we haven't - // got any data from cgroup - if v.MemoryStats.Limit != 0 { - memPercent = float64(v.MemoryStats.Usage) / float64(v.MemoryStats.Limit) * 100.0 + if daemonOSType != "windows" { + // MemoryStats.Limit will never be 0 unless the container is not running and we haven't + // got any data from cgroup + if v.MemoryStats.Limit != 0 { + memPercent = float64(v.MemoryStats.Usage) / float64(v.MemoryStats.Limit) * 100.0 + } + previousCPU = v.PreCPUStats.CPUUsage.TotalUsage + previousSystem = v.PreCPUStats.SystemUsage + cpuPercent = calculateCPUPercentUnix(previousCPU, previousSystem, v) + blkRead, blkWrite = calculateBlockIO(v.BlkioStats) + mem = float64(v.MemoryStats.Usage) + + } else { + cpuPercent = calculateCPUPercentWindows(v) + blkRead = v.StorageStats.ReadSizeBytes + blkWrite = v.StorageStats.WriteSizeBytes + mem = float64(v.MemoryStats.PrivateWorkingSet) } - previousCPU = v.PreCPUStats.CPUUsage.TotalUsage - previousSystem = v.PreCPUStats.SystemUsage - cpuPercent = calculateCPUPercent(previousCPU, previousSystem, v) - blkRead, blkWrite := calculateBlockIO(v.BlkioStats) s.mu.Lock() s.CPUPercentage = cpuPercent - s.Memory = float64(v.MemoryStats.Usage) - s.MemoryLimit = float64(v.MemoryStats.Limit) - s.MemoryPercentage = memPercent + s.Memory = mem s.NetworkRx, s.NetworkTx = calculateNetwork(v.Networks) s.BlockRead = float64(blkRead) s.BlockWrite = float64(blkWrite) - s.PidsCurrent = v.PidsStats.Current + if daemonOSType != "windows" { + s.MemoryLimit = float64(v.MemoryStats.Limit) + s.MemoryPercentage = memPercent + s.PidsCurrent = v.PidsStats.Current + } s.mu.Unlock() u <- nil if !streamStats { @@ -178,29 +200,49 @@ func (s *containerStats) Collect(ctx context.Context, cli client.APIClient, stre func (s *containerStats) Display(w io.Writer) error { s.mu.Lock() defer s.mu.Unlock() - // NOTE: if you change this format, you must also change the err format below! - format := "%s\t%.2f%%\t%s / %s\t%.2f%%\t%s / %s\t%s / %s\t%d\n" - if s.err != nil { - format = "%s\t%s\t%s / %s\t%s\t%s / %s\t%s / %s\t%s\n" - errStr := "--" + if daemonOSType == "windows" { + // NOTE: if you change this format, you must also change the err format below! + format := "%s\t%.2f%%\t%s\t%s / %s\t%s / %s\n" + if s.err != nil { + format = "%s\t%s\t%s\t%s / %s\t%s / %s\n" + errStr := "--" + fmt.Fprintf(w, format, + s.Name, errStr, errStr, errStr, errStr, errStr, errStr, + ) + err := s.err + return err + } fmt.Fprintf(w, format, - s.Name, errStr, errStr, errStr, errStr, errStr, errStr, errStr, errStr, errStr, - ) - err := s.err - return err + s.Name, + s.CPUPercentage, + units.BytesSize(s.Memory), + units.HumanSizeWithPrecision(s.NetworkRx, 3), units.HumanSizeWithPrecision(s.NetworkTx, 3), + units.HumanSizeWithPrecision(s.BlockRead, 3), units.HumanSizeWithPrecision(s.BlockWrite, 3)) + } else { + // NOTE: if you change this format, you must also change the err format below! + format := "%s\t%.2f%%\t%s / %s\t%.2f%%\t%s / %s\t%s / %s\t%d\n" + if s.err != nil { + format = "%s\t%s\t%s / %s\t%s\t%s / %s\t%s / %s\t%s\n" + errStr := "--" + fmt.Fprintf(w, format, + s.Name, errStr, errStr, errStr, errStr, errStr, errStr, errStr, errStr, errStr, + ) + err := s.err + return err + } + fmt.Fprintf(w, format, + s.Name, + s.CPUPercentage, + units.BytesSize(s.Memory), units.BytesSize(s.MemoryLimit), + s.MemoryPercentage, + units.HumanSizeWithPrecision(s.NetworkRx, 3), units.HumanSizeWithPrecision(s.NetworkTx, 3), + units.HumanSizeWithPrecision(s.BlockRead, 3), units.HumanSizeWithPrecision(s.BlockWrite, 3), + s.PidsCurrent) } - fmt.Fprintf(w, format, - s.Name, - s.CPUPercentage, - units.BytesSize(s.Memory), units.BytesSize(s.MemoryLimit), - s.MemoryPercentage, - units.HumanSizeWithPrecision(s.NetworkRx, 3), units.HumanSizeWithPrecision(s.NetworkTx, 3), - units.HumanSizeWithPrecision(s.BlockRead, 3), units.HumanSizeWithPrecision(s.BlockWrite, 3), - s.PidsCurrent) return nil } -func calculateCPUPercent(previousCPU, previousSystem uint64, v *types.StatsJSON) float64 { +func calculateCPUPercentUnix(previousCPU, previousSystem uint64, v *types.StatsJSON) float64 { var ( cpuPercent = 0.0 // calculate the change for the cpu usage of the container in between readings @@ -215,6 +257,22 @@ func calculateCPUPercent(previousCPU, previousSystem uint64, v *types.StatsJSON) return cpuPercent } +func calculateCPUPercentWindows(v *types.StatsJSON) float64 { + // Max number of 100ns intervals between the previous time read and now + possIntervals := uint64(v.Read.Sub(v.PreRead).Nanoseconds()) // Start with number of ns intervals + possIntervals /= 100 // Convert to number of 100ns intervals + possIntervals *= uint64(v.NumProcs) // Multiple by the number of processors + + // Intervals used + intervalsUsed := v.CPUStats.CPUUsage.TotalUsage - v.PreCPUStats.CPUUsage.TotalUsage + + // Percentage avoiding divide-by-zero + if possIntervals > 0 { + return float64(intervalsUsed) / float64(possIntervals) * 100.0 + } + return 0.00 +} + func calculateBlockIO(blkio types.BlkioStats) (blkRead uint64, blkWrite uint64) { for _, bioEntry := range blkio.IoServiceBytesRecursive { switch strings.ToLower(bioEntry.Op) { From f35e1280b53a0548d92acdf9beeff4d9e502de85 Mon Sep 17 00:00:00 2001 From: John Howard Date: Wed, 7 Sep 2016 16:08:51 -0700 Subject: [PATCH 142/978] Windows: stats support Signed-off-by: John Howard Upstream-commit: 86c86fc1663049374d6cf86d9a27f812b6691683 Component: cli --- components/cli/container_stats.go | 10 ++++++---- components/cli/container_stats_test.go | 6 +++--- components/cli/image_build.go | 5 +++-- components/cli/image_build_test.go | 2 +- components/cli/interface.go | 2 +- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/components/cli/container_stats.go b/components/cli/container_stats.go index 2cc67c3af1..3be7a988f4 100644 --- a/components/cli/container_stats.go +++ b/components/cli/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/components/cli/container_stats_test.go b/components/cli/container_stats_test.go index 22ecd6170f..dc7c56492b 100644 --- a/components/cli/container_stats_test.go +++ b/components/cli/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/components/cli/image_build.go b/components/cli/image_build.go index 8dd6744859..a84bf57821 100644 --- a/components/cli/image_build.go +++ b/components/cli/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/components/cli/image_build_test.go b/components/cli/image_build_test.go index 8261c54854..def88c3cb6 100644 --- a/components/cli/image_build_test.go +++ b/components/cli/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/components/cli/interface.go b/components/cli/interface.go index 1bfeb6aeb6..2d5555ff06 100644 --- a/components/cli/interface.go +++ b/components/cli/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 a8c10a8cb0b41cc76346c5109b2eaa1bfbcc914e Mon Sep 17 00:00:00 2001 From: Boaz Shuster Date: Mon, 18 Jul 2016 21:30:15 +0300 Subject: [PATCH 143/978] Add the format switch to the stats command Signed-off-by: Boaz Shuster Upstream-commit: a4f3442403ec2cb89052f7fafb21bd6ab306f748 Component: cli --- components/cli/command/container/stats.go | 82 +++++------ .../cli/command/container/stats_helpers.go | 95 +++--------- .../cli/command/container/stats_unit_test.go | 25 ---- components/cli/command/formatter/stats.go | 135 ++++++++++++++++++ 4 files changed, 188 insertions(+), 149 deletions(-) create mode 100644 components/cli/command/formatter/stats.go diff --git a/components/cli/command/container/stats.go b/components/cli/command/container/stats.go index 4c97883898..2bd5e3db75 100644 --- a/components/cli/command/container/stats.go +++ b/components/cli/command/container/stats.go @@ -5,25 +5,24 @@ import ( "io" "strings" "sync" - "text/tabwriter" "time" "golang.org/x/net/context" - "github.com/Sirupsen/logrus" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/events" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/formatter" "github.com/docker/docker/cli/command/system" "github.com/spf13/cobra" ) type statsOptions struct { - all bool - noStream bool - + all bool + noStream bool + format string containers []string } @@ -44,6 +43,7 @@ func NewStatsCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() flags.BoolVarP(&opts.all, "all", "a", false, "Show all containers (default shows just running)") flags.BoolVar(&opts.noStream, "no-stream", false, "Disable streaming stats and only pull the first result") + flags.StringVar(&opts.format, "format", "", "Pretty-print images using a Go template") return cmd } @@ -98,10 +98,10 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error { closeChan <- err } for _, container := range cs { - s := &containerStats{Name: container.ID[:12]} + s := formatter.NewContainerStats(container.ID[:12], daemonOSType) if cStats.add(s) { waitFirst.Add(1) - go s.Collect(ctx, dockerCli.Client(), !opts.noStream, waitFirst) + go collect(s, ctx, dockerCli.Client(), !opts.noStream, waitFirst) } } } @@ -115,19 +115,19 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error { eh := system.InitEventHandler() eh.Handle("create", func(e events.Message) { if opts.all { - s := &containerStats{Name: e.ID[:12]} + s := formatter.NewContainerStats(e.ID[:12], daemonOSType) if cStats.add(s) { waitFirst.Add(1) - go s.Collect(ctx, dockerCli.Client(), !opts.noStream, waitFirst) + go collect(s, ctx, dockerCli.Client(), !opts.noStream, waitFirst) } } }) eh.Handle("start", func(e events.Message) { - s := &containerStats{Name: e.ID[:12]} + s := formatter.NewContainerStats(e.ID[:12], daemonOSType) if cStats.add(s) { waitFirst.Add(1) - go s.Collect(ctx, dockerCli.Client(), !opts.noStream, waitFirst) + go collect(s, ctx, dockerCli.Client(), !opts.noStream, waitFirst) } }) @@ -150,10 +150,10 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error { // Artificially send creation events for the containers we were asked to // monitor (same code path than we use when monitoring all containers). for _, name := range opts.containers { - s := &containerStats{Name: name} + s := formatter.NewContainerStats(name, daemonOSType) if cStats.add(s) { waitFirst.Add(1) - go s.Collect(ctx, dockerCli.Client(), !opts.noStream, waitFirst) + go collect(s, ctx, dockerCli.Client(), !opts.noStream, waitFirst) } } @@ -166,11 +166,11 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error { var errs []string cStats.mu.Lock() for _, c := range cStats.cs { - c.mu.Lock() - if c.err != nil { - errs = append(errs, fmt.Sprintf("%s: %v", c.Name, c.err)) + c.Mu.Lock() + if c.Err != nil { + errs = append(errs, fmt.Sprintf("%s: %v", c.Name, c.Err)) } - c.mu.Unlock() + c.Mu.Unlock() } cStats.mu.Unlock() if len(errs) > 0 { @@ -180,44 +180,34 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error { // before print to screen, make sure each container get at least one valid stat data waitFirst.Wait() + f := "table" + if len(opts.format) > 0 { + f = opts.format + } + statsCtx := formatter.Context{ + Output: dockerCli.Out(), + Format: formatter.NewStatsFormat(f, daemonOSType), + } - w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0) - printHeader := func() { + cleanHeader := func() { if !opts.noStream { fmt.Fprint(dockerCli.Out(), "\033[2J") fmt.Fprint(dockerCli.Out(), "\033[H") } - switch daemonOSType { - case "": - // Before we have any stats from the daemon, we don't know the platform... - io.WriteString(w, "Waiting for statistics...\n") - case "windows": - io.WriteString(w, "CONTAINER\tCPU %\tPRIV WORKING SET\tNET I/O\tBLOCK I/O\n") - default: - io.WriteString(w, "CONTAINER\tCPU %\tMEM USAGE / LIMIT\tMEM %\tNET I/O\tBLOCK I/O\tPIDS\n") - } } + var err error for range time.Tick(500 * time.Millisecond) { - printHeader() - toRemove := []string{} - cStats.mu.Lock() - for _, s := range cStats.cs { - if err := s.Display(w); err != nil && !opts.noStream { - logrus.Debugf("stats: got error for %s: %v", s.Name, err) - if err == io.EOF { - toRemove = append(toRemove, s.Name) - } - } + cleanHeader() + cStats.mu.RLock() + csLen := len(cStats.cs) + if err = formatter.ContainerStatsWrite(statsCtx, cStats.cs); err != nil { + break } - cStats.mu.Unlock() - for _, name := range toRemove { - cStats.remove(name) + cStats.mu.RUnlock() + if csLen == 0 && !showAll { + break } - if len(cStats.cs) == 0 && !showAll { - return nil - } - w.Flush() if opts.noStream { break } @@ -237,5 +227,5 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error { // just skip } } - return nil + return err } diff --git a/components/cli/command/container/stats_helpers.go b/components/cli/command/container/stats_helpers.go index b48d9c7c60..2039d2ade6 100644 --- a/components/cli/command/container/stats_helpers.go +++ b/components/cli/command/container/stats_helpers.go @@ -3,7 +3,6 @@ package container import ( "encoding/json" "errors" - "fmt" "io" "strings" "sync" @@ -11,30 +10,15 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/docker/api/types" + "github.com/docker/docker/cli/command/formatter" "github.com/docker/docker/client" - "github.com/docker/go-units" "golang.org/x/net/context" ) -type containerStats struct { - Name string - CPUPercentage float64 - Memory float64 // On Windows this is the private working set - MemoryLimit float64 // Not used on Windows - MemoryPercentage float64 // Not used on Windows - NetworkRx float64 - NetworkTx float64 - BlockRead float64 - BlockWrite float64 - PidsCurrent uint64 // Not used on Windows - mu sync.Mutex - err error -} - type stats struct { - mu sync.Mutex ostype string - cs []*containerStats + mu sync.RWMutex + cs []*formatter.ContainerStats } // daemonOSType is set once we have at least one stat for a container @@ -42,7 +26,7 @@ type stats struct { // on the daemon platform. var daemonOSType string -func (s *stats) add(cs *containerStats) bool { +func (s *stats) add(cs *formatter.ContainerStats) bool { s.mu.Lock() defer s.mu.Unlock() if _, exists := s.isKnownContainer(cs.Name); !exists { @@ -69,7 +53,7 @@ func (s *stats) isKnownContainer(cid string) (int, bool) { return -1, false } -func (s *containerStats) Collect(ctx context.Context, cli client.APIClient, streamStats bool, waitFirst *sync.WaitGroup) { +func collect(s *formatter.ContainerStats, ctx context.Context, cli client.APIClient, streamStats bool, waitFirst *sync.WaitGroup) { logrus.Debugf("collecting stats for %s", s.Name) var ( getFirst bool @@ -88,9 +72,9 @@ func (s *containerStats) Collect(ctx context.Context, cli client.APIClient, stre response, err := cli.ContainerStats(ctx, s.Name, streamStats) if err != nil { - s.mu.Lock() - s.err = err - s.mu.Unlock() + s.Mu.Lock() + s.Err = err + s.Mu.Unlock() return } defer response.Body.Close() @@ -137,7 +121,7 @@ func (s *containerStats) Collect(ctx context.Context, cli client.APIClient, stre mem = float64(v.MemoryStats.PrivateWorkingSet) } - s.mu.Lock() + s.Mu.Lock() s.CPUPercentage = cpuPercent s.Memory = mem s.NetworkRx, s.NetworkTx = calculateNetwork(v.Networks) @@ -148,7 +132,7 @@ func (s *containerStats) Collect(ctx context.Context, cli client.APIClient, stre s.MemoryPercentage = memPercent s.PidsCurrent = v.PidsStats.Current } - s.mu.Unlock() + s.Mu.Unlock() u <- nil if !streamStats { return @@ -160,7 +144,7 @@ func (s *containerStats) Collect(ctx context.Context, cli client.APIClient, stre case <-time.After(2 * time.Second): // zero out the values if we have not received an update within // the specified duration. - s.mu.Lock() + s.Mu.Lock() s.CPUPercentage = 0 s.Memory = 0 s.MemoryPercentage = 0 @@ -170,8 +154,8 @@ func (s *containerStats) Collect(ctx context.Context, cli client.APIClient, stre s.BlockRead = 0 s.BlockWrite = 0 s.PidsCurrent = 0 - s.err = errors.New("timeout waiting for stats") - s.mu.Unlock() + s.Err = errors.New("timeout waiting for stats") + s.Mu.Unlock() // if this is the first stat you get, release WaitGroup if !getFirst { getFirst = true @@ -179,12 +163,12 @@ func (s *containerStats) Collect(ctx context.Context, cli client.APIClient, stre } case err := <-u: if err != nil { - s.mu.Lock() - s.err = err - s.mu.Unlock() + s.Mu.Lock() + s.Err = err + s.Mu.Unlock() continue } - s.err = nil + s.Err = nil // if this is the first stat you get, release WaitGroup if !getFirst { getFirst = true @@ -197,51 +181,6 @@ func (s *containerStats) Collect(ctx context.Context, cli client.APIClient, stre } } -func (s *containerStats) Display(w io.Writer) error { - s.mu.Lock() - defer s.mu.Unlock() - if daemonOSType == "windows" { - // NOTE: if you change this format, you must also change the err format below! - format := "%s\t%.2f%%\t%s\t%s / %s\t%s / %s\n" - if s.err != nil { - format = "%s\t%s\t%s\t%s / %s\t%s / %s\n" - errStr := "--" - fmt.Fprintf(w, format, - s.Name, errStr, errStr, errStr, errStr, errStr, errStr, - ) - err := s.err - return err - } - fmt.Fprintf(w, format, - s.Name, - s.CPUPercentage, - units.BytesSize(s.Memory), - units.HumanSizeWithPrecision(s.NetworkRx, 3), units.HumanSizeWithPrecision(s.NetworkTx, 3), - units.HumanSizeWithPrecision(s.BlockRead, 3), units.HumanSizeWithPrecision(s.BlockWrite, 3)) - } else { - // NOTE: if you change this format, you must also change the err format below! - format := "%s\t%.2f%%\t%s / %s\t%.2f%%\t%s / %s\t%s / %s\t%d\n" - if s.err != nil { - format = "%s\t%s\t%s / %s\t%s\t%s / %s\t%s / %s\t%s\n" - errStr := "--" - fmt.Fprintf(w, format, - s.Name, errStr, errStr, errStr, errStr, errStr, errStr, errStr, errStr, errStr, - ) - err := s.err - return err - } - fmt.Fprintf(w, format, - s.Name, - s.CPUPercentage, - units.BytesSize(s.Memory), units.BytesSize(s.MemoryLimit), - s.MemoryPercentage, - units.HumanSizeWithPrecision(s.NetworkRx, 3), units.HumanSizeWithPrecision(s.NetworkTx, 3), - units.HumanSizeWithPrecision(s.BlockRead, 3), units.HumanSizeWithPrecision(s.BlockWrite, 3), - s.PidsCurrent) - } - return nil -} - func calculateCPUPercentUnix(previousCPU, previousSystem uint64, v *types.StatsJSON) float64 { var ( cpuPercent = 0.0 diff --git a/components/cli/command/container/stats_unit_test.go b/components/cli/command/container/stats_unit_test.go index 182ab5b30d..fc6563c4d9 100644 --- a/components/cli/command/container/stats_unit_test.go +++ b/components/cli/command/container/stats_unit_test.go @@ -1,36 +1,11 @@ package container import ( - "bytes" "testing" "github.com/docker/docker/api/types" ) -func TestDisplay(t *testing.T) { - c := &containerStats{ - Name: "app", - CPUPercentage: 30.0, - Memory: 100 * 1024 * 1024.0, - MemoryLimit: 2048 * 1024 * 1024.0, - MemoryPercentage: 100.0 / 2048.0 * 100.0, - NetworkRx: 100 * 1024 * 1024, - NetworkTx: 800 * 1024 * 1024, - BlockRead: 100 * 1024 * 1024, - BlockWrite: 800 * 1024 * 1024, - PidsCurrent: 1, - } - var b bytes.Buffer - if err := c.Display(&b); err != nil { - t.Fatalf("c.Display() gave error: %s", err) - } - got := b.String() - want := "app\t30.00%\t100 MiB / 2 GiB\t4.88%\t105 MB / 839 MB\t105 MB / 839 MB\t1\n" - if got != want { - t.Fatalf("c.Display() = %q, want %q", got, want) - } -} - func TestCalculBlockIO(t *testing.T) { blkio := types.BlkioStats{ IoServiceBytesRecursive: []types.BlkioStatEntry{{8, 0, "read", 1234}, {8, 1, "read", 4567}, {8, 0, "write", 123}, {8, 1, "write", 456}}, diff --git a/components/cli/command/formatter/stats.go b/components/cli/command/formatter/stats.go new file mode 100644 index 0000000000..939431da1c --- /dev/null +++ b/components/cli/command/formatter/stats.go @@ -0,0 +1,135 @@ +package formatter + +import ( + "fmt" + "sync" + + "github.com/docker/go-units" +) + +const ( + defaultStatsTableFormat = "table {{.Container}}\t{{.CPUPrec}}\t{{.MemUsage}}\t{{.MemPrec}}\t{{.NetIO}}\t{{.BlockIO}}\t{{.PIDs}}" + winDefaultStatsTableFormat = "table {{.Container}}\t{{.CPUPrec}}\t{{{.MemUsage}}\t{.NetIO}}\t{{.BlockIO}}" + emptyStatsTableFormat = "Waiting for statistics..." + + containerHeader = "CONTAINER" + cpuPrecHeader = "CPU %" + netIOHeader = "NET I/O" + blockIOHeader = "BLOCK I/O" + winMemPrecHeader = "PRIV WORKING SET" // Used only on Window + memPrecHeader = "MEM %" // Used only on Linux + memUseHeader = "MEM USAGE / LIMIT" // Used only on Linux + pidsHeader = "PIDS" // Used only on Linux +) + +// ContainerStatsAttrs represents the statistics data collected from a container. +type ContainerStatsAttrs struct { + Windows bool + Name string + CPUPercentage float64 + Memory float64 // On Windows this is the private working set + MemoryLimit float64 // Not used on Windows + MemoryPercentage float64 // Not used on Windows + NetworkRx float64 + NetworkTx float64 + BlockRead float64 + BlockWrite float64 + PidsCurrent uint64 // Not used on Windows +} + +// ContainerStats represents the containers statistics data. +type ContainerStats struct { + Mu sync.RWMutex + ContainerStatsAttrs + Err error +} + +// NewStatsFormat returns a format for rendering an CStatsContext +func NewStatsFormat(source, osType string) Format { + if source == TableFormatKey { + if osType == "windows" { + return Format(winDefaultStatsTableFormat) + } + return Format(defaultStatsTableFormat) + } + return Format(source) +} + +// NewContainerStats returns a new ContainerStats entity and sets in it the given name +func NewContainerStats(name, osType string) *ContainerStats { + return &ContainerStats{ + ContainerStatsAttrs: ContainerStatsAttrs{ + Name: name, + Windows: (osType == "windows"), + }, + } +} + +// ContainerStatsWrite renders the context for a list of containers statistics +func ContainerStatsWrite(ctx Context, containerStats []*ContainerStats) error { + render := func(format func(subContext subContext) error) error { + for _, cstats := range containerStats { + cstats.Mu.RLock() + cstatsAttrs := cstats.ContainerStatsAttrs + cstats.Mu.RUnlock() + containerStatsCtx := &containerStatsContext{ + s: cstatsAttrs, + } + if err := format(containerStatsCtx); err != nil { + return err + } + } + return nil + } + return ctx.Write(&containerStatsContext{}, render) +} + +type containerStatsContext struct { + HeaderContext + s ContainerStatsAttrs +} + +func (c *containerStatsContext) Container() string { + c.AddHeader(containerHeader) + return c.s.Name +} + +func (c *containerStatsContext) CPUPrec() string { + c.AddHeader(cpuPrecHeader) + return fmt.Sprintf("%.2f%%", c.s.CPUPercentage) +} + +func (c *containerStatsContext) MemUsage() string { + c.AddHeader(memUseHeader) + if !c.s.Windows { + return fmt.Sprintf("%s / %s", units.BytesSize(c.s.Memory), units.BytesSize(c.s.MemoryLimit)) + } + return fmt.Sprintf("-- / --") +} + +func (c *containerStatsContext) MemPrec() string { + header := memPrecHeader + if c.s.Windows { + header = winMemPrecHeader + } + c.AddHeader(header) + return fmt.Sprintf("%.2f%%", c.s.MemoryPercentage) +} + +func (c *containerStatsContext) NetIO() string { + c.AddHeader(netIOHeader) + return fmt.Sprintf("%s / %s", units.HumanSizeWithPrecision(c.s.NetworkRx, 3), units.HumanSizeWithPrecision(c.s.NetworkTx, 3)) +} + +func (c *containerStatsContext) BlockIO() string { + c.AddHeader(blockIOHeader) + return fmt.Sprintf("%s / %s", units.HumanSizeWithPrecision(c.s.BlockRead, 3), units.HumanSizeWithPrecision(c.s.BlockWrite, 3)) +} + +func (c *containerStatsContext) PIDs() string { + c.AddHeader(pidsHeader) + if !c.s.Windows { + return fmt.Sprintf("%d", c.s.PidsCurrent) + } + return fmt.Sprintf("-") +} From 5ae46f25cb1d0a58773471374dbee7e8d8e87c08 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 23 Jun 2016 13:03:40 -0400 Subject: [PATCH 144/978] Move canonical image and container commands into a command group Hide some top level commands Add docker container and image inspect commands. Signed-off-by: Daniel Nephin Upstream-commit: accc5d5bd46de3c0d90d84eacb2d589290b497da Component: cli --- components/cli/command/commands/commands.go | 75 +++++++++++-------- components/cli/command/container/cmd.go | 49 ++++++++++++ components/cli/command/container/inspect.go | 47 ++++++++++++ .../cli/command/container/{ps.go => list.go} | 7 ++ components/cli/command/image/cmd.go | 37 +++++++++ components/cli/command/image/inspect.go | 44 +++++++++++ .../cli/command/image/{images.go => list.go} | 7 ++ components/cli/command/image/remove.go | 7 ++ components/cli/command/stack/cmd.go | 4 +- 9 files changed, 242 insertions(+), 35 deletions(-) create mode 100644 components/cli/command/container/cmd.go create mode 100644 components/cli/command/container/inspect.go rename components/cli/command/container/{ps.go => list.go} (94%) create mode 100644 components/cli/command/image/cmd.go create mode 100644 components/cli/command/image/inspect.go rename components/cli/command/image/{images.go => list.go} (91%) diff --git a/components/cli/command/commands/commands.go b/components/cli/command/commands/commands.go index 0adf8e3f3e..95615fe0b5 100644 --- a/components/cli/command/commands/commands.go +++ b/components/cli/command/commands/commands.go @@ -25,49 +25,58 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) { stack.NewStackCommand(dockerCli), stack.NewTopLevelDeployCommand(dockerCli), swarm.NewSwarmCommand(dockerCli), - container.NewAttachCommand(dockerCli), - container.NewCommitCommand(dockerCli), - container.NewCopyCommand(dockerCli), - container.NewCreateCommand(dockerCli), - container.NewDiffCommand(dockerCli), - container.NewExecCommand(dockerCli), - container.NewExportCommand(dockerCli), - container.NewKillCommand(dockerCli), - container.NewLogsCommand(dockerCli), - container.NewPauseCommand(dockerCli), - container.NewPortCommand(dockerCli), - container.NewPsCommand(dockerCli), - container.NewRenameCommand(dockerCli), - container.NewRestartCommand(dockerCli), - container.NewRmCommand(dockerCli), + container.NewContainerCommand(dockerCli), + image.NewImageCommand(dockerCli), container.NewRunCommand(dockerCli), - container.NewStartCommand(dockerCli), - container.NewStatsCommand(dockerCli), - container.NewStopCommand(dockerCli), - container.NewTopCommand(dockerCli), - container.NewUnpauseCommand(dockerCli), - container.NewUpdateCommand(dockerCli), - container.NewWaitCommand(dockerCli), image.NewBuildCommand(dockerCli), - image.NewHistoryCommand(dockerCli), - image.NewImagesCommand(dockerCli), - image.NewLoadCommand(dockerCli), - image.NewRemoveCommand(dockerCli), - image.NewSaveCommand(dockerCli), - image.NewPullCommand(dockerCli), - image.NewPushCommand(dockerCli), - image.NewSearchCommand(dockerCli), - image.NewImportCommand(dockerCli), - image.NewTagCommand(dockerCli), network.NewNetworkCommand(dockerCli), system.NewEventsCommand(dockerCli), - system.NewInspectCommand(dockerCli), registry.NewLoginCommand(dockerCli), registry.NewLogoutCommand(dockerCli), system.NewVersionCommand(dockerCli), volume.NewVolumeCommand(dockerCli), system.NewInfoCommand(dockerCli), + hide(container.NewAttachCommand(dockerCli)), + hide(container.NewCommitCommand(dockerCli)), + hide(container.NewCopyCommand(dockerCli)), + hide(container.NewCreateCommand(dockerCli)), + hide(container.NewDiffCommand(dockerCli)), + hide(container.NewExecCommand(dockerCli)), + hide(container.NewExportCommand(dockerCli)), + hide(container.NewKillCommand(dockerCli)), + hide(container.NewLogsCommand(dockerCli)), + hide(container.NewPauseCommand(dockerCli)), + hide(container.NewPortCommand(dockerCli)), + hide(container.NewPsCommand(dockerCli)), + hide(container.NewRenameCommand(dockerCli)), + hide(container.NewRestartCommand(dockerCli)), + hide(container.NewRmCommand(dockerCli)), + hide(container.NewStartCommand(dockerCli)), + hide(container.NewStatsCommand(dockerCli)), + hide(container.NewStopCommand(dockerCli)), + hide(container.NewTopCommand(dockerCli)), + hide(container.NewUnpauseCommand(dockerCli)), + hide(container.NewUpdateCommand(dockerCli)), + hide(container.NewWaitCommand(dockerCli)), + hide(image.NewHistoryCommand(dockerCli)), + hide(image.NewImagesCommand(dockerCli)), + hide(image.NewImportCommand(dockerCli)), + hide(image.NewLoadCommand(dockerCli)), + hide(image.NewPullCommand(dockerCli)), + hide(image.NewPushCommand(dockerCli)), + hide(image.NewRemoveCommand(dockerCli)), + hide(image.NewSaveCommand(dockerCli)), + hide(image.NewSearchCommand(dockerCli)), + hide(image.NewTagCommand(dockerCli)), + hide(system.NewInspectCommand(dockerCli)), ) checkpoint.NewCheckpointCommand(cmd, dockerCli) plugin.NewPluginCommand(cmd, dockerCli) } + +func hide(cmd *cobra.Command) *cobra.Command { + cmdCopy := *cmd + cmdCopy.Hidden = true + cmdCopy.Aliases = []string{} + return &cmdCopy +} diff --git a/components/cli/command/container/cmd.go b/components/cli/command/container/cmd.go new file mode 100644 index 0000000000..6d71e53eba --- /dev/null +++ b/components/cli/command/container/cmd.go @@ -0,0 +1,49 @@ +package container + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" +) + +// NewContainerCommand returns a cobra command for `container` subcommands +func NewContainerCommand(dockerCli *command.DockerCli) *cobra.Command { + cmd := &cobra.Command{ + Use: "container", + Short: "Manage Docker containers", + Args: cli.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + }, + } + cmd.AddCommand( + NewAttachCommand(dockerCli), + NewCommitCommand(dockerCli), + NewCopyCommand(dockerCli), + NewCreateCommand(dockerCli), + NewDiffCommand(dockerCli), + NewExecCommand(dockerCli), + NewExportCommand(dockerCli), + NewKillCommand(dockerCli), + NewLogsCommand(dockerCli), + NewPauseCommand(dockerCli), + NewPortCommand(dockerCli), + NewRenameCommand(dockerCli), + NewRestartCommand(dockerCli), + NewRmCommand(dockerCli), + NewRunCommand(dockerCli), + NewStartCommand(dockerCli), + NewStatsCommand(dockerCli), + NewStopCommand(dockerCli), + NewTopCommand(dockerCli), + NewUnpauseCommand(dockerCli), + NewUpdateCommand(dockerCli), + NewWaitCommand(dockerCli), + newListCommand(dockerCli), + newInspectCommand(dockerCli), + ) + return cmd +} diff --git a/components/cli/command/container/inspect.go b/components/cli/command/container/inspect.go new file mode 100644 index 0000000000..0bef51a61d --- /dev/null +++ b/components/cli/command/container/inspect.go @@ -0,0 +1,47 @@ +package container + +import ( + "golang.org/x/net/context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/inspect" + "github.com/spf13/cobra" +) + +type inspectOptions struct { + format string + size bool + refs []string +} + +// newInspectCommand creates a new cobra.Command for `docker container inspect` +func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts inspectOptions + + cmd := &cobra.Command{ + Use: "inspect [OPTIONS] CONTAINER [CONTAINER...]", + Short: "Display detailed information on one or more containers", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.refs = args + return runInspect(dockerCli, opts) + }, + } + + flags := cmd.Flags() + flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template") + flags.BoolVarP(&opts.size, "size", "s", false, "Display total file sizes") + + return cmd +} + +func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { + client := dockerCli.Client() + ctx := context.Background() + + getRefFunc := func(ref string) (interface{}, []byte, error) { + return client.ContainerInspectWithRaw(ctx, ref, opts.size) + } + return inspect.Inspect(dockerCli.Out(), opts.refs, opts.format, getRefFunc) +} diff --git a/components/cli/command/container/ps.go b/components/cli/command/container/list.go similarity index 94% rename from components/cli/command/container/ps.go rename to components/cli/command/container/list.go index b5a3be06e9..7f10ce8bd1 100644 --- a/components/cli/command/container/ps.go +++ b/components/cli/command/container/list.go @@ -52,6 +52,13 @@ func NewPsCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } +func newListCommand(dockerCli *command.DockerCli) *cobra.Command { + cmd := *NewPsCommand(dockerCli) + cmd.Aliases = []string{"ps", "list"} + cmd.Use = "ls [OPTIONS]" + return &cmd +} + type preProcessor struct { types.Container opts *types.ContainerListOptions diff --git a/components/cli/command/image/cmd.go b/components/cli/command/image/cmd.go new file mode 100644 index 0000000000..e04c4c23f3 --- /dev/null +++ b/components/cli/command/image/cmd.go @@ -0,0 +1,37 @@ +package image + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" +) + +// NewImageCommand returns a cobra command for `image` subcommands +func NewImageCommand(dockerCli *command.DockerCli) *cobra.Command { + cmd := &cobra.Command{ + Use: "image", + Short: "Manage Docker images", + Args: cli.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + }, + } + cmd.AddCommand( + NewBuildCommand(dockerCli), + NewHistoryCommand(dockerCli), + NewImportCommand(dockerCli), + NewLoadCommand(dockerCli), + NewPullCommand(dockerCli), + NewPushCommand(dockerCli), + NewSaveCommand(dockerCli), + NewSearchCommand(dockerCli), + NewTagCommand(dockerCli), + newListCommand(dockerCli), + newRemoveCommand(dockerCli), + newInspectCommand(dockerCli), + ) + return cmd +} diff --git a/components/cli/command/image/inspect.go b/components/cli/command/image/inspect.go new file mode 100644 index 0000000000..11c528ef2a --- /dev/null +++ b/components/cli/command/image/inspect.go @@ -0,0 +1,44 @@ +package image + +import ( + "golang.org/x/net/context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/inspect" + "github.com/spf13/cobra" +) + +type inspectOptions struct { + format string + refs []string +} + +// newInspectCommand creates a new cobra.Command for `docker image inspect` +func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts inspectOptions + + cmd := &cobra.Command{ + Use: "inspect [OPTIONS] IMAGE [IMAGE...]", + Short: "Display detailed information on one or more images", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.refs = args + return runInspect(dockerCli, opts) + }, + } + + flags := cmd.Flags() + flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template") + return cmd +} + +func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { + client := dockerCli.Client() + ctx := context.Background() + + getRefFunc := func(ref string) (interface{}, []byte, error) { + return client.ImageInspectWithRaw(ctx, ref) + } + return inspect.Inspect(dockerCli.Out(), opts.refs, opts.format, getRefFunc) +} diff --git a/components/cli/command/image/images.go b/components/cli/command/image/list.go similarity index 91% rename from components/cli/command/image/images.go rename to components/cli/command/image/list.go index 0229734ce4..587869fdf1 100644 --- a/components/cli/command/image/images.go +++ b/components/cli/command/image/list.go @@ -50,6 +50,13 @@ func NewImagesCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } +func newListCommand(dockerCli *command.DockerCli) *cobra.Command { + cmd := *NewImagesCommand(dockerCli) + cmd.Aliases = []string{"images", "list"} + cmd.Use = "ls [OPTIONS] [REPOSITORY[:TAG]]" + return &cmd +} + func runImages(dockerCli *command.DockerCli, opts imagesOptions) error { ctx := context.Background() diff --git a/components/cli/command/image/remove.go b/components/cli/command/image/remove.go index 51a7b21642..c79ceba7a8 100644 --- a/components/cli/command/image/remove.go +++ b/components/cli/command/image/remove.go @@ -38,6 +38,13 @@ func NewRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } +func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { + cmd := *NewRemoveCommand(dockerCli) + cmd.Aliases = []string{"rmi", "remove"} + cmd.Use = "rm [OPTIONS] IMAGE [IMAGE...]" + return &cmd +} + func runRemove(dockerCli *command.DockerCli, opts removeOptions, images []string) error { client := dockerCli.Client() ctx := context.Background() diff --git a/components/cli/command/stack/cmd.go b/components/cli/command/stack/cmd.go index d459e0a9a1..22f076c4d0 100644 --- a/components/cli/command/stack/cmd.go +++ b/components/cli/command/stack/cmd.go @@ -32,8 +32,8 @@ func NewStackCommand(dockerCli *command.DockerCli) *cobra.Command { // NewTopLevelDeployCommand returns a command for `docker deploy` func NewTopLevelDeployCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := newDeployCommand(dockerCli) + cmd := *newDeployCommand(dockerCli) // Remove the aliases at the top level cmd.Aliases = []string{} - return cmd + return &cmd } From 93bceb1528a0c7041110e3e835a31350dc0ad063 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 29 Aug 2016 09:59:41 -0400 Subject: [PATCH 145/978] Move the search command to the registry package. And move it back to the top-level command. Signed-off-by: Daniel Nephin Upstream-commit: 68b7f55a456b4970850c72a8b9d3381950bf5e20 Component: cli --- components/cli/command/commands/commands.go | 2 +- components/cli/command/image/cmd.go | 1 - components/cli/command/{image => registry}/search.go | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) rename components/cli/command/{image => registry}/search.go (99%) diff --git a/components/cli/command/commands/commands.go b/components/cli/command/commands/commands.go index 95615fe0b5..cace1b152c 100644 --- a/components/cli/command/commands/commands.go +++ b/components/cli/command/commands/commands.go @@ -33,6 +33,7 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) { system.NewEventsCommand(dockerCli), registry.NewLoginCommand(dockerCli), registry.NewLogoutCommand(dockerCli), + registry.NewSearchCommand(dockerCli), system.NewVersionCommand(dockerCli), volume.NewVolumeCommand(dockerCli), system.NewInfoCommand(dockerCli), @@ -66,7 +67,6 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) { hide(image.NewPushCommand(dockerCli)), hide(image.NewRemoveCommand(dockerCli)), hide(image.NewSaveCommand(dockerCli)), - hide(image.NewSearchCommand(dockerCli)), hide(image.NewTagCommand(dockerCli)), hide(system.NewInspectCommand(dockerCli)), ) diff --git a/components/cli/command/image/cmd.go b/components/cli/command/image/cmd.go index e04c4c23f3..4a7d2b3fd9 100644 --- a/components/cli/command/image/cmd.go +++ b/components/cli/command/image/cmd.go @@ -27,7 +27,6 @@ func NewImageCommand(dockerCli *command.DockerCli) *cobra.Command { NewPullCommand(dockerCli), NewPushCommand(dockerCli), NewSaveCommand(dockerCli), - NewSearchCommand(dockerCli), NewTagCommand(dockerCli), newListCommand(dockerCli), newRemoveCommand(dockerCli), diff --git a/components/cli/command/image/search.go b/components/cli/command/registry/search.go similarity index 99% rename from components/cli/command/image/search.go rename to components/cli/command/registry/search.go index 93db7006ac..124b4ae6cc 100644 --- a/components/cli/command/image/search.go +++ b/components/cli/command/registry/search.go @@ -1,4 +1,4 @@ -package image +package registry import ( "fmt" From a765266b0ae0b45df7e1a4fe9c097e389ae53c1a Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 12 Sep 2016 11:37:00 -0400 Subject: [PATCH 146/978] Only hide commands if the env variable is set. Better formatting for usage template. Group commands in usage to management/operation commands. Remove the word Docker from the description of management commands. Signed-off-by: Daniel Nephin Upstream-commit: 1f0f7ecb5a943602aa582ab5a7ae05214b995f85 Component: cli --- components/cli/cobra.go | 79 +++++++++++++++++-- .../command/checkpoint/cmd_experimental.go | 2 +- components/cli/command/commands/commands.go | 5 ++ components/cli/command/container/cmd.go | 2 +- components/cli/command/image/cmd.go | 2 +- components/cli/command/network/cmd.go | 2 +- components/cli/command/node/cmd.go | 2 +- .../cli/command/plugin/cmd_experimental.go | 2 +- components/cli/command/service/cmd.go | 2 +- components/cli/command/stack/cmd.go | 2 +- components/cli/command/swarm/cmd.go | 2 +- components/cli/command/volume/cmd.go | 2 +- 12 files changed, 86 insertions(+), 18 deletions(-) diff --git a/components/cli/cobra.go b/components/cli/cobra.go index 836196d76e..324c0d7b2d 100644 --- a/components/cli/cobra.go +++ b/components/cli/cobra.go @@ -9,6 +9,11 @@ import ( // SetupRootCommand sets default usage, help, and error handling for the // root command. func SetupRootCommand(rootCmd *cobra.Command) { + cobra.AddTemplateFunc("hasSubCommands", hasSubCommands) + cobra.AddTemplateFunc("hasManagementSubCommands", hasManagementSubCommands) + cobra.AddTemplateFunc("operationSubCommands", operationSubCommands) + cobra.AddTemplateFunc("managementSubCommands", managementSubCommands) + rootCmd.SetUsageTemplate(usageTemplate) rootCmd.SetHelpTemplate(helpTemplate) rootCmd.SetFlagErrorFunc(FlagErrorFunc) @@ -34,23 +39,81 @@ func FlagErrorFunc(cmd *cobra.Command, err error) error { } } -var usageTemplate = `Usage: {{if not .HasSubCommands}}{{.UseLine}}{{end}}{{if .HasSubCommands}}{{ .CommandPath}} COMMAND{{end}} +func hasSubCommands(cmd *cobra.Command) bool { + return len(operationSubCommands(cmd)) > 0 +} -{{ .Short | trim }}{{if gt .Aliases 0}} +func hasManagementSubCommands(cmd *cobra.Command) bool { + return len(managementSubCommands(cmd)) > 0 +} + +func operationSubCommands(cmd *cobra.Command) []*cobra.Command { + cmds := []*cobra.Command{} + for _, sub := range cmd.Commands() { + if sub.IsAvailableCommand() && !sub.HasSubCommands() { + cmds = append(cmds, sub) + } + } + return cmds +} + +func managementSubCommands(cmd *cobra.Command) []*cobra.Command { + cmds := []*cobra.Command{} + for _, sub := range cmd.Commands() { + if sub.IsAvailableCommand() && sub.HasSubCommands() { + cmds = append(cmds, sub) + } + } + return cmds +} + +var usageTemplate = `Usage: + +{{- if not .HasSubCommands}} {{.UseLine}}{{end}} +{{- if .HasSubCommands}} {{ .CommandPath}} COMMAND{{end}} + +{{ .Short | trim }} + +{{- if gt .Aliases 0}} Aliases: - {{.NameAndAliases}}{{end}}{{if .HasExample}} + {{.NameAndAliases}} + +{{- end}} +{{- if .HasExample}} Examples: -{{ .Example }}{{end}}{{if .HasFlags}} +{{ .Example }} + +{{- end}} +{{- if .HasFlags}} Options: -{{.Flags.FlagUsages | trimRightSpace}}{{end}}{{ if .HasAvailableSubCommands}} +{{.Flags.FlagUsages | trimRightSpace}} -Commands:{{range .Commands}}{{if .IsAvailableCommand}} - {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{ if .HasSubCommands }} +{{- end}} +{{- if hasManagementSubCommands . }} -Run '{{.CommandPath}} COMMAND --help' for more information on a command.{{end}} +Management Commands: + +{{- range managementSubCommands . }} + {{rpad .Name .NamePadding }} {{.Short}} +{{- end}} + +{{- end}} +{{- if hasSubCommands .}} + +Commands: + +{{- range operationSubCommands . }} + {{rpad .Name .NamePadding }} {{.Short}} +{{- end}} +{{- end}} + +{{- if .HasSubCommands }} + +Run '{{.CommandPath}} COMMAND --help' for more information on a command. +{{- end}} ` var helpTemplate = ` diff --git a/components/cli/command/checkpoint/cmd_experimental.go b/components/cli/command/checkpoint/cmd_experimental.go index 7939678cd5..c05d3ded40 100644 --- a/components/cli/command/checkpoint/cmd_experimental.go +++ b/components/cli/command/checkpoint/cmd_experimental.go @@ -15,7 +15,7 @@ import ( func NewCheckpointCommand(rootCmd *cobra.Command, dockerCli *command.DockerCli) { cmd := &cobra.Command{ Use: "checkpoint", - Short: "Manage Container Checkpoints", + Short: "Manage checkpoints", Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) diff --git a/components/cli/command/commands/commands.go b/components/cli/command/commands/commands.go index cace1b152c..d618233997 100644 --- a/components/cli/command/commands/commands.go +++ b/components/cli/command/commands/commands.go @@ -1,6 +1,8 @@ package commands import ( + "os" + "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/checkpoint" "github.com/docker/docker/cli/command/container" @@ -75,6 +77,9 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) { } func hide(cmd *cobra.Command) *cobra.Command { + if os.Getenv("DOCKER_HIDE_LEGACY_COMMANDS") == "" { + return cmd + } cmdCopy := *cmd cmdCopy.Hidden = true cmdCopy.Aliases = []string{} diff --git a/components/cli/command/container/cmd.go b/components/cli/command/container/cmd.go index 6d71e53eba..da9ea6d41d 100644 --- a/components/cli/command/container/cmd.go +++ b/components/cli/command/container/cmd.go @@ -13,7 +13,7 @@ import ( func NewContainerCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := &cobra.Command{ Use: "container", - Short: "Manage Docker containers", + Short: "Manage containers", Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) diff --git a/components/cli/command/image/cmd.go b/components/cli/command/image/cmd.go index 4a7d2b3fd9..f60ffeeb8f 100644 --- a/components/cli/command/image/cmd.go +++ b/components/cli/command/image/cmd.go @@ -13,7 +13,7 @@ import ( func NewImageCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := &cobra.Command{ Use: "image", - Short: "Manage Docker images", + Short: "Manage images", Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) diff --git a/components/cli/command/network/cmd.go b/components/cli/command/network/cmd.go index a7c9b3fce3..b33f98cd30 100644 --- a/components/cli/command/network/cmd.go +++ b/components/cli/command/network/cmd.go @@ -13,7 +13,7 @@ import ( func NewNetworkCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := &cobra.Command{ Use: "network", - Short: "Manage Docker networks", + Short: "Manage networks", Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) diff --git a/components/cli/command/node/cmd.go b/components/cli/command/node/cmd.go index 6aa4dfcb18..c7d0cf8181 100644 --- a/components/cli/command/node/cmd.go +++ b/components/cli/command/node/cmd.go @@ -14,7 +14,7 @@ import ( func NewNodeCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := &cobra.Command{ Use: "node", - Short: "Manage Docker Swarm nodes", + Short: "Manage Swarm nodes", Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) diff --git a/components/cli/command/plugin/cmd_experimental.go b/components/cli/command/plugin/cmd_experimental.go index cc779143fa..33c1c93acb 100644 --- a/components/cli/command/plugin/cmd_experimental.go +++ b/components/cli/command/plugin/cmd_experimental.go @@ -14,7 +14,7 @@ import ( func NewPluginCommand(rootCmd *cobra.Command, dockerCli *command.DockerCli) { cmd := &cobra.Command{ Use: "plugin", - Short: "Manage Docker plugins", + Short: "Manage plugins", Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) diff --git a/components/cli/command/service/cmd.go b/components/cli/command/service/cmd.go index 282ce2b4b9..9f342e1342 100644 --- a/components/cli/command/service/cmd.go +++ b/components/cli/command/service/cmd.go @@ -13,7 +13,7 @@ import ( func NewServiceCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := &cobra.Command{ Use: "service", - Short: "Manage Docker services", + Short: "Manage services", Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) diff --git a/components/cli/command/stack/cmd.go b/components/cli/command/stack/cmd.go index 22f076c4d0..22a2334419 100644 --- a/components/cli/command/stack/cmd.go +++ b/components/cli/command/stack/cmd.go @@ -14,7 +14,7 @@ import ( func NewStackCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := &cobra.Command{ Use: "stack", - Short: "Manage Docker stacks", + Short: "Manage stacks", Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) diff --git a/components/cli/command/swarm/cmd.go b/components/cli/command/swarm/cmd.go index db2b6a2530..9f9df53950 100644 --- a/components/cli/command/swarm/cmd.go +++ b/components/cli/command/swarm/cmd.go @@ -13,7 +13,7 @@ import ( func NewSwarmCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := &cobra.Command{ Use: "swarm", - Short: "Manage Docker Swarm", + Short: "Manage Swarm", Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) diff --git a/components/cli/command/volume/cmd.go b/components/cli/command/volume/cmd.go index 090a006439..caf6afcaa3 100644 --- a/components/cli/command/volume/cmd.go +++ b/components/cli/command/volume/cmd.go @@ -13,7 +13,7 @@ import ( func NewVolumeCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := &cobra.Command{ Use: "volume COMMAND", - Short: "Manage Docker volumes", + Short: "Manage volumes", Long: volumeDescription, Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { From 16523320264099437b939608e932857e599b0931 Mon Sep 17 00:00:00 2001 From: John Howard Date: Wed, 14 Sep 2016 11:55:07 -0700 Subject: [PATCH 147/978] Windows: OCI process struct convergence Signed-off-by: John Howard Upstream-commit: f14f7711e7753a60cab182674e4ea064be5f9159 Component: cli --- components/cli/command/container/run.go | 2 +- components/cli/command/container/tty.go | 2 +- components/cli/command/out.go | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/components/cli/command/container/run.go b/components/cli/command/container/run.go index d36ab610cf..a167e78f9a 100644 --- a/components/cli/command/container/run.go +++ b/components/cli/command/container/run.go @@ -135,7 +135,7 @@ func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions // a far better user experience rather than relying on subsequent resizes // to cause things to catch up. if runtime.GOOS == "windows" { - hostConfig.ConsoleSize[0], hostConfig.ConsoleSize[1] = dockerCli.Out().GetTtySize() + hostConfig.ConsoleSize.Height, hostConfig.ConsoleSize.Width = dockerCli.Out().GetTtySize() } ctx, cancelFun := context.WithCancel(context.Background()) diff --git a/components/cli/command/container/tty.go b/components/cli/command/container/tty.go index edb11592d3..6af8e2becf 100644 --- a/components/cli/command/container/tty.go +++ b/components/cli/command/container/tty.go @@ -16,7 +16,7 @@ import ( ) // resizeTtyTo resizes tty to specific height and width -func resizeTtyTo(ctx context.Context, client client.ContainerAPIClient, id string, height, width int, isExec bool) { +func resizeTtyTo(ctx context.Context, client client.ContainerAPIClient, id string, height, width uint, isExec bool) { if height == 0 && width == 0 { return } diff --git a/components/cli/command/out.go b/components/cli/command/out.go index 09375d07d7..85718d7acd 100644 --- a/components/cli/command/out.go +++ b/components/cli/command/out.go @@ -48,7 +48,7 @@ func (o *OutStream) RestoreTerminal() { } // GetTtySize returns the height and width in characters of the tty -func (o *OutStream) GetTtySize() (int, int) { +func (o *OutStream) GetTtySize() (uint, uint) { if !o.isTerminal { return 0, 0 } @@ -59,7 +59,7 @@ func (o *OutStream) GetTtySize() (int, int) { return 0, 0 } } - return int(ws.Height), int(ws.Width) + return uint(ws.Height), uint(ws.Width) } // NewOutStream returns a new OutStream object from a Writer From 9f2c286fe015e6e5f001840e939ae99d6eea7d90 Mon Sep 17 00:00:00 2001 From: John Howard Date: Wed, 14 Sep 2016 11:55:07 -0700 Subject: [PATCH 148/978] Windows: OCI process struct convergence Signed-off-by: John Howard Upstream-commit: 6be7efbe303c9bdc87566807703dbb31d0b0deee Component: cli --- components/cli/container_resize.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/cli/container_resize.go b/components/cli/container_resize.go index a7f38b024b..66c3cc1940 100644 --- a/components/cli/container_resize.go +++ b/components/cli/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 0d757a87e7cdae79f404a420da50acc9bed678d6 Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Thu, 8 Sep 2016 20:44:25 -0700 Subject: [PATCH 149/978] 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 Upstream-commit: c648e163ebbe3bc5bf9b10b9390e41c0785a409b Component: cli --- components/cli/checkpoint_create_test.go | 4 +- components/cli/checkpoint_delete_test.go | 4 +- components/cli/checkpoint_list_test.go | 4 +- components/cli/client.go | 20 +-- components/cli/client_mock_test.go | 37 +----- components/cli/client_test.go | 2 +- components/cli/container_commit_test.go | 4 +- components/cli/container_copy_test.go | 20 +-- components/cli/container_create_test.go | 8 +- components/cli/container_diff_test.go | 4 +- components/cli/container_exec_test.go | 12 +- components/cli/container_export_test.go | 4 +- components/cli/container_inspect_test.go | 8 +- components/cli/container_kill_test.go | 4 +- components/cli/container_list_test.go | 4 +- components/cli/container_logs_test.go | 4 +- components/cli/container_pause_test.go | 4 +- components/cli/container_remove_test.go | 4 +- components/cli/container_rename_test.go | 4 +- components/cli/container_resize_test.go | 8 +- components/cli/container_restart_test.go | 4 +- components/cli/container_start_test.go | 4 +- components/cli/container_stats_test.go | 4 +- components/cli/container_stop_test.go | 4 +- components/cli/container_top_test.go | 4 +- components/cli/container_unpause_test.go | 4 +- components/cli/container_update_test.go | 4 +- components/cli/container_wait_test.go | 4 +- components/cli/events_test.go | 6 +- components/cli/hijack.go | 7 +- components/cli/image_build_test.go | 4 +- components/cli/image_create_test.go | 4 +- components/cli/image_history_test.go | 4 +- components/cli/image_import_test.go | 4 +- components/cli/image_inspect_test.go | 6 +- components/cli/image_list_test.go | 4 +- components/cli/image_load_test.go | 4 +- components/cli/image_pull_test.go | 14 +-- components/cli/image_push_test.go | 14 +-- components/cli/image_remove_test.go | 4 +- components/cli/image_save_test.go | 4 +- components/cli/image_search_test.go | 12 +- components/cli/image_tag_test.go | 6 +- components/cli/info_test.go | 6 +- components/cli/network_connect_test.go | 6 +- components/cli/network_create_test.go | 4 +- components/cli/network_disconnect_test.go | 4 +- components/cli/network_inspect_test.go | 6 +- components/cli/network_list_test.go | 4 +- components/cli/network_remove_test.go | 4 +- components/cli/node_inspect_test.go | 6 +- components/cli/node_list_test.go | 4 +- components/cli/node_remove_test.go | 4 +- components/cli/node_update_test.go | 4 +- components/cli/plugin_disable_test.go | 4 +- components/cli/plugin_enable_test.go | 4 +- components/cli/plugin_inspect_test.go | 4 +- components/cli/plugin_list_test.go | 4 +- components/cli/plugin_push_test.go | 4 +- components/cli/plugin_remove_test.go | 4 +- components/cli/plugin_set_test.go | 4 +- components/cli/request.go | 17 ++- components/cli/request_test.go | 5 +- components/cli/service_create_test.go | 4 +- components/cli/service_inspect_test.go | 6 +- components/cli/service_list_test.go | 4 +- components/cli/service_remove_test.go | 4 +- components/cli/service_update_test.go | 4 +- components/cli/swarm_init_test.go | 4 +- components/cli/swarm_inspect_test.go | 4 +- components/cli/swarm_join_test.go | 4 +- components/cli/swarm_leave_test.go | 4 +- components/cli/swarm_update_test.go | 4 +- components/cli/task_inspect_test.go | 4 +- components/cli/task_list_test.go | 4 +- components/cli/transport.go | 51 ++++++++ components/cli/transport/cancellable/LICENSE | 27 ---- .../cli/transport/cancellable/canceler.go | 23 ---- .../transport/cancellable/canceler_go14.go | 27 ---- .../cli/transport/cancellable/cancellable.go | 115 ------------------ components/cli/transport/client.go | 47 ------- components/cli/transport/transport.go | 57 --------- components/cli/volume_create_test.go | 4 +- components/cli/volume_inspect_test.go | 6 +- components/cli/volume_list_test.go | 4 +- components/cli/volume_remove_test.go | 4 +- 86 files changed, 276 insertions(+), 533 deletions(-) create mode 100644 components/cli/transport.go delete mode 100644 components/cli/transport/cancellable/LICENSE delete mode 100644 components/cli/transport/cancellable/canceler.go delete mode 100644 components/cli/transport/cancellable/canceler_go14.go delete mode 100644 components/cli/transport/cancellable/cancellable.go delete mode 100644 components/cli/transport/client.go delete mode 100644 components/cli/transport/transport.go diff --git a/components/cli/checkpoint_create_test.go b/components/cli/checkpoint_create_test.go index e2ae36e1e0..96e5187618 100644 --- a/components/cli/checkpoint_create_test.go +++ b/components/cli/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/components/cli/checkpoint_delete_test.go b/components/cli/checkpoint_delete_test.go index 097ab37693..23931c6523 100644 --- a/components/cli/checkpoint_delete_test.go +++ b/components/cli/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/components/cli/checkpoint_list_test.go b/components/cli/checkpoint_list_test.go index 5960436eb1..e636995bc1 100644 --- a/components/cli/checkpoint_list_test.go +++ b/components/cli/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/components/cli/client.go b/components/cli/client.go index 6a85121c6d..deccb4ab74 100644 --- a/components/cli/client.go +++ b/components/cli/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/components/cli/client_mock_test.go b/components/cli/client_mock_test.go index 33c247266c..0ab935d536 100644 --- a/components/cli/client_mock_test.go +++ b/components/cli/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/components/cli/client_test.go b/components/cli/client_test.go index 60af3db029..60e44dc299 100644 --- a/components/cli/client_test.go +++ b/components/cli/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/components/cli/container_commit_test.go b/components/cli/container_commit_test.go index 3fc3e5cfd0..8f1b58be81 100644 --- a/components/cli/container_commit_test.go +++ b/components/cli/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/components/cli/container_copy_test.go b/components/cli/container_copy_test.go index 39cd05ac2d..7eded611fd 100644 --- a/components/cli/container_copy_test.go +++ b/components/cli/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/components/cli/container_create_test.go b/components/cli/container_create_test.go index 4c14cdc5d1..5325156beb 100644 --- a/components/cli/container_create_test.go +++ b/components/cli/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/components/cli/container_diff_test.go b/components/cli/container_diff_test.go index 03ea3354d2..1ce1117684 100644 --- a/components/cli/container_diff_test.go +++ b/components/cli/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/components/cli/container_exec_test.go b/components/cli/container_exec_test.go index abe824e47b..42146ae8a5 100644 --- a/components/cli/container_exec_test.go +++ b/components/cli/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/components/cli/container_export_test.go b/components/cli/container_export_test.go index 10eba33d2f..5849fe9252 100644 --- a/components/cli/container_export_test.go +++ b/components/cli/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/components/cli/container_inspect_test.go b/components/cli/container_inspect_test.go index 0dc8ac3753..f1a6f4ac7d 100644 --- a/components/cli/container_inspect_test.go +++ b/components/cli/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/components/cli/container_kill_test.go b/components/cli/container_kill_test.go index a34a7b5b11..9477b0abd2 100644 --- a/components/cli/container_kill_test.go +++ b/components/cli/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/components/cli/container_list_test.go b/components/cli/container_list_test.go index 3aa2101f27..5068b7573e 100644 --- a/components/cli/container_list_test.go +++ b/components/cli/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/components/cli/container_logs_test.go b/components/cli/container_logs_test.go index d7f0adc9c0..99e31842c9 100644 --- a/components/cli/container_logs_test.go +++ b/components/cli/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/components/cli/container_pause_test.go b/components/cli/container_pause_test.go index ebd12a6ac7..0ee2f05d7e 100644 --- a/components/cli/container_pause_test.go +++ b/components/cli/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/components/cli/container_remove_test.go b/components/cli/container_remove_test.go index 6e135d6ef2..798c08b333 100644 --- a/components/cli/container_remove_test.go +++ b/components/cli/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/components/cli/container_rename_test.go b/components/cli/container_rename_test.go index 9344bab7db..732ebff5f7 100644 --- a/components/cli/container_rename_test.go +++ b/components/cli/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/components/cli/container_resize_test.go b/components/cli/container_resize_test.go index e0056c88d1..5b2efecdce 100644 --- a/components/cli/container_resize_test.go +++ b/components/cli/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/components/cli/container_restart_test.go b/components/cli/container_restart_test.go index 080656d368..8c3cfd6a6f 100644 --- a/components/cli/container_restart_test.go +++ b/components/cli/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/components/cli/container_start_test.go b/components/cli/container_start_test.go index 79f85b332a..5826fa8bc7 100644 --- a/components/cli/container_start_test.go +++ b/components/cli/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/components/cli/container_stats_test.go b/components/cli/container_stats_test.go index 22ecd6170f..76e4a09ddf 100644 --- a/components/cli/container_stats_test.go +++ b/components/cli/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/components/cli/container_stop_test.go b/components/cli/container_stop_test.go index 4b052f9908..c32cd691c4 100644 --- a/components/cli/container_stop_test.go +++ b/components/cli/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/components/cli/container_top_test.go b/components/cli/container_top_test.go index 4df7d82d84..7802be063e 100644 --- a/components/cli/container_top_test.go +++ b/components/cli/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/components/cli/container_unpause_test.go b/components/cli/container_unpause_test.go index a5b21bf56c..2c42727191 100644 --- a/components/cli/container_unpause_test.go +++ b/components/cli/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/components/cli/container_update_test.go b/components/cli/container_update_test.go index 46e34d6936..e151637a2b 100644 --- a/components/cli/container_update_test.go +++ b/components/cli/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/components/cli/container_wait_test.go b/components/cli/container_wait_test.go index bf2ba6b925..dab5acbdd3 100644 --- a/components/cli/container_wait_test.go +++ b/components/cli/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/components/cli/events_test.go b/components/cli/events_test.go index f7cb33f611..48b948fa37 100644 --- a/components/cli/events_test.go +++ b/components/cli/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/components/cli/hijack.go b/components/cli/hijack.go index e3f63e20c2..f3461ecf78 100644 --- a/components/cli/hijack.go +++ b/components/cli/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/components/cli/image_build_test.go b/components/cli/image_build_test.go index 8261c54854..ec0cbe2ee4 100644 --- a/components/cli/image_build_test.go +++ b/components/cli/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/components/cli/image_create_test.go b/components/cli/image_create_test.go index a2e001be5d..5c2edd2ad5 100644 --- a/components/cli/image_create_test.go +++ b/components/cli/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/components/cli/image_history_test.go b/components/cli/image_history_test.go index c9516151b7..729edb1ad5 100644 --- a/components/cli/image_history_test.go +++ b/components/cli/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/components/cli/image_import_test.go b/components/cli/image_import_test.go index b64ca74d7b..e309be74e6 100644 --- a/components/cli/image_import_test.go +++ b/components/cli/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/components/cli/image_inspect_test.go b/components/cli/image_inspect_test.go index 5c7ca2721f..74a4e49805 100644 --- a/components/cli/image_inspect_test.go +++ b/components/cli/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/components/cli/image_list_test.go b/components/cli/image_list_test.go index 99ed1964a2..2a52279081 100644 --- a/components/cli/image_list_test.go +++ b/components/cli/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/components/cli/image_load_test.go b/components/cli/image_load_test.go index 0ee7cf35a6..68dc14ff22 100644 --- a/components/cli/image_load_test.go +++ b/components/cli/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/components/cli/image_pull_test.go b/components/cli/image_pull_test.go index c33a6dcc8a..fe6bafed97 100644 --- a/components/cli/image_pull_test.go +++ b/components/cli/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/components/cli/image_push_test.go b/components/cli/image_push_test.go index d32f3ef3c7..b52da8b8dc 100644 --- a/components/cli/image_push_test.go +++ b/components/cli/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/components/cli/image_remove_test.go b/components/cli/image_remove_test.go index 696d06729d..7b004f70e6 100644 --- a/components/cli/image_remove_test.go +++ b/components/cli/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/components/cli/image_save_test.go b/components/cli/image_save_test.go index 8ee40c43ae..8f0cf88640 100644 --- a/components/cli/image_save_test.go +++ b/components/cli/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/components/cli/image_search_test.go b/components/cli/image_search_test.go index 2f21b2cc14..e46d86437f 100644 --- a/components/cli/image_search_test.go +++ b/components/cli/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/components/cli/image_tag_test.go b/components/cli/image_tag_test.go index f3571dfdd3..7925db9f1b 100644 --- a/components/cli/image_tag_test.go +++ b/components/cli/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/components/cli/info_test.go b/components/cli/info_test.go index 9d51b1a78b..79f23c8af2 100644 --- a/components/cli/info_test.go +++ b/components/cli/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/components/cli/network_connect_test.go b/components/cli/network_connect_test.go index 95b149e685..d472f4520c 100644 --- a/components/cli/network_connect_test.go +++ b/components/cli/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/components/cli/network_create_test.go b/components/cli/network_create_test.go index 611ed8173e..0e2457f89c 100644 --- a/components/cli/network_create_test.go +++ b/components/cli/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/components/cli/network_disconnect_test.go b/components/cli/network_disconnect_test.go index d9dbb67159..b54a2b1ccf 100644 --- a/components/cli/network_disconnect_test.go +++ b/components/cli/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/components/cli/network_inspect_test.go b/components/cli/network_inspect_test.go index a6eb626c67..1f926d66ba 100644 --- a/components/cli/network_inspect_test.go +++ b/components/cli/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/components/cli/network_list_test.go b/components/cli/network_list_test.go index cb66139271..4d443496ac 100644 --- a/components/cli/network_list_test.go +++ b/components/cli/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/components/cli/network_remove_test.go b/components/cli/network_remove_test.go index d8cfa0ed6e..2a7b9640c1 100644 --- a/components/cli/network_remove_test.go +++ b/components/cli/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/components/cli/node_inspect_test.go b/components/cli/node_inspect_test.go index bf67728311..fc13283084 100644 --- a/components/cli/node_inspect_test.go +++ b/components/cli/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/components/cli/node_list_test.go b/components/cli/node_list_test.go index 899ac7f455..1b3b35f357 100644 --- a/components/cli/node_list_test.go +++ b/components/cli/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/components/cli/node_remove_test.go b/components/cli/node_remove_test.go index 9fdf2d7eb3..f2f8adc4a3 100644 --- a/components/cli/node_remove_test.go +++ b/components/cli/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/components/cli/node_update_test.go b/components/cli/node_update_test.go index 1acf65854a..613ff104eb 100644 --- a/components/cli/node_update_test.go +++ b/components/cli/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/components/cli/plugin_disable_test.go b/components/cli/plugin_disable_test.go index f37c157866..7b50b25730 100644 --- a/components/cli/plugin_disable_test.go +++ b/components/cli/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/components/cli/plugin_enable_test.go b/components/cli/plugin_enable_test.go index fc0fe226a9..a2b57be4c2 100644 --- a/components/cli/plugin_enable_test.go +++ b/components/cli/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/components/cli/plugin_inspect_test.go b/components/cli/plugin_inspect_test.go index 19f829b2de..df4ca9c841 100644 --- a/components/cli/plugin_inspect_test.go +++ b/components/cli/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/components/cli/plugin_list_test.go b/components/cli/plugin_list_test.go index 92aee61187..95c51595ca 100644 --- a/components/cli/plugin_list_test.go +++ b/components/cli/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/components/cli/plugin_push_test.go b/components/cli/plugin_push_test.go index b77ea00273..ed685694ec 100644 --- a/components/cli/plugin_push_test.go +++ b/components/cli/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/components/cli/plugin_remove_test.go b/components/cli/plugin_remove_test.go index de565f441b..fc789fd04d 100644 --- a/components/cli/plugin_remove_test.go +++ b/components/cli/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/components/cli/plugin_set_test.go b/components/cli/plugin_set_test.go index 128dee04be..fa1cde044e 100644 --- a/components/cli/plugin_set_test.go +++ b/components/cli/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/components/cli/request.go b/components/cli/request.go index 7b4f5406b8..f5c239bf25 100644 --- a/components/cli/request.go +++ b/components/cli/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/components/cli/request_test.go b/components/cli/request_test.go index 446adf9c66..63908aec4b 100644 --- a/components/cli/request_test.go +++ b/components/cli/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/components/cli/service_create_test.go b/components/cli/service_create_test.go index a79f040c0a..1e07382870 100644 --- a/components/cli/service_create_test.go +++ b/components/cli/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/components/cli/service_inspect_test.go b/components/cli/service_inspect_test.go index e4eafff5d7..e235cf0fef 100644 --- a/components/cli/service_inspect_test.go +++ b/components/cli/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/components/cli/service_list_test.go b/components/cli/service_list_test.go index 6e6851a3a5..728187919f 100644 --- a/components/cli/service_list_test.go +++ b/components/cli/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/components/cli/service_remove_test.go b/components/cli/service_remove_test.go index e1316f959b..8e2ac259c1 100644 --- a/components/cli/service_remove_test.go +++ b/components/cli/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/components/cli/service_update_test.go b/components/cli/service_update_test.go index bd616c09bf..081649f492 100644 --- a/components/cli/service_update_test.go +++ b/components/cli/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/components/cli/swarm_init_test.go b/components/cli/swarm_init_test.go index 077c8c4efb..811155aff4 100644 --- a/components/cli/swarm_init_test.go +++ b/components/cli/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/components/cli/swarm_inspect_test.go b/components/cli/swarm_inspect_test.go index 7143e77181..6432d172b4 100644 --- a/components/cli/swarm_inspect_test.go +++ b/components/cli/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/components/cli/swarm_join_test.go b/components/cli/swarm_join_test.go index 922716d85f..31ef2a76ee 100644 --- a/components/cli/swarm_join_test.go +++ b/components/cli/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/components/cli/swarm_leave_test.go b/components/cli/swarm_leave_test.go index d0bef2b257..c96dac8120 100644 --- a/components/cli/swarm_leave_test.go +++ b/components/cli/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/components/cli/swarm_update_test.go b/components/cli/swarm_update_test.go index ecf1731e5b..3b23db078f 100644 --- a/components/cli/swarm_update_test.go +++ b/components/cli/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/components/cli/task_inspect_test.go b/components/cli/task_inspect_test.go index 2c73b37642..148cdad3a7 100644 --- a/components/cli/task_inspect_test.go +++ b/components/cli/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/components/cli/task_list_test.go b/components/cli/task_list_test.go index b520ab589f..2d9b812bc2 100644 --- a/components/cli/task_list_test.go +++ b/components/cli/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/components/cli/transport.go b/components/cli/transport.go new file mode 100644 index 0000000000..43a667272d --- /dev/null +++ b/components/cli/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/components/cli/transport/cancellable/LICENSE b/components/cli/transport/cancellable/LICENSE deleted file mode 100644 index 6a66aea5ea..0000000000 --- a/components/cli/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/components/cli/transport/cancellable/canceler.go b/components/cli/transport/cancellable/canceler.go deleted file mode 100644 index 62770b777b..0000000000 --- a/components/cli/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/components/cli/transport/cancellable/canceler_go14.go b/components/cli/transport/cancellable/canceler_go14.go deleted file mode 100644 index dd2723d94f..0000000000 --- a/components/cli/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/components/cli/transport/cancellable/cancellable.go b/components/cli/transport/cancellable/cancellable.go deleted file mode 100644 index 1f8eac5c1c..0000000000 --- a/components/cli/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/components/cli/transport/client.go b/components/cli/transport/client.go deleted file mode 100644 index 13d4b3ab3d..0000000000 --- a/components/cli/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/components/cli/transport/transport.go b/components/cli/transport/transport.go deleted file mode 100644 index ff28af1855..0000000000 --- a/components/cli/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/components/cli/volume_create_test.go b/components/cli/volume_create_test.go index d3cfa7132f..75085296cc 100644 --- a/components/cli/volume_create_test.go +++ b/components/cli/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/components/cli/volume_inspect_test.go b/components/cli/volume_inspect_test.go index 4b9f47358d..0d1d118828 100644 --- a/components/cli/volume_inspect_test.go +++ b/components/cli/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/components/cli/volume_list_test.go b/components/cli/volume_list_test.go index d30d9fcd52..0af420eaff 100644 --- a/components/cli/volume_list_test.go +++ b/components/cli/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/components/cli/volume_remove_test.go b/components/cli/volume_remove_test.go index 0675bfd458..1fe657349a 100644 --- a/components/cli/volume_remove_test.go +++ b/components/cli/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 54e30a1ee602c6d5f2e1e7657e46bd973de0a0bc Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 19 Sep 2016 14:31:53 -0400 Subject: [PATCH 150/978] Make all the experimental subcommand consistent. Signed-off-by: Daniel Nephin Upstream-commit: 3e1b9350f58802cbfd5074e855252f09ef63267b Component: cli --- components/cli/command/checkpoint/cmd.go | 5 ++- .../command/checkpoint/cmd_experimental.go | 7 ++-- components/cli/command/commands/commands.go | 4 +- components/cli/command/plugin/cmd.go | 3 +- .../cli/command/plugin/cmd_experimental.go | 5 +-- components/cli/command/stack/cmd.go | 31 +++------------ .../cli/command/stack/cmd_experimental.go | 39 +++++++++++++++++++ components/cli/command/stack/cmd_stub.go | 18 --------- 8 files changed, 56 insertions(+), 56 deletions(-) create mode 100644 components/cli/command/stack/cmd_experimental.go delete mode 100644 components/cli/command/stack/cmd_stub.go diff --git a/components/cli/command/checkpoint/cmd.go b/components/cli/command/checkpoint/cmd.go index bc8224a2ff..7c3950bba8 100644 --- a/components/cli/command/checkpoint/cmd.go +++ b/components/cli/command/checkpoint/cmd.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/cobra" ) -// NewCheckpointCommand appends the `checkpoint` subcommands to rootCmd (only in experimental) -func NewCheckpointCommand(rootCmd *cobra.Command, dockerCli *command.DockerCli) { +// NewCheckpointCommand returns the `checkpoint` subcommand (only in experimental) +func NewCheckpointCommand(dockerCli *command.DockerCli) *cobra.Command { + return &cobra.Command{} } diff --git a/components/cli/command/checkpoint/cmd_experimental.go b/components/cli/command/checkpoint/cmd_experimental.go index c05d3ded40..3c89545778 100644 --- a/components/cli/command/checkpoint/cmd_experimental.go +++ b/components/cli/command/checkpoint/cmd_experimental.go @@ -11,8 +11,8 @@ import ( "github.com/docker/docker/cli/command" ) -// NewCheckpointCommand appends the `checkpoint` subcommands to rootCmd -func NewCheckpointCommand(rootCmd *cobra.Command, dockerCli *command.DockerCli) { +// NewCheckpointCommand returns the `checkpoint` subcommand (only in experimental) +func NewCheckpointCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := &cobra.Command{ Use: "checkpoint", Short: "Manage checkpoints", @@ -26,6 +26,5 @@ func NewCheckpointCommand(rootCmd *cobra.Command, dockerCli *command.DockerCli) newListCommand(dockerCli), newRemoveCommand(dockerCli), ) - - rootCmd.AddCommand(cmd) + return cmd } diff --git a/components/cli/command/commands/commands.go b/components/cli/command/commands/commands.go index d618233997..3e8aa25af2 100644 --- a/components/cli/command/commands/commands.go +++ b/components/cli/command/commands/commands.go @@ -71,9 +71,9 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) { hide(image.NewSaveCommand(dockerCli)), hide(image.NewTagCommand(dockerCli)), hide(system.NewInspectCommand(dockerCli)), + checkpoint.NewCheckpointCommand(dockerCli), + plugin.NewPluginCommand(dockerCli), ) - checkpoint.NewCheckpointCommand(cmd, dockerCli) - plugin.NewPluginCommand(cmd, dockerCli) } func hide(cmd *cobra.Command) *cobra.Command { diff --git a/components/cli/command/plugin/cmd.go b/components/cli/command/plugin/cmd.go index 67d0d5031c..10074218dd 100644 --- a/components/cli/command/plugin/cmd.go +++ b/components/cli/command/plugin/cmd.go @@ -8,5 +8,6 @@ import ( ) // NewPluginCommand returns a cobra command for `plugin` subcommands -func NewPluginCommand(cmd *cobra.Command, dockerCli *command.DockerCli) { +func NewPluginCommand(dockerCli *command.DockerCli) *cobra.Command { + return &cobra.Command{} } diff --git a/components/cli/command/plugin/cmd_experimental.go b/components/cli/command/plugin/cmd_experimental.go index 33c1c93acb..8bb3416097 100644 --- a/components/cli/command/plugin/cmd_experimental.go +++ b/components/cli/command/plugin/cmd_experimental.go @@ -11,7 +11,7 @@ import ( ) // NewPluginCommand returns a cobra command for `plugin` subcommands -func NewPluginCommand(rootCmd *cobra.Command, dockerCli *command.DockerCli) { +func NewPluginCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := &cobra.Command{ Use: "plugin", Short: "Manage plugins", @@ -31,6 +31,5 @@ func NewPluginCommand(rootCmd *cobra.Command, dockerCli *command.DockerCli) { newSetCommand(dockerCli), newPushCommand(dockerCli), ) - - rootCmd.AddCommand(cmd) + return cmd } diff --git a/components/cli/command/stack/cmd.go b/components/cli/command/stack/cmd.go index 22a2334419..51cb2d1bcf 100644 --- a/components/cli/command/stack/cmd.go +++ b/components/cli/command/stack/cmd.go @@ -1,39 +1,18 @@ -// +build experimental +// +build !experimental package stack import ( - "fmt" - - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/spf13/cobra" ) -// NewStackCommand returns a cobra command for `stack` subcommands +// NewStackCommand returns no command func NewStackCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "stack", - Short: "Manage stacks", - Args: cli.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) - }, - } - cmd.AddCommand( - newConfigCommand(dockerCli), - newDeployCommand(dockerCli), - newRemoveCommand(dockerCli), - newServicesCommand(dockerCli), - newPsCommand(dockerCli), - ) - return cmd + return &cobra.Command{} } -// NewTopLevelDeployCommand returns a command for `docker deploy` +// NewTopLevelDeployCommand returns no command func NewTopLevelDeployCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := *newDeployCommand(dockerCli) - // Remove the aliases at the top level - cmd.Aliases = []string{} - return &cmd + return &cobra.Command{} } diff --git a/components/cli/command/stack/cmd_experimental.go b/components/cli/command/stack/cmd_experimental.go new file mode 100644 index 0000000000..d459e0a9a1 --- /dev/null +++ b/components/cli/command/stack/cmd_experimental.go @@ -0,0 +1,39 @@ +// +build experimental + +package stack + +import ( + "fmt" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +// NewStackCommand returns a cobra command for `stack` subcommands +func NewStackCommand(dockerCli *command.DockerCli) *cobra.Command { + cmd := &cobra.Command{ + Use: "stack", + Short: "Manage Docker stacks", + Args: cli.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + }, + } + cmd.AddCommand( + newConfigCommand(dockerCli), + newDeployCommand(dockerCli), + newRemoveCommand(dockerCli), + newServicesCommand(dockerCli), + newPsCommand(dockerCli), + ) + return cmd +} + +// NewTopLevelDeployCommand returns a command for `docker deploy` +func NewTopLevelDeployCommand(dockerCli *command.DockerCli) *cobra.Command { + cmd := newDeployCommand(dockerCli) + // Remove the aliases at the top level + cmd.Aliases = []string{} + return cmd +} diff --git a/components/cli/command/stack/cmd_stub.go b/components/cli/command/stack/cmd_stub.go deleted file mode 100644 index 51cb2d1bcf..0000000000 --- a/components/cli/command/stack/cmd_stub.go +++ /dev/null @@ -1,18 +0,0 @@ -// +build !experimental - -package stack - -import ( - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" -) - -// NewStackCommand returns no command -func NewStackCommand(dockerCli *command.DockerCli) *cobra.Command { - return &cobra.Command{} -} - -// NewTopLevelDeployCommand returns no command -func NewTopLevelDeployCommand(dockerCli *command.DockerCli) *cobra.Command { - return &cobra.Command{} -} From 311b0765b18b4563d4afc937f8aa631e815eedd7 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Mon, 25 Jul 2016 15:24:34 -0400 Subject: [PATCH 151/978] Add formatter for service inspect Allows the user to use `pretty` as the format string. This enables users to put custom format options into their CLI config just like is supported for `docker ps` and `docker images` Signed-off-by: Brian Goff Upstream-commit: 20c5a9448d3d16bd473359db375796a15e9f74a9 Component: cli --- components/cli/command/formatter/formatter.go | 8 +- components/cli/command/formatter/service.go | 285 ++++++++++++++++++ components/cli/command/service/inspect.go | 148 ++------- .../cli/command/service/inspect_test.go | 14 +- 4 files changed, 324 insertions(+), 131 deletions(-) create mode 100644 components/cli/command/formatter/service.go diff --git a/components/cli/command/formatter/formatter.go b/components/cli/command/formatter/formatter.go index 32f9a4d359..e859a1ca26 100644 --- a/components/cli/command/formatter/formatter.go +++ b/components/cli/command/formatter/formatter.go @@ -11,11 +11,11 @@ import ( "github.com/docker/docker/utils/templates" ) +// Format keys used to specify certain kinds of output formats const ( - // TableFormatKey is the key used to format as a table - TableFormatKey = "table" - // RawFormatKey is the key used to format as raw JSON - RawFormatKey = "raw" + TableFormatKey = "table" + RawFormatKey = "raw" + PrettyFormatKey = "pretty" defaultQuietFormat = "{{.ID}}" ) diff --git a/components/cli/command/formatter/service.go b/components/cli/command/formatter/service.go new file mode 100644 index 0000000000..2ce18aba5c --- /dev/null +++ b/components/cli/command/formatter/service.go @@ -0,0 +1,285 @@ +package formatter + +import ( + "fmt" + "strings" + "time" + + mounttypes "github.com/docker/docker/api/types/mount" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/command/inspect" + units "github.com/docker/go-units" +) + +const serviceInspectPrettyTemplate Format = ` +ID: {{.ID}} +Name: {{.Name}} +{{- if .Labels }} +Labels: +{{- range $k, $v := .Labels }} + {{ $k }}{{if $v }}={{ $v }}{{ end }} +{{- end }}{{ end }} +Mode: +{{- if .IsModeGlobal }} Global +{{- else }} Replicated +{{- if .ModeReplicatedReplicas }} + Replicas: {{ .ModeReplicatedReplicas }} +{{- end }}{{ end }} +{{- if .HasUpdateStatus }} +UpdateStatus: + State: {{ .UpdateStatusState }} + Started: {{ .UpdateStatusStarted }} +{{- if .UpdateIsCompleted }} + Completed: {{ .UpdateStatusCompleted }} +{{- end }} + Message: {{ .UpdateStatusMessage }} +{{- end }} +Placement: +{{- if .TaskPlacementConstraints -}} + Contraints: {{ .TaskPlacementConstraints }} +{{- end }} +{{- if .HasUpdateConfig }} +UpdateConfig: + Parallelism: {{ .UpdateParallelism }} +{{- if .HasUpdateDelay -}} + Delay: {{ .UpdateDelay }} +{{- end }} + On failure: {{ .UpdateOnFailure }} +{{- end }} +ContainerSpec: + Image: {{ .ContainerImage }} +{{- if .ContainerArgs }} + Args: {{ range $arg := .ContainerArgs }}{{ $arg }} {{ end }} +{{- end -}} +{{- if .ContainerEnv }} + Env: {{ range $env := .ContainerEnv }}{{ $env }} {{ end }} +{{- end -}} +{{- if .ContainerWorkDir }} + Dir: {{ .ContainerWorkDir }} +{{- end -}} +{{- if .ContainerUser }} + User: {{ .ContainerUser }} +{{- end }} +{{- if .ContainerMounts }} +Mounts: +{{- end }} +{{- range $mount := .ContainerMounts }} + Target = {{ $mount.Target }} + Source = {{ $mount.Source }} + ReadOnly = {{ $mount.ReadOnly }} + Type = {{ $mount.Type }} +{{- end -}} +{{- if .HasResources }} +Resources: +{{- if .HasResourceReservations }} + Reservations: +{{- end }} +{{- if gt .ResourceReservationNanoCPUs 0.0 }} + CPU: {{ .ResourceReservationNanoCPUs }} +{{- end }} +{{- if .ResourceReservationMemory }} + Memory: {{ .ResourceReservationMemory }} +{{- end }} +{{- if .HasResourceLimits }} + Limits: +{{- end }} +{{- if gt .ResourceLimitsNanoCPUs 0.0 }} + CPU: {{ .ResourceLimitsNanoCPUs }} +{{- end }} +{{- if .ResourceLimitMemory }} + Memory: {{ .ResourceLimitMemory }} +{{- end }}{{ end }} +{{- if .Networks }} +Networks: +{{- range $network := .Networks }} {{ $network }}{{ end }} {{ end }} +{{- if .Ports }} +Ports: +{{- range $port := .Ports }} + PublishedPort {{ $port.PublishedPort }} + Protocol = {{ $port.Protocol }} + TargetPort = {{ $port.TargetPort }} +{{- end }} {{ end -}} +` + +// NewServiceFormat returns a Format for rendering using a Context +func NewServiceFormat(source string) Format { + switch source { + case PrettyFormatKey: + return serviceInspectPrettyTemplate + default: + return Format(strings.TrimPrefix(source, RawFormatKey)) + } +} + +// ServiceInspectWrite renders the context for a list of services +func ServiceInspectWrite(ctx Context, refs []string, getRef inspect.GetRefFunc) error { + if ctx.Format != serviceInspectPrettyTemplate { + return inspect.Inspect(ctx.Output, refs, string(ctx.Format), getRef) + } + render := func(format func(subContext subContext) error) error { + for _, ref := range refs { + serviceI, _, err := getRef(ref) + if err != nil { + return err + } + service, ok := serviceI.(swarm.Service) + if !ok { + return fmt.Errorf("got wrong object to inspect") + } + if err := format(&serviceInspectContext{Service: service}); err != nil { + return err + } + } + return nil + } + return ctx.Write(&serviceInspectContext{}, render) +} + +type serviceInspectContext struct { + swarm.Service + subContext +} + +func (ctx *serviceInspectContext) ID() string { + return ctx.Service.ID +} + +func (ctx *serviceInspectContext) Name() string { + return ctx.Service.Spec.Name +} + +func (ctx *serviceInspectContext) Labels() map[string]string { + return ctx.Service.Spec.Labels +} + +func (ctx *serviceInspectContext) IsModeGlobal() bool { + return ctx.Service.Spec.Mode.Global != nil +} + +func (ctx *serviceInspectContext) ModeReplicatedReplicas() *uint64 { + return ctx.Service.Spec.Mode.Replicated.Replicas +} + +func (ctx *serviceInspectContext) HasUpdateStatus() bool { + return ctx.Service.UpdateStatus.State != "" +} + +func (ctx *serviceInspectContext) UpdateStatusState() swarm.UpdateState { + return ctx.Service.UpdateStatus.State +} + +func (ctx *serviceInspectContext) UpdateStatusStarted() string { + return units.HumanDuration(time.Since(ctx.Service.UpdateStatus.StartedAt)) +} + +func (ctx *serviceInspectContext) UpdateIsCompleted() bool { + return ctx.Service.UpdateStatus.State == swarm.UpdateStateCompleted +} + +func (ctx *serviceInspectContext) UpdateStatusCompleted() string { + return units.HumanDuration(time.Since(ctx.Service.UpdateStatus.CompletedAt)) +} + +func (ctx *serviceInspectContext) UpdateStatusMessage() string { + return ctx.Service.UpdateStatus.Message +} + +func (ctx *serviceInspectContext) TaskPlacementConstraints() []string { + if ctx.Service.Spec.TaskTemplate.Placement != nil { + return ctx.Service.Spec.TaskTemplate.Placement.Constraints + } + return nil +} + +func (ctx *serviceInspectContext) HasUpdateConfig() bool { + return ctx.Service.Spec.UpdateConfig != nil +} + +func (ctx *serviceInspectContext) UpdateParallelism() uint64 { + return ctx.Service.Spec.UpdateConfig.Parallelism +} + +func (ctx *serviceInspectContext) HasUpdateDelay() bool { + return ctx.Service.Spec.UpdateConfig.Delay.Nanoseconds() > 0 +} + +func (ctx *serviceInspectContext) UpdateDelay() time.Duration { + return ctx.Service.Spec.UpdateConfig.Delay +} + +func (ctx *serviceInspectContext) UpdateOnFailure() string { + return ctx.Service.Spec.UpdateConfig.FailureAction +} + +func (ctx *serviceInspectContext) ContainerImage() string { + return ctx.Service.Spec.TaskTemplate.ContainerSpec.Image +} + +func (ctx *serviceInspectContext) ContainerArgs() []string { + return ctx.Service.Spec.TaskTemplate.ContainerSpec.Args +} + +func (ctx *serviceInspectContext) ContainerEnv() []string { + return ctx.Service.Spec.TaskTemplate.ContainerSpec.Env +} + +func (ctx *serviceInspectContext) ContainerWorkDir() string { + return ctx.Service.Spec.TaskTemplate.ContainerSpec.Dir +} + +func (ctx *serviceInspectContext) ContainerUser() string { + return ctx.Service.Spec.TaskTemplate.ContainerSpec.User +} + +func (ctx *serviceInspectContext) ContainerMounts() []mounttypes.Mount { + return ctx.Service.Spec.TaskTemplate.ContainerSpec.Mounts +} + +func (ctx *serviceInspectContext) HasResources() bool { + return ctx.Service.Spec.TaskTemplate.Resources != nil +} + +func (ctx *serviceInspectContext) HasResourceReservations() bool { + return ctx.Service.Spec.TaskTemplate.Resources.Reservations.NanoCPUs > 0 || ctx.Service.Spec.TaskTemplate.Resources.Reservations.MemoryBytes > 0 +} + +func (ctx *serviceInspectContext) ResourceReservationNanoCPUs() float64 { + if ctx.Service.Spec.TaskTemplate.Resources.Reservations.NanoCPUs == 0 { + return float64(0) + } + return float64(ctx.Service.Spec.TaskTemplate.Resources.Reservations.NanoCPUs) / 1e9 +} + +func (ctx *serviceInspectContext) ResourceReservationMemory() string { + if ctx.Service.Spec.TaskTemplate.Resources.Reservations.MemoryBytes == 0 { + return "" + } + return units.BytesSize(float64(ctx.Service.Spec.TaskTemplate.Resources.Reservations.MemoryBytes)) +} + +func (ctx *serviceInspectContext) HasResourceLimits() bool { + return ctx.Service.Spec.TaskTemplate.Resources.Limits.NanoCPUs > 0 || ctx.Service.Spec.TaskTemplate.Resources.Limits.MemoryBytes > 0 +} + +func (ctx *serviceInspectContext) ResourceLimitsNanoCPUs() float64 { + return float64(ctx.Service.Spec.TaskTemplate.Resources.Limits.NanoCPUs) / 1e9 +} + +func (ctx *serviceInspectContext) ResourceLimitMemory() string { + if ctx.Service.Spec.TaskTemplate.Resources.Limits.MemoryBytes == 0 { + return "" + } + return units.BytesSize(float64(ctx.Service.Spec.TaskTemplate.Resources.Limits.MemoryBytes)) +} + +func (ctx *serviceInspectContext) Networks() []string { + var out []string + for _, n := range ctx.Service.Spec.Networks { + out = append(out, n.Target) + } + return out +} + +func (ctx *serviceInspectContext) Ports() []swarm.PortConfig { + return ctx.Service.Endpoint.Ports +} diff --git a/components/cli/command/service/inspect.go b/components/cli/command/service/inspect.go index 8facb1f28b..054c24383e 100644 --- a/components/cli/command/service/inspect.go +++ b/components/cli/command/service/inspect.go @@ -2,19 +2,14 @@ package service import ( "fmt" - "io" "strings" - "time" "golang.org/x/net/context" - "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/inspect" + "github.com/docker/docker/cli/command/formatter" apiclient "github.com/docker/docker/client" - "github.com/docker/docker/pkg/ioutils" - "github.com/docker/go-units" "github.com/spf13/cobra" ) @@ -51,6 +46,10 @@ func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { client := dockerCli.Client() ctx := context.Background() + if opts.pretty { + opts.format = "pretty" + } + getRef := func(ref string) (interface{}, []byte, error) { service, _, err := client.ServiceInspectWithRaw(ctx, ref) if err == nil || !apiclient.IsErrServiceNotFound(err) { @@ -59,130 +58,27 @@ func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { return nil, nil, fmt.Errorf("Error: no such service: %s", ref) } - if !opts.pretty { - return inspect.Inspect(dockerCli.Out(), opts.refs, opts.format, getRef) + f := opts.format + if len(f) == 0 { + f = "raw" + if len(dockerCli.ConfigFile().ServiceInspectFormat) > 0 { + f = dockerCli.ConfigFile().ServiceInspectFormat + } } - return printHumanFriendly(dockerCli.Out(), opts.refs, getRef) -} + // check if the user is trying to apply a template to the pretty format, which + // is not supported + if strings.HasPrefix(f, "pretty") && f != "pretty" { + return fmt.Errorf("Cannot supply extra formatting options to the pretty template") + } -func printHumanFriendly(out io.Writer, refs []string, getRef inspect.GetRefFunc) error { - for idx, ref := range refs { - obj, _, err := getRef(ref) - if err != nil { - return err - } - printService(out, obj.(swarm.Service)) + serviceCtx := formatter.Context{ + Output: dockerCli.Out(), + Format: formatter.NewServiceFormat(f), + } - // TODO: better way to do this? - // print extra space between objects, but not after the last one - if idx+1 != len(refs) { - fmt.Fprintf(out, "\n\n") - } + if err := formatter.ServiceInspectWrite(serviceCtx, opts.refs, getRef); err != nil { + return cli.StatusError{StatusCode: 1, Status: err.Error()} } return nil } - -// TODO: use a template -func printService(out io.Writer, service swarm.Service) { - fmt.Fprintf(out, "ID:\t\t%s\n", service.ID) - fmt.Fprintf(out, "Name:\t\t%s\n", service.Spec.Name) - if service.Spec.Labels != nil { - fmt.Fprintln(out, "Labels:") - for k, v := range service.Spec.Labels { - fmt.Fprintf(out, " - %s=%s\n", k, v) - } - } - - if service.Spec.Mode.Global != nil { - fmt.Fprintln(out, "Mode:\t\tGlobal") - } else { - fmt.Fprintln(out, "Mode:\t\tReplicated") - if service.Spec.Mode.Replicated.Replicas != nil { - fmt.Fprintf(out, " Replicas:\t%d\n", *service.Spec.Mode.Replicated.Replicas) - } - } - - if service.UpdateStatus.State != "" { - fmt.Fprintln(out, "Update status:") - fmt.Fprintf(out, " State:\t\t%s\n", service.UpdateStatus.State) - fmt.Fprintf(out, " Started:\t%s ago\n", strings.ToLower(units.HumanDuration(time.Since(service.UpdateStatus.StartedAt)))) - if service.UpdateStatus.State == swarm.UpdateStateCompleted { - fmt.Fprintf(out, " Completed:\t%s ago\n", strings.ToLower(units.HumanDuration(time.Since(service.UpdateStatus.CompletedAt)))) - } - fmt.Fprintf(out, " Message:\t%s\n", service.UpdateStatus.Message) - } - - fmt.Fprintln(out, "Placement:") - if service.Spec.TaskTemplate.Placement != nil && len(service.Spec.TaskTemplate.Placement.Constraints) > 0 { - ioutils.FprintfIfNotEmpty(out, " Constraints\t: %s\n", strings.Join(service.Spec.TaskTemplate.Placement.Constraints, ", ")) - } - if service.Spec.UpdateConfig != nil { - fmt.Fprintf(out, "UpdateConfig:\n") - fmt.Fprintf(out, " Parallelism:\t%d\n", service.Spec.UpdateConfig.Parallelism) - if service.Spec.UpdateConfig.Delay.Nanoseconds() > 0 { - fmt.Fprintf(out, " Delay:\t\t%s\n", service.Spec.UpdateConfig.Delay) - } - fmt.Fprintf(out, " On failure:\t%s\n", service.Spec.UpdateConfig.FailureAction) - } - - fmt.Fprintf(out, "ContainerSpec:\n") - printContainerSpec(out, service.Spec.TaskTemplate.ContainerSpec) - - resources := service.Spec.TaskTemplate.Resources - if resources != nil { - fmt.Fprintln(out, "Resources:") - printResources := func(out io.Writer, requirement string, r *swarm.Resources) { - if r == nil || (r.MemoryBytes == 0 && r.NanoCPUs == 0) { - return - } - fmt.Fprintf(out, " %s:\n", requirement) - if r.NanoCPUs != 0 { - fmt.Fprintf(out, " CPU:\t\t%g\n", float64(r.NanoCPUs)/1e9) - } - if r.MemoryBytes != 0 { - fmt.Fprintf(out, " Memory:\t%s\n", units.BytesSize(float64(r.MemoryBytes))) - } - } - printResources(out, "Reservations", resources.Reservations) - printResources(out, "Limits", resources.Limits) - } - if len(service.Spec.Networks) > 0 { - fmt.Fprintf(out, "Networks:") - for _, n := range service.Spec.Networks { - fmt.Fprintf(out, " %s", n.Target) - } - fmt.Fprintln(out, "") - } - - if len(service.Endpoint.Ports) > 0 { - fmt.Fprintln(out, "Ports:") - for _, port := range service.Endpoint.Ports { - ioutils.FprintfIfNotEmpty(out, " Name = %s\n", port.Name) - fmt.Fprintf(out, " Protocol = %s\n", port.Protocol) - fmt.Fprintf(out, " TargetPort = %d\n", port.TargetPort) - fmt.Fprintf(out, " PublishedPort = %d\n", port.PublishedPort) - } - } -} - -func printContainerSpec(out io.Writer, containerSpec swarm.ContainerSpec) { - fmt.Fprintf(out, " Image:\t\t%s\n", containerSpec.Image) - if len(containerSpec.Args) > 0 { - fmt.Fprintf(out, " Args:\t\t%s\n", strings.Join(containerSpec.Args, " ")) - } - if len(containerSpec.Env) > 0 { - fmt.Fprintf(out, " Env:\t\t%s\n", strings.Join(containerSpec.Env, " ")) - } - ioutils.FprintfIfNotEmpty(out, " Dir\t\t%s\n", containerSpec.Dir) - ioutils.FprintfIfNotEmpty(out, " User\t\t%s\n", containerSpec.User) - if len(containerSpec.Mounts) > 0 { - fmt.Fprintln(out, " Mounts:") - for _, v := range containerSpec.Mounts { - fmt.Fprintf(out, " Target = %s\n", v.Target) - fmt.Fprintf(out, " Source = %s\n", v.Source) - fmt.Fprintf(out, " ReadOnly = %v\n", v.ReadOnly) - fmt.Fprintf(out, " Type = %v\n", v.Type) - } - } -} diff --git a/components/cli/command/service/inspect_test.go b/components/cli/command/service/inspect_test.go index 0e0f2ae74f..8e73a70efa 100644 --- a/components/cli/command/service/inspect_test.go +++ b/components/cli/command/service/inspect_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/command/formatter" ) func TestPrettyPrintWithNoUpdateConfig(t *testing.T) { @@ -77,7 +78,18 @@ func TestPrettyPrintWithNoUpdateConfig(t *testing.T) { }, } - printService(b, s) + ctx := formatter.Context{ + Output: b, + Format: formatter.NewServiceFormat("pretty"), + } + + err := formatter.ServiceInspectWrite(ctx, []string{"de179gar9d0o7ltdybungplod"}, func(ref string) (interface{}, []byte, error) { + return s, nil, nil + }) + if err != nil { + t.Fatal(err) + } + if strings.Contains(b.String(), "UpdateStatus") { t.Fatal("Pretty print failed before parsing UpdateStatus") } From 4c5847396611717d41d0281f5109dda73e10c47c Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 19 Sep 2016 13:38:58 -0400 Subject: [PATCH 152/978] Create a system subcommand for events and info. Signed-off-by: Daniel Nephin Upstream-commit: 1136c3458bd9b9de95a90107e871940883bd07cc Component: cli --- components/cli/command/commands/commands.go | 5 ++-- components/cli/command/system/cmd.go | 27 +++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 components/cli/command/system/cmd.go diff --git a/components/cli/command/commands/commands.go b/components/cli/command/commands/commands.go index d618233997..a25abf0c56 100644 --- a/components/cli/command/commands/commands.go +++ b/components/cli/command/commands/commands.go @@ -29,16 +29,17 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) { swarm.NewSwarmCommand(dockerCli), container.NewContainerCommand(dockerCli), image.NewImageCommand(dockerCli), + system.NewSystemCommand(dockerCli), container.NewRunCommand(dockerCli), image.NewBuildCommand(dockerCli), network.NewNetworkCommand(dockerCli), - system.NewEventsCommand(dockerCli), + hide(system.NewEventsCommand(dockerCli)), registry.NewLoginCommand(dockerCli), registry.NewLogoutCommand(dockerCli), registry.NewSearchCommand(dockerCli), system.NewVersionCommand(dockerCli), volume.NewVolumeCommand(dockerCli), - system.NewInfoCommand(dockerCli), + hide(system.NewInfoCommand(dockerCli)), hide(container.NewAttachCommand(dockerCli)), hide(container.NewCommitCommand(dockerCli)), hide(container.NewCopyCommand(dockerCli)), diff --git a/components/cli/command/system/cmd.go b/components/cli/command/system/cmd.go new file mode 100644 index 0000000000..8ce9d93ae7 --- /dev/null +++ b/components/cli/command/system/cmd.go @@ -0,0 +1,27 @@ +package system + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" +) + +// NewSystemCommand returns a cobra command for `system` subcommands +func NewSystemCommand(dockerCli *command.DockerCli) *cobra.Command { + cmd := &cobra.Command{ + Use: "system", + Short: "Manage Docker", + Args: cli.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + }, + } + cmd.AddCommand( + NewEventsCommand(dockerCli), + NewInfoCommand(dockerCli), + ) + return cmd +} From e5fc4c58fbd1cf48edb7af2b72b0757c320ee37c Mon Sep 17 00:00:00 2001 From: John Howard Date: Tue, 20 Sep 2016 12:01:04 -0700 Subject: [PATCH 153/978] Revert Box from HostConfig Signed-off-by: John Howard Upstream-commit: 1385ad8b008cbd1e0e22f915ad420bad360885d5 Component: cli --- components/cli/command/container/run.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/container/run.go b/components/cli/command/container/run.go index a167e78f9a..d36ab610cf 100644 --- a/components/cli/command/container/run.go +++ b/components/cli/command/container/run.go @@ -135,7 +135,7 @@ func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions // a far better user experience rather than relying on subsequent resizes // to cause things to catch up. if runtime.GOOS == "windows" { - hostConfig.ConsoleSize.Height, hostConfig.ConsoleSize.Width = dockerCli.Out().GetTtySize() + hostConfig.ConsoleSize[0], hostConfig.ConsoleSize[1] = dockerCli.Out().GetTtySize() } ctx, cancelFun := context.WithCancel(context.Background()) From d13d0e99465050b77bc0939f2217e7b967871901 Mon Sep 17 00:00:00 2001 From: Misty Stanley-Jones Date: Thu, 1 Sep 2016 15:38:25 -0700 Subject: [PATCH 154/978] Clarify usage of --force when used on a swarm manager Fixes #26125 Signed-off-by: Misty Stanley-Jones Upstream-commit: bfbdb15f555956fea7a377be30db31a4e231fb8d Component: cli --- components/cli/command/swarm/leave.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/command/swarm/leave.go b/components/cli/command/swarm/leave.go index 9224113409..ae13884154 100644 --- a/components/cli/command/swarm/leave.go +++ b/components/cli/command/swarm/leave.go @@ -19,7 +19,7 @@ func newLeaveCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := &cobra.Command{ Use: "leave [OPTIONS]", - Short: "Leave a swarm", + Short: "Leave the swarm (workers only)", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { return runLeave(dockerCli, opts) @@ -27,7 +27,7 @@ func newLeaveCommand(dockerCli *command.DockerCli) *cobra.Command { } flags := cmd.Flags() - flags.BoolVar(&opts.force, "force", false, "Force leave ignoring warnings.") + flags.BoolVar(&opts.force, "force", false, "Force this node to leave the swarm, ignoring warnings") return cmd } From 412bbe39ebdf02de835cdf48504bea9a9653c50b Mon Sep 17 00:00:00 2001 From: Josh Chorlton Date: Thu, 22 Sep 2016 15:00:30 +0800 Subject: [PATCH 155/978] Move /x/net/context to context in docker client README Signed-off-by: Josh Chorlton Upstream-commit: 59e38197ffa4ada6c4db550fda9e55308e971abd Component: cli --- components/cli/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/README.md b/components/cli/README.md index 7872d94a53..34cf7372db 100644 --- a/components/cli/README.md +++ b/components/cli/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 37441dedaa3b87493980bb0cd7a39cabfb1a00f9 Mon Sep 17 00:00:00 2001 From: Josh Horwitz Date: Tue, 9 Aug 2016 10:34:07 -1000 Subject: [PATCH 156/978] Refactor to new events api Signed-off-by: Josh Horwitz Upstream-commit: d700b905768687e556e8a6e3060ab2e98016f95e Component: cli --- components/cli/command/container/run.go | 5 +- components/cli/command/container/start.go | 8 +--- components/cli/command/container/stats.go | 24 +++++----- components/cli/command/container/utils.go | 42 ++++++++++------- components/cli/command/system/events.go | 47 ++++++++++--------- components/cli/command/system/events_utils.go | 21 +-------- 6 files changed, 67 insertions(+), 80 deletions(-) diff --git a/components/cli/command/container/run.go b/components/cli/command/container/run.go index d36ab610cf..2f1181659d 100644 --- a/components/cli/command/container/run.go +++ b/components/cli/command/container/run.go @@ -211,10 +211,7 @@ func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions }) } - statusChan, err := waitExitOrRemoved(dockerCli, context.Background(), createResponse.ID, hostConfig.AutoRemove) - if err != nil { - return fmt.Errorf("Error waiting container's exit code: %v", err) - } + statusChan := waitExitOrRemoved(dockerCli, ctx, createResponse.ID, hostConfig.AutoRemove) //start the container if err := client.ContainerStart(ctx, createResponse.ID, types.ContainerStartOptions{}); err != nil { diff --git a/components/cli/command/container/start.go b/components/cli/command/container/start.go index 9f414a7c66..4c31f9bf97 100644 --- a/components/cli/command/container/start.go +++ b/components/cli/command/container/start.go @@ -108,7 +108,7 @@ func runStart(dockerCli *command.DockerCli, opts *startOptions) error { // 3. We should open a channel for receiving status code of the container // no matter it's detached, removed on daemon side(--rm) or exit normally. - statusChan, statusErr := waitExitOrRemoved(dockerCli, context.Background(), c.ID, c.HostConfig.AutoRemove) + statusChan := waitExitOrRemoved(dockerCli, ctx, c.ID, c.HostConfig.AutoRemove) startOptions := types.ContainerStartOptions{ CheckpointID: opts.checkpoint, } @@ -117,7 +117,7 @@ func runStart(dockerCli *command.DockerCli, opts *startOptions) error { if err := dockerCli.Client().ContainerStart(ctx, c.ID, startOptions); err != nil { cancelFun() <-cErr - if c.HostConfig.AutoRemove && statusErr == nil { + if c.HostConfig.AutoRemove { // wait container to be removed <-statusChan } @@ -134,10 +134,6 @@ func runStart(dockerCli *command.DockerCli, opts *startOptions) error { return attchErr } - if statusErr != nil { - return fmt.Errorf("can't get container's exit code: %v", statusErr) - } - if status := <-statusChan; status != 0 { return cli.StatusError{StatusCode: status} } diff --git a/components/cli/command/container/stats.go b/components/cli/command/container/stats.go index 2bd5e3db75..394302d087 100644 --- a/components/cli/command/container/stats.go +++ b/components/cli/command/container/stats.go @@ -63,24 +63,22 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error { options := types.EventsOptions{ Filters: f, } - resBody, err := dockerCli.Client().Events(ctx, options) - // Whether we successfully subscribed to events or not, we can now + + eventq, errq := dockerCli.Client().Events(ctx, options) + + // Whether we successfully subscribed to eventq or not, we can now // unblock the main goroutine. close(started) - if err != nil { - closeChan <- err - return - } - defer resBody.Close() - system.DecodeEvents(resBody, func(event events.Message, err error) error { - if err != nil { + for { + select { + case event := <-eventq: + c <- event + case err := <-errq: closeChan <- err - return nil + return } - c <- event - return nil - }) + } } // waitFirst is a WaitGroup to wait first stat data's reach for each container diff --git a/components/cli/command/container/utils.go b/components/cli/command/container/utils.go index 7e895834f9..9df1d115e2 100644 --- a/components/cli/command/container/utils.go +++ b/components/cli/command/container/utils.go @@ -1,7 +1,6 @@ package container import ( - "fmt" "strconv" "golang.org/x/net/context" @@ -11,11 +10,10 @@ import ( "github.com/docker/docker/api/types/events" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/system" clientapi "github.com/docker/docker/client" ) -func waitExitOrRemoved(dockerCli *command.DockerCli, ctx context.Context, containerID string, waitRemove bool) (chan int, error) { +func waitExitOrRemoved(dockerCli *command.DockerCli, ctx context.Context, containerID string, waitRemove bool) chan int { if len(containerID) == 0 { // containerID can never be empty panic("Internal Error: waitExitOrRemoved needs a containerID as parameter") @@ -24,11 +22,7 @@ func waitExitOrRemoved(dockerCli *command.DockerCli, ctx context.Context, contai statusChan := make(chan int) exitCode := 125 - eventProcessor := func(e events.Message, err error) error { - if err != nil { - statusChan <- exitCode - return fmt.Errorf("failed to decode event: %v", err) - } + eventProcessor := func(e events.Message) bool { stopProcessing := false switch e.Status { @@ -53,11 +47,10 @@ func waitExitOrRemoved(dockerCli *command.DockerCli, ctx context.Context, contai if stopProcessing { statusChan <- exitCode - // stop the loop processing - return fmt.Errorf("done") + return true } - return nil + return false } // Get events via Events API @@ -67,14 +60,29 @@ func waitExitOrRemoved(dockerCli *command.DockerCli, ctx context.Context, contai options := types.EventsOptions{ Filters: f, } - resBody, err := dockerCli.Client().Events(ctx, options) - if err != nil { - return nil, fmt.Errorf("can't get events from daemon: %v", err) - } - go system.DecodeEvents(resBody, eventProcessor) + eventCtx, cancel := context.WithCancel(ctx) + eventq, errq := dockerCli.Client().Events(eventCtx, options) - return statusChan, nil + go func() { + defer cancel() + + for { + select { + case evt := <-eventq: + if eventProcessor(evt) { + return + } + + case err := <-errq: + logrus.Errorf("error getting events from daemon: %v", err) + statusChan <- exitCode + return + } + } + }() + + return statusChan } // getExitCode performs an inspect on the container. It returns diff --git a/components/cli/command/system/events.go b/components/cli/command/system/events.go index f2946b8763..7b5fb592cb 100644 --- a/components/cli/command/system/events.go +++ b/components/cli/command/system/events.go @@ -63,13 +63,33 @@ func runEvents(dockerCli *command.DockerCli, opts *eventsOptions) error { Filters: opts.filter.Value(), } - responseBody, err := dockerCli.Client().Events(context.Background(), options) - if err != nil { - return err - } - defer responseBody.Close() + ctx, cancel := context.WithCancel(context.Background()) + events, errs := dockerCli.Client().Events(ctx, options) + defer cancel() - return streamEvents(dockerCli.Out(), responseBody, tmpl) + out := dockerCli.Out() + + for { + select { + case event := <-events: + if err := handleEvent(out, event, tmpl); err != nil { + return err + } + case err := <-errs: + if err == io.EOF { + return nil + } + return err + } + } +} + +func handleEvent(out io.Writer, event eventtypes.Message, tmpl *template.Template) error { + if tmpl == nil { + return prettyPrintEvent(out, event) + } + + return formatEvent(out, event, tmpl) } func makeTemplate(format string) (*template.Template, error) { @@ -85,21 +105,6 @@ func makeTemplate(format string) (*template.Template, error) { return tmpl, tmpl.Execute(ioutil.Discard, &eventtypes.Message{}) } -// streamEvents decodes prints the incoming events in the provided output. -func streamEvents(out io.Writer, input io.Reader, tmpl *template.Template) error { - return DecodeEvents(input, func(event eventtypes.Message, err error) error { - if err != nil { - return err - } - if tmpl == nil { - return prettyPrintEvent(out, event) - } - return formatEvent(out, event, tmpl) - }) -} - -type eventProcessor func(event eventtypes.Message, err error) error - // prettyPrintEvent prints all types of event information. // Each output includes the event type, actor id, name and action. // Actor attributes are printed at the end if the actor has any. diff --git a/components/cli/command/system/events_utils.go b/components/cli/command/system/events_utils.go index 71c1b0476b..b0dd909d15 100644 --- a/components/cli/command/system/events_utils.go +++ b/components/cli/command/system/events_utils.go @@ -1,14 +1,14 @@ package system import ( - "encoding/json" - "io" "sync" "github.com/Sirupsen/logrus" eventtypes "github.com/docker/docker/api/types/events" ) +type eventProcessor func(eventtypes.Message, error) error + // EventHandler is abstract interface for user to customize // own handle functions of each type of events type EventHandler interface { @@ -47,20 +47,3 @@ func (w *eventHandler) Watch(c <-chan eventtypes.Message) { go h(e) } } - -// DecodeEvents decodes event from input stream -func DecodeEvents(input io.Reader, ep eventProcessor) error { - dec := json.NewDecoder(input) - for { - var event eventtypes.Message - err := dec.Decode(&event) - if err != nil && err == io.EOF { - break - } - - if procErr := ep(event, err); procErr != nil { - return procErr - } - } - return nil -} From 92c2c018ea0aa0bca7adf498770f5fbb424792ae Mon Sep 17 00:00:00 2001 From: Josh Horwitz Date: Tue, 9 Aug 2016 10:34:07 -1000 Subject: [PATCH 157/978] Refactor to new events api Signed-off-by: Josh Horwitz Upstream-commit: 9acc93282ec40ffef007c936254261f69d662cc0 Component: cli --- components/cli/events.go | 69 ++++++++++++++++++++++---- components/cli/events_test.go | 93 +++++++++++++++++++++++++---------- components/cli/interface.go | 3 +- 3 files changed, 127 insertions(+), 38 deletions(-) diff --git a/components/cli/events.go b/components/cli/events.go index 0ba7114f94..c154f7dcf9 100644 --- a/components/cli/events.go +++ b/components/cli/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/components/cli/events_test.go b/components/cli/events_test.go index 6328983609..ba82d2f542 100644 --- a/components/cli/events_test.go +++ b/components/cli/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/components/cli/interface.go b/components/cli/interface.go index 2d5555ff06..81320918b3 100644 --- a/components/cli/interface.go +++ b/components/cli/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 7883253071aa753231397002e98b23494bbe837b Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 23 Jun 2016 05:00:21 +0000 Subject: [PATCH 158/978] add `docker stack ls` Signed-off-by: Akihiro Suda Upstream-commit: b06f3f27a46bdab60e9a9eafaa7aa7c76ecca1fc Component: cli --- .../cli/command/stack/cmd_experimental.go | 1 + components/cli/command/stack/list.go | 119 ++++++++++++++++++ components/cli/command/stack/services.go | 4 - 3 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 components/cli/command/stack/list.go diff --git a/components/cli/command/stack/cmd_experimental.go b/components/cli/command/stack/cmd_experimental.go index d459e0a9a1..b32d925330 100644 --- a/components/cli/command/stack/cmd_experimental.go +++ b/components/cli/command/stack/cmd_experimental.go @@ -23,6 +23,7 @@ func NewStackCommand(dockerCli *command.DockerCli) *cobra.Command { cmd.AddCommand( newConfigCommand(dockerCli), newDeployCommand(dockerCli), + newListCommand(dockerCli), newRemoveCommand(dockerCli), newServicesCommand(dockerCli), newPsCommand(dockerCli), diff --git a/components/cli/command/stack/list.go b/components/cli/command/stack/list.go new file mode 100644 index 0000000000..9fe626d96d --- /dev/null +++ b/components/cli/command/stack/list.go @@ -0,0 +1,119 @@ +// +build experimental + +package stack + +import ( + "fmt" + "io" + "strconv" + "text/tabwriter" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/client" + "github.com/spf13/cobra" +) + +const ( + listItemFmt = "%s\t%s\n" +) + +type listOptions struct { +} + +func newListCommand(dockerCli *command.DockerCli) *cobra.Command { + opts := listOptions{} + + cmd := &cobra.Command{ + Use: "ls", + Aliases: []string{"list"}, + Short: "List stacks", + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runList(dockerCli, opts) + }, + } + + return cmd +} + +func runList(dockerCli *command.DockerCli, opts listOptions) error { + client := dockerCli.Client() + ctx := context.Background() + + stacks, err := getStacks(ctx, client) + if err != nil { + return err + } + + out := dockerCli.Out() + printTable(out, stacks) + return nil +} + +func printTable(out io.Writer, stacks []*stack) { + writer := tabwriter.NewWriter(out, 0, 4, 2, ' ', 0) + + // Ignore flushing errors + defer writer.Flush() + + fmt.Fprintf(writer, listItemFmt, "NAME", "SERVICES") + for _, stack := range stacks { + fmt.Fprintf( + writer, + listItemFmt, + stack.Name, + strconv.Itoa(stack.Services), + ) + } +} + +type stack struct { + // Name is the name of the stack + Name string + // Services is the number of the services + Services int +} + +func getStacks( + ctx context.Context, + apiclient client.APIClient, +) ([]*stack, error) { + + filter := filters.NewArgs() + filter.Add("label", labelNamespace) + + services, err := apiclient.ServiceList( + ctx, + types.ServiceListOptions{Filter: filter}) + if err != nil { + return nil, err + } + m := make(map[string]*stack, 0) + for _, service := range services { + labels := service.Spec.Labels + name, ok := labels[labelNamespace] + if !ok { + return nil, fmt.Errorf("cannot get label %s for service %s", + labelNamespace, service.ID) + } + ztack, ok := m[name] + if !ok { + m[name] = &stack{ + Name: name, + Services: 1, + } + } else { + ztack.Services++ + } + } + var stacks []*stack + for _, stack := range m { + stacks = append(stacks, stack) + } + return stacks, nil +} diff --git a/components/cli/command/stack/services.go b/components/cli/command/stack/services.go index 22906378d6..60f52c30c7 100644 --- a/components/cli/command/stack/services.go +++ b/components/cli/command/stack/services.go @@ -16,10 +16,6 @@ import ( "github.com/spf13/cobra" ) -const ( - listItemFmt = "%s\t%s\t%s\t%s\t%s\n" -) - type servicesOptions struct { quiet bool filter opts.FilterOpt From b06624604bf5663f3045b0bf9c7911ada5a5345e Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Thu, 22 Sep 2016 22:38:18 +0200 Subject: [PATCH 159/978] Deprecate "daemon" subcommand The daemon is in a separate (dockerd) binary since docker 1.12, so should no longer be used. This marks the command as deprecated, and adds it to the deprecated features list. Signed-off-by: Sebastiaan van Stijn Upstream-commit: 4a2f7d80920e4cdce78af9dc7ecbcd428f77b186 Component: cli --- components/cli/daemon_unix.go | 1 + 1 file changed, 1 insertion(+) diff --git a/components/cli/daemon_unix.go b/components/cli/daemon_unix.go index 754bdeece3..f68d220c2f 100644 --- a/components/cli/daemon_unix.go +++ b/components/cli/daemon_unix.go @@ -24,6 +24,7 @@ func newDaemonCommand() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { return runDaemon() }, + Deprecated: "and will be removed in Docker 1.16. Please run `dockerd` directly.", } cmd.SetHelpFunc(helpFunc) return cmd From 09eec7183c4ea04f2f4100386b1e697f23f26ae8 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 160/978] Implement build cache based on history array Based on work by KJ Tsanaktsidis Signed-off-by: Tonis Tiigi Signed-off-by: KJ Tsanaktsidis Upstream-commit: fe4cc3fd77576a6c87c862ac6898cca15242aacd Component: cli --- components/cli/command/image/build.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go index 17be405bd5..51d0ea9f08 100644 --- a/components/cli/command/image/build.go +++ b/components/cli/command/image/build.go @@ -55,6 +55,7 @@ type buildOptions struct { rm bool forceRm bool pull bool + cacheFrom []string } // NewBuildCommand creates a new `docker build` command @@ -98,6 +99,7 @@ func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command { flags.BoolVar(&options.forceRm, "force-rm", false, "Always remove intermediate containers") flags.BoolVarP(&options.quiet, "quiet", "q", false, "Suppress the build output and print image ID on success") flags.BoolVar(&options.pull, "pull", false, "Always attempt to pull a newer version of the image") + flags.StringSliceVar(&options.cacheFrom, "cache-from", []string{}, "Images to consider as cache sources") command.AddTrustedFlags(flags, true) @@ -289,6 +291,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { BuildArgs: runconfigopts.ConvertKVStringsToMap(options.buildArgs.GetAll()), AuthConfigs: authConfig, Labels: runconfigopts.ConvertKVStringsToMap(options.labels), + CacheFrom: options.cacheFrom, } response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions) From 3af0faba4d6fd268c29ab7ed8de869b5324b57c0 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 161/978] Implement build cache based on history array Based on work by KJ Tsanaktsidis Signed-off-by: Tonis Tiigi Signed-off-by: KJ Tsanaktsidis Upstream-commit: 9f20fabc69b43be38f717065667c50e1ff616efa Component: cli --- components/cli/image_build.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/components/cli/image_build.go b/components/cli/image_build.go index a84bf57821..0094602a6e 100644 --- a/components/cli/image_build.go +++ b/components/cli/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 3d802fdc1710863288316d5cd6c447c48f0fd1d1 Mon Sep 17 00:00:00 2001 From: John Howard Date: Wed, 7 Sep 2016 15:10:00 -0700 Subject: [PATCH 162/978] Add isolation to info Signed-off-by: John Howard Upstream-commit: 266b7564a5e52794217c54b1cc213f29f3dcffe0 Component: cli --- components/cli/command/system/info.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/components/cli/command/system/info.go b/components/cli/command/system/info.go index a2d0abad23..e82661d4ec 100644 --- a/components/cli/command/system/info.go +++ b/components/cli/command/system/info.go @@ -141,6 +141,11 @@ func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error { fmt.Fprintf(dockerCli.Out(), "\n") } + // Isolation only has meaning on a Windows daemon. + if info.OSType == "windows" { + fmt.Fprintf(dockerCli.Out(), "Default Isolation: %v\n", info.Isolation) + } + ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Kernel Version: %s\n", info.KernelVersion) ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Operating System: %s\n", info.OperatingSystem) ioutils.FprintfIfNotEmpty(dockerCli.Out(), "OSType: %s\n", info.OSType) From 700f218052dca30ce6b5673414671c00598decab Mon Sep 17 00:00:00 2001 From: qudongfang Date: Thu, 8 Sep 2016 09:57:54 +0800 Subject: [PATCH 163/978] ensures that transport.Client is closed while using cli.NewClient with *http.Client = nil. Signed-off-by: qudongfang Upstream-commit: 9403a5b63e1ba5e86ffeacfb19f35e67a192cbdd Component: cli --- components/cli/client.go | 13 +++++++++++++ components/cli/client_test.go | 5 +++++ 2 files changed, 18 insertions(+) diff --git a/components/cli/client.go b/components/cli/client.go index deccb4ab74..bee429b8ca 100644 --- a/components/cli/client.go +++ b/components/cli/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/components/cli/client_test.go b/components/cli/client_test.go index 60e44dc299..222f23d45e 100644 --- a/components/cli/client_test.go +++ b/components/cli/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 505f16cebeccaa93131e964200ea644ce97f3390 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Mon, 26 Sep 2016 10:12:24 +0100 Subject: [PATCH 164/978] cli: Add more nil checking to service pretty-printer Currently, if the service mode is not "global", this code assumes that Replicated is non-nil. This assumption may not be true in the future. Instead of making the assumption, explicitly check that Replicated is non-nil before using it. Similarly, for limits and reservations, enclose methods that read from Limits and Reservations within checks that those fields are non-nil. Signed-off-by: Aaron Lehmann Upstream-commit: 56d92bfdff47bc356528372427798f4460db0e7c Component: cli --- components/cli/command/formatter/service.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/components/cli/command/formatter/service.go b/components/cli/command/formatter/service.go index 2ce18aba5c..a1872e91b9 100644 --- a/components/cli/command/formatter/service.go +++ b/components/cli/command/formatter/service.go @@ -21,7 +21,7 @@ Labels: {{- end }}{{ end }} Mode: {{- if .IsModeGlobal }} Global -{{- else }} Replicated +{{- else if .IsModeReplicated }} Replicated {{- if .ModeReplicatedReplicas }} Replicas: {{ .ModeReplicatedReplicas }} {{- end }}{{ end }} @@ -73,22 +73,20 @@ Mounts: Resources: {{- if .HasResourceReservations }} Reservations: -{{- end }} {{- if gt .ResourceReservationNanoCPUs 0.0 }} CPU: {{ .ResourceReservationNanoCPUs }} {{- end }} {{- if .ResourceReservationMemory }} Memory: {{ .ResourceReservationMemory }} -{{- end }} +{{- end }}{{ end }} {{- if .HasResourceLimits }} Limits: -{{- end }} {{- if gt .ResourceLimitsNanoCPUs 0.0 }} CPU: {{ .ResourceLimitsNanoCPUs }} {{- end }} {{- if .ResourceLimitMemory }} Memory: {{ .ResourceLimitMemory }} -{{- end }}{{ end }} +{{- end }}{{ end }}{{ end }} {{- if .Networks }} Networks: {{- range $network := .Networks }} {{ $network }}{{ end }} {{ end }} @@ -156,6 +154,10 @@ func (ctx *serviceInspectContext) IsModeGlobal() bool { return ctx.Service.Spec.Mode.Global != nil } +func (ctx *serviceInspectContext) IsModeReplicated() bool { + return ctx.Service.Spec.Mode.Replicated != nil +} + func (ctx *serviceInspectContext) ModeReplicatedReplicas() *uint64 { return ctx.Service.Spec.Mode.Replicated.Replicas } From c8795696d6732ff7757226bf311808a9393c38a7 Mon Sep 17 00:00:00 2001 From: allencloud Date: Mon, 19 Sep 2016 16:40:44 +0800 Subject: [PATCH 165/978] validate service parameter in client side to avoid api call Signed-off-by: allencloud Upstream-commit: a16fed83af4716a80c79ad44712dfafff559e3df Component: cli --- components/cli/command/service/scale.go | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/components/cli/command/service/scale.go b/components/cli/command/service/scale.go index 2e2982db43..61b73bc354 100644 --- a/components/cli/command/service/scale.go +++ b/components/cli/command/service/scale.go @@ -46,9 +46,17 @@ func runScale(dockerCli *command.DockerCli, args []string) error { var errors []string for _, arg := range args { parts := strings.SplitN(arg, "=", 2) - serviceID, scale := parts[0], parts[1] + serviceID, scaleStr := parts[0], parts[1] + + // validate input arg scale number + scale, err := strconv.ParseUint(scaleStr, 10, 64) + if err != nil { + errors = append(errors, fmt.Sprintf("%s: invalid replicas value %s: %v", serviceID, scaleStr, err)) + continue + } + if err := runServiceScale(dockerCli, serviceID, scale); err != nil { - errors = append(errors, fmt.Sprintf("%s: %s", serviceID, err.Error())) + errors = append(errors, fmt.Sprintf("%s: %v", serviceID, err)) } } @@ -58,12 +66,11 @@ func runScale(dockerCli *command.DockerCli, args []string) error { return fmt.Errorf(strings.Join(errors, "\n")) } -func runServiceScale(dockerCli *command.DockerCli, serviceID string, scale string) error { +func runServiceScale(dockerCli *command.DockerCli, serviceID string, scale uint64) error { client := dockerCli.Client() ctx := context.Background() service, _, err := client.ServiceInspectWithRaw(ctx, serviceID) - if err != nil { return err } @@ -72,17 +79,14 @@ func runServiceScale(dockerCli *command.DockerCli, serviceID string, scale strin if serviceMode.Replicated == nil { return fmt.Errorf("scale can only be used with replicated mode") } - uintScale, err := strconv.ParseUint(scale, 10, 64) - if err != nil { - return fmt.Errorf("invalid replicas value %s: %s", scale, err.Error()) - } - serviceMode.Replicated.Replicas = &uintScale + + serviceMode.Replicated.Replicas = &scale err = client.ServiceUpdate(ctx, service.ID, service.Version, service.Spec, types.ServiceUpdateOptions{}) if err != nil { return err } - fmt.Fprintf(dockerCli.Out(), "%s scaled to %s\n", serviceID, scale) + fmt.Fprintf(dockerCli.Out(), "%s scaled to %d\n", serviceID, scale) return nil } From b83d53e6c9b117daac21ac44ceb55e5adfd19cc7 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sat, 23 Jul 2016 09:58:58 -0700 Subject: [PATCH 166/978] Fix partial/full filter issue in `service tasks --filter` This fix tries to address the issue related to 24108 and 24790, and also the case from 24620#issuecomment-233715656 The reason for the failure case in the above mentioned issues is that currently Task names are actually indexed by Service Name (`e.ServiceAnnotations.Name`) To fix it, a pull request in swarmkit (swarmkit/pull/1193) has been opened separately. This fix adds the integration tests for the above mentioned issues. Swarmkit revendoring is needed to completely fix the issues. This fix fixes 24108. This fix fixes 24790. This fix is related to 24620. Signed-off-by: Yong Tang Upstream-commit: 2d844ea5c8834b93239bd330f53cc43b90e060e0 Component: cli --- components/cli/command/task/print.go | 30 ++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/components/cli/command/task/print.go b/components/cli/command/task/print.go index 963aea95ce..b9d6b3eaf4 100644 --- a/components/cli/command/task/print.go +++ b/components/cli/command/task/print.go @@ -16,7 +16,7 @@ import ( ) const ( - psTaskItemFmt = "%s\t%s\t%s\t%s\t%s\t%s %s ago\t%s\n" + psTaskItemFmt = "%s\t%s\t%s\t%s\t%s %s ago\t%s\n" maxErrLength = 30 ) @@ -48,11 +48,12 @@ func Print(dockerCli *command.DockerCli, ctx context.Context, tasks []swarm.Task // Ignore flushing errors defer writer.Flush() - fmt.Fprintln(writer, strings.Join([]string{"ID", "NAME", "IMAGE", "NODE", "DESIRED STATE", "CURRENT STATE", "ERROR"}, "\t")) + fmt.Fprintln(writer, strings.Join([]string{"NAME", "IMAGE", "NODE", "DESIRED STATE", "CURRENT STATE", "ERROR"}, "\t")) - prevName := "" + prevServiceName := "" + prevSlot := 0 for _, task := range tasks { - serviceValue, err := resolver.Resolve(ctx, swarm.Service{}, task.ServiceID) + serviceName, err := resolver.Resolve(ctx, swarm.Service{}, task.ServiceID) if err != nil { return err } @@ -61,17 +62,27 @@ func Print(dockerCli *command.DockerCli, ctx context.Context, tasks []swarm.Task return err } - name := serviceValue - if task.Slot > 0 { - name = fmt.Sprintf("%s.%d", name, task.Slot) + name := task.Annotations.Name + // TODO: This is the fallback .. in case task name is not present in + // Annotations (upgraded from 1.12). + // We may be able to remove the following in the future. + if name == "" { + if task.Slot != 0 { + name = fmt.Sprintf("%v.%v.%v", serviceName, task.Slot, task.ID) + } else { + name = fmt.Sprintf("%v.%v.%v", serviceName, task.NodeID, task.ID) + } } // Indent the name if necessary indentedName := name - if prevName == name { + // Since the new format of the task name is .., we should only compare + // and here. + if prevServiceName == serviceName && prevSlot == task.Slot { indentedName = fmt.Sprintf(" \\_ %s", indentedName) } - prevName = name + prevServiceName = serviceName + prevSlot = task.Slot // Trim and quote the error message. taskErr := task.Status.Err @@ -85,7 +96,6 @@ func Print(dockerCli *command.DockerCli, ctx context.Context, tasks []swarm.Task fmt.Fprintf( writer, psTaskItemFmt, - task.ID, indentedName, task.Spec.ContainerSpec.Image, nodeValue, From 64b2b3acec3b9af0c322f4fc7896dcec7b3963a6 Mon Sep 17 00:00:00 2001 From: allencloud Date: Sun, 25 Sep 2016 16:47:45 +0800 Subject: [PATCH 167/978] add endpoint mode in service pretty Signed-off-by: allencloud Upstream-commit: cc375fafd04f4149a75185a85e1b6f1fad50ae6e Component: cli --- components/cli/command/formatter/service.go | 15 ++++++++++++--- components/cli/command/inspect/inspector.go | 4 ++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/components/cli/command/formatter/service.go b/components/cli/command/formatter/service.go index a1872e91b9..a92326e75d 100644 --- a/components/cli/command/formatter/service.go +++ b/components/cli/command/formatter/service.go @@ -19,9 +19,9 @@ Labels: {{- range $k, $v := .Labels }} {{ $k }}{{if $v }}={{ $v }}{{ end }} {{- end }}{{ end }} -Mode: -{{- if .IsModeGlobal }} Global -{{- else if .IsModeReplicated }} Replicated +Service Mode: +{{- if .IsModeGlobal }} Global +{{- else if .IsModeReplicated }} Replicated {{- if .ModeReplicatedReplicas }} Replicas: {{ .ModeReplicatedReplicas }} {{- end }}{{ end }} @@ -90,6 +90,7 @@ Resources: {{- if .Networks }} Networks: {{- range $network := .Networks }} {{ $network }}{{ end }} {{ end }} +Endpoint Mode: {{ .EndpointMode }} {{- if .Ports }} Ports: {{- range $port := .Ports }} @@ -282,6 +283,14 @@ func (ctx *serviceInspectContext) Networks() []string { return out } +func (ctx *serviceInspectContext) EndpointMode() string { + if ctx.Service.Spec.EndpointSpec == nil { + return "" + } + + return string(ctx.Service.Spec.EndpointSpec.Mode) +} + func (ctx *serviceInspectContext) Ports() []swarm.PortConfig { return ctx.Service.Endpoint.Ports } diff --git a/components/cli/command/inspect/inspector.go b/components/cli/command/inspect/inspector.go index b0537e8464..1d81643fb1 100644 --- a/components/cli/command/inspect/inspector.go +++ b/components/cli/command/inspect/inspector.go @@ -122,7 +122,7 @@ func (i *TemplateInspector) tryRawInspectFallback(rawElement []byte) error { return nil } -// Flush write the result of inspecting all elements into the output stream. +// Flush writes the result of inspecting all elements into the output stream. func (i *TemplateInspector) Flush() error { if i.buffer.Len() == 0 { _, err := io.WriteString(i.outputStream, "\n") @@ -156,7 +156,7 @@ func (i *IndentedInspector) Inspect(typedElement interface{}, rawElement []byte) return nil } -// Flush write the result of inspecting all elements into the output stream. +// Flush writes the result of inspecting all elements into the output stream. func (i *IndentedInspector) Flush() error { if len(i.elements) == 0 && len(i.rawElements) == 0 { _, err := io.WriteString(i.outputStream, "[]\n") From c8b419cfefafc4d70054f05a8db58f534d810604 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Thu, 25 Aug 2016 21:08:53 -0700 Subject: [PATCH 168/978] Let swarmkit handle cluster defaults in `swarm init` if not specified This fix tries to address the issue raised in 24958 where previously `docker swarm init` will automatically fill in all the default value (instead of letting swarmkit to handle the default). This fix update the `swarm init` so that initial value are passed only when a flag change has been detected. This fix fixes 24958. Signed-off-by: Yong Tang Upstream-commit: e0f229d2caa4a46f06a327deffcfea80fceff915 Component: cli --- components/cli/command/swarm/init.go | 2 +- components/cli/command/swarm/opts.go | 19 ++++++++++++++----- components/cli/command/swarm/update.go | 3 ++- components/cli/command/system/info.go | 6 +++++- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/components/cli/command/swarm/init.go b/components/cli/command/swarm/init.go index 9a17224bde..60fb8e8fe3 100644 --- a/components/cli/command/swarm/init.go +++ b/components/cli/command/swarm/init.go @@ -59,7 +59,7 @@ func runInit(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts initOption ListenAddr: opts.listenAddr.String(), AdvertiseAddr: opts.advertiseAddr, ForceNewCluster: opts.forceNewCluster, - Spec: opts.swarmOptions.ToSpec(), + Spec: opts.swarmOptions.ToSpec(flags), } nodeID, err := client.SwarmInit(ctx, req) diff --git a/components/cli/command/swarm/opts.go b/components/cli/command/swarm/opts.go index 7fcf25d136..58330b7f8a 100644 --- a/components/cli/command/swarm/opts.go +++ b/components/cli/command/swarm/opts.go @@ -169,11 +169,20 @@ func addSwarmFlags(flags *pflag.FlagSet, opts *swarmOptions) { flags.Var(&opts.externalCA, flagExternalCA, "Specifications of one or more certificate signing endpoints") } -func (opts *swarmOptions) ToSpec() swarm.Spec { +func (opts *swarmOptions) ToSpec(flags *pflag.FlagSet) swarm.Spec { spec := swarm.Spec{} - spec.Orchestration.TaskHistoryRetentionLimit = opts.taskHistoryLimit - spec.Dispatcher.HeartbeatPeriod = opts.dispatcherHeartbeat - spec.CAConfig.NodeCertExpiry = opts.nodeCertExpiry - spec.CAConfig.ExternalCAs = opts.externalCA.Value() + + if flags.Changed(flagTaskHistoryLimit) { + spec.Orchestration.TaskHistoryRetentionLimit = &opts.taskHistoryLimit + } + if flags.Changed(flagDispatcherHeartbeat) { + spec.Dispatcher.HeartbeatPeriod = opts.dispatcherHeartbeat + } + if flags.Changed(flagCertExpiry) { + spec.CAConfig.NodeCertExpiry = opts.nodeCertExpiry + } + if flags.Changed(flagExternalCA) { + spec.CAConfig.ExternalCAs = opts.externalCA.Value() + } return spec } diff --git a/components/cli/command/swarm/update.go b/components/cli/command/swarm/update.go index 9884b79169..71451e450c 100644 --- a/components/cli/command/swarm/update.go +++ b/components/cli/command/swarm/update.go @@ -58,7 +58,8 @@ func mergeSwarm(swarm *swarm.Swarm, flags *pflag.FlagSet) error { spec := &swarm.Spec if flags.Changed(flagTaskHistoryLimit) { - spec.Orchestration.TaskHistoryRetentionLimit, _ = flags.GetInt64(flagTaskHistoryLimit) + taskHistoryRetentionLimit, _ := flags.GetInt64(flagTaskHistoryLimit) + spec.Orchestration.TaskHistoryRetentionLimit = &taskHistoryRetentionLimit } if flags.Changed(flagDispatcherHeartbeat) { diff --git a/components/cli/command/system/info.go b/components/cli/command/system/info.go index a2d0abad23..1debf755f1 100644 --- a/components/cli/command/system/info.go +++ b/components/cli/command/system/info.go @@ -107,7 +107,11 @@ func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error { fmt.Fprintf(dockerCli.Out(), " Managers: %d\n", info.Swarm.Managers) fmt.Fprintf(dockerCli.Out(), " Nodes: %d\n", info.Swarm.Nodes) fmt.Fprintf(dockerCli.Out(), " Orchestration:\n") - fmt.Fprintf(dockerCli.Out(), " Task History Retention Limit: %d\n", info.Swarm.Cluster.Spec.Orchestration.TaskHistoryRetentionLimit) + taskHistoryRetentionLimit := int64(0) + if info.Swarm.Cluster.Spec.Orchestration.TaskHistoryRetentionLimit != nil { + taskHistoryRetentionLimit = *info.Swarm.Cluster.Spec.Orchestration.TaskHistoryRetentionLimit + } + fmt.Fprintf(dockerCli.Out(), " Task History Retention Limit: %d\n", taskHistoryRetentionLimit) fmt.Fprintf(dockerCli.Out(), " Raft:\n") fmt.Fprintf(dockerCli.Out(), " Snapshot Interval: %d\n", info.Swarm.Cluster.Spec.Raft.SnapshotInterval) fmt.Fprintf(dockerCli.Out(), " Heartbeat Tick: %d\n", info.Swarm.Cluster.Spec.Raft.HeartbeatTick) From 841efb11eb288121da72bc3d419ffaf01d3e0f39 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Tue, 27 Sep 2016 15:27:02 +0000 Subject: [PATCH 169/978] Fix cli/command/service/opts_test.go, and add some extra test cases `m.Set("type=volume,target=/foo,volume-nocopy")` is valid even though it lacks "source" Signed-off-by: Akihiro Suda Upstream-commit: 2126d8160d69e2d577cb12f8ad6a6602baa8e717 Component: cli --- components/cli/command/service/opts.go | 4 --- components/cli/command/service/opts_test.go | 31 +++++++++++++++++++-- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index 7236980e80..1e966f90c6 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -235,10 +235,6 @@ func (m *MountOpt) Set(value string) error { return fmt.Errorf("target is required") } - if mount.VolumeOptions != nil && mount.Source == "" { - return fmt.Errorf("source is required when specifying volume-* options") - } - if mount.Type == mounttypes.TypeBind && mount.VolumeOptions != nil { return fmt.Errorf("cannot mix 'volume-*' options with mount type '%s'", mounttypes.TypeBind) } diff --git a/components/cli/command/service/opts_test.go b/components/cli/command/service/opts_test.go index 30e261b8de..8ef3cacb45 100644 --- a/components/cli/command/service/opts_test.go +++ b/components/cli/command/service/opts_test.go @@ -76,7 +76,7 @@ func TestMountOptString(t *testing.T) { assert.Equal(t, mount.String(), expected) } -func TestMountOptSetNoError(t *testing.T) { +func TestMountOptSetBindNoErrorBind(t *testing.T) { for _, testcase := range []string{ // tests several aliases that should have same result. "type=bind,target=/target,source=/source", @@ -98,6 +98,28 @@ func TestMountOptSetNoError(t *testing.T) { } } +func TestMountOptSetVolumeNoError(t *testing.T) { + for _, testcase := range []string{ + // tests several aliases that should have same result. + "type=volume,target=/target,source=/source", + "type=volume,src=/source,dst=/target", + "type=volume,source=/source,dst=/target", + "type=volume,src=/source,target=/target", + } { + var mount MountOpt + + assert.NilError(t, mount.Set(testcase)) + + mounts := mount.Value() + assert.Equal(t, len(mounts), 1) + assert.Equal(t, mounts[0], mounttypes.Mount{ + Type: mounttypes.TypeVolume, + Source: "/source", + Target: "/target", + }) + } +} + // TestMountOptDefaultType ensures that a mount without the type defaults to a // volume mount. func TestMountOptDefaultType(t *testing.T) { @@ -140,6 +162,10 @@ func TestMountOptDefaultEnableReadOnly(t *testing.T) { assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=1")) assert.Equal(t, m.values[0].ReadOnly, true) + m = MountOpt{} + assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=true")) + assert.Equal(t, m.values[0].ReadOnly, true) + m = MountOpt{} assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=0")) assert.Equal(t, m.values[0].ReadOnly, false) @@ -147,7 +173,8 @@ func TestMountOptDefaultEnableReadOnly(t *testing.T) { func TestMountOptVolumeNoCopy(t *testing.T) { var m MountOpt - assert.Error(t, m.Set("type=volume,target=/foo,volume-nocopy"), "source is required") + assert.NilError(t, m.Set("type=volume,target=/foo,volume-nocopy")) + assert.Equal(t, m.values[0].Source, "") m = MountOpt{} assert.NilError(t, m.Set("type=volume,target=/foo,source=foo")) From d17ffe273dc4d788b4005bb4dee99bf7b0d49f2d Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Wed, 21 Sep 2016 19:16:44 -0700 Subject: [PATCH 170/978] 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 Upstream-commit: e7678f3a37e49533366e4a669f102df70ea1116c Component: cli --- components/cli/client.go | 15 ++++++++------- components/cli/client_test.go | 31 ++++++++++++++++++++++++++++++- components/cli/hijack.go | 7 +------ components/cli/request.go | 8 ++------ components/cli/transport.go | 20 +++++++------------- 5 files changed, 48 insertions(+), 33 deletions(-) diff --git a/components/cli/client.go b/components/cli/client.go index deccb4ab74..ff9efa5700 100644 --- a/components/cli/client.go +++ b/components/cli/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/components/cli/client_test.go b/components/cli/client_test.go index 60e44dc299..eaac339658 100644 --- a/components/cli/client_test.go +++ b/components/cli/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/components/cli/hijack.go b/components/cli/hijack.go index f3461ecf78..dededb7af2 100644 --- a/components/cli/hijack.go +++ b/components/cli/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/components/cli/request.go b/components/cli/request.go index f5c239bf25..07a12657a4 100644 --- a/components/cli/request.go +++ b/components/cli/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/components/cli/transport.go b/components/cli/transport.go index 43a667272d..771d76f06b 100644 --- a/components/cli/transport.go +++ b/components/cli/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 03427eb0022191b05ab307671cac8bf5ca0b959c Mon Sep 17 00:00:00 2001 From: Zhang Wei Date: Wed, 21 Sep 2016 22:06:43 +0800 Subject: [PATCH 171/978] Support parallel kill Signed-off-by: Zhang Wei Upstream-commit: f612b93d336fc9430d6780a84ad2667ebd61d0ef Component: cli --- components/cli/command/container/kill.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/components/cli/command/container/kill.go b/components/cli/command/container/kill.go index 8d9af6f7a6..6da91a40e3 100644 --- a/components/cli/command/container/kill.go +++ b/components/cli/command/container/kill.go @@ -39,8 +39,11 @@ func NewKillCommand(dockerCli *command.DockerCli) *cobra.Command { func runKill(dockerCli *command.DockerCli, opts *killOptions) error { var errs []string ctx := context.Background() + errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, container string) error { + return dockerCli.Client().ContainerKill(ctx, container, opts.signal) + }) for _, name := range opts.containers { - if err := dockerCli.Client().ContainerKill(ctx, name, opts.signal); err != nil { + if err := <-errChan; err != nil { errs = append(errs, err.Error()) } else { fmt.Fprintf(dockerCli.Out(), "%s\n", name) From cf39dde2227f35981de67b9d891f80b166c339ab Mon Sep 17 00:00:00 2001 From: Zhang Wei Date: Wed, 21 Sep 2016 22:35:08 +0800 Subject: [PATCH 172/978] Support parallel rm Signed-off-by: Zhang Wei Upstream-commit: 5c1362ce59a25d0fefb3daaeec601afc66fe334c Component: cli --- components/cli/command/container/rm.go | 29 ++++++++++------------- components/cli/command/container/utils.go | 10 ++++---- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/components/cli/command/container/rm.go b/components/cli/command/container/rm.go index 622a69b510..60724f194b 100644 --- a/components/cli/command/container/rm.go +++ b/components/cli/command/container/rm.go @@ -45,13 +45,22 @@ func runRm(dockerCli *command.DockerCli, opts *rmOptions) error { ctx := context.Background() var errs []string - for _, name := range opts.containers { - if name == "" { + options := types.ContainerRemoveOptions{ + RemoveVolumes: opts.rmVolumes, + RemoveLinks: opts.rmLink, + Force: opts.force, + } + + errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, container string) error { + if container == "" { return fmt.Errorf("Container name cannot be empty") } - name = strings.Trim(name, "/") + container = strings.Trim(container, "/") + return dockerCli.Client().ContainerRemove(ctx, container, options) + }) - if err := removeContainer(dockerCli, ctx, name, opts.rmVolumes, opts.rmLink, opts.force); err != nil { + for _, name := range opts.containers { + if err := <-errChan; err != nil { errs = append(errs, err.Error()) } else { fmt.Fprintf(dockerCli.Out(), "%s\n", name) @@ -62,15 +71,3 @@ func runRm(dockerCli *command.DockerCli, opts *rmOptions) error { } return nil } - -func removeContainer(dockerCli *command.DockerCli, ctx context.Context, container string, removeVolumes, removeLinks, force bool) error { - options := types.ContainerRemoveOptions{ - RemoveVolumes: removeVolumes, - RemoveLinks: removeLinks, - Force: force, - } - if err := dockerCli.Client().ContainerRemove(ctx, container, options); err != nil { - return err - } - return nil -} diff --git a/components/cli/command/container/utils.go b/components/cli/command/container/utils.go index 7e895834f9..2e129f0324 100644 --- a/components/cli/command/container/utils.go +++ b/components/cli/command/container/utils.go @@ -91,8 +91,8 @@ func getExitCode(dockerCli *command.DockerCli, ctx context.Context, containerID return c.State.Running, c.State.ExitCode, nil } -func parallelOperation(ctx context.Context, cids []string, op func(ctx context.Context, id string) error) chan error { - if len(cids) == 0 { +func parallelOperation(ctx context.Context, containers []string, op func(ctx context.Context, container string) error) chan error { + if len(containers) == 0 { return nil } const defaultParallel int = 50 @@ -101,18 +101,18 @@ func parallelOperation(ctx context.Context, cids []string, op func(ctx context.C // make sure result is printed in correct order output := map[string]chan error{} - for _, c := range cids { + for _, c := range containers { output[c] = make(chan error, 1) } go func() { - for _, c := range cids { + for _, c := range containers { err := <-output[c] errChan <- err } }() go func() { - for _, c := range cids { + for _, c := range containers { sem <- struct{}{} // Wait for active queue sem to drain. go func(container string) { output[container] <- op(ctx, container) From d882807966404a998f3c70fcb0db6f8a28061d13 Mon Sep 17 00:00:00 2001 From: allencloud Date: Thu, 29 Sep 2016 15:35:00 +0800 Subject: [PATCH 173/978] add \n in engine labels display in docker node inspect xxx --pretty Signed-off-by: allencloud Upstream-commit: 65b1e54c73989d237cf6ef8522b88673b14c4d69 Component: cli --- components/cli/command/node/inspect.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/components/cli/command/node/inspect.go b/components/cli/command/node/inspect.go index c73b83a87c..a11182f082 100644 --- a/components/cli/command/node/inspect.go +++ b/components/cli/command/node/inspect.go @@ -137,8 +137,7 @@ func printNode(out io.Writer, node swarm.Node) { if len(node.Description.Engine.Labels) != 0 { fmt.Fprintln(out, "Engine Labels:") for k, v := range node.Description.Engine.Labels { - fmt.Fprintf(out, " - %s = %s", k, v) + fmt.Fprintf(out, " - %s = %s\n", k, v) } } - } From 6713fa0784d26c96d987b9cce837c8d5fffa2595 Mon Sep 17 00:00:00 2001 From: Kenfe-Mickael Laventure Date: Tue, 23 Aug 2016 16:19:37 -0700 Subject: [PATCH 174/978] Update Images() to allow retrieving specific image size data Those data include: - size of data shared with other images - size of data unique to a given image - how many containers are using a given image Signed-off-by: Kenfe-Mickael Laventure Upstream-commit: 82dc15836b93b2fb528583e67170806a746a48db Component: cli --- components/cli/command/formatter/image.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/cli/command/formatter/image.go b/components/cli/command/formatter/image.go index 54cb7b62fa..39e05378c7 100644 --- a/components/cli/command/formatter/image.go +++ b/components/cli/command/formatter/image.go @@ -225,5 +225,6 @@ func (c *imageContext) CreatedAt() string { func (c *imageContext) Size() string { c.AddHeader(sizeHeader) - return units.HumanSizeWithPrecision(float64(c.i.Size), 3) + //NOTE: For backward compatibility we need to return VirtualSize + return units.HumanSizeWithPrecision(float64(c.i.VirtualSize), 3) } From a476179204417a2a2d858a0e4b173abac147dfde Mon Sep 17 00:00:00 2001 From: Kenfe-Mickael Laventure Date: Thu, 22 Sep 2016 14:04:34 -0700 Subject: [PATCH 175/978] Add subcommand prune to the container, volume, image and system commands Signed-off-by: Kenfe-Mickael Laventure Upstream-commit: da8eef56ce3eb5380d379046bacc7304171b2fe7 Component: cli --- components/cli/container_prune.go | 26 ++++++++++++++++++++++++++ components/cli/image_prune.go | 26 ++++++++++++++++++++++++++ components/cli/interface.go | 4 ++++ components/cli/volume_prune.go | 26 ++++++++++++++++++++++++++ 4 files changed, 82 insertions(+) create mode 100644 components/cli/container_prune.go create mode 100644 components/cli/image_prune.go create mode 100644 components/cli/volume_prune.go diff --git a/components/cli/container_prune.go b/components/cli/container_prune.go new file mode 100644 index 0000000000..0d8bd3292c --- /dev/null +++ b/components/cli/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/components/cli/image_prune.go b/components/cli/image_prune.go new file mode 100644 index 0000000000..f6752e5043 --- /dev/null +++ b/components/cli/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/components/cli/interface.go b/components/cli/interface.go index 81320918b3..de06b848ae 100644 --- a/components/cli/interface.go +++ b/components/cli/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/components/cli/volume_prune.go b/components/cli/volume_prune.go new file mode 100644 index 0000000000..e7ea7b591d --- /dev/null +++ b/components/cli/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 c80b266f918ce41fd5b1dec6ca074241e65f5a9d Mon Sep 17 00:00:00 2001 From: Kenfe-Mickael Laventure Date: Thu, 22 Sep 2016 14:04:34 -0700 Subject: [PATCH 176/978] Add subcommand prune to the container, volume, image and system commands Signed-off-by: Kenfe-Mickael Laventure Upstream-commit: 6f8bb41ecbaf076c96493a00de1ab87c748ad373 Component: cli --- components/cli/command/container/cmd.go | 1 + components/cli/command/container/prune.go | 74 +++++++++++++++ components/cli/command/container/stats.go | 3 +- .../cli/command/{system => }/events_utils.go | 2 +- components/cli/command/image/cmd.go | 2 + components/cli/command/image/prune.go | 90 +++++++++++++++++++ components/cli/command/prune/prune.go | 39 ++++++++ components/cli/command/system/cmd.go | 1 + components/cli/command/system/prune.go | 90 +++++++++++++++++++ components/cli/command/utils.go | 22 +++++ components/cli/command/volume/cmd.go | 1 + components/cli/command/volume/prune.go | 74 +++++++++++++++ 12 files changed, 396 insertions(+), 3 deletions(-) create mode 100644 components/cli/command/container/prune.go rename components/cli/command/{system => }/events_utils.go (98%) create mode 100644 components/cli/command/image/prune.go create mode 100644 components/cli/command/prune/prune.go create mode 100644 components/cli/command/system/prune.go create mode 100644 components/cli/command/volume/prune.go diff --git a/components/cli/command/container/cmd.go b/components/cli/command/container/cmd.go index da9ea6d41d..f06b863b58 100644 --- a/components/cli/command/container/cmd.go +++ b/components/cli/command/container/cmd.go @@ -44,6 +44,7 @@ func NewContainerCommand(dockerCli *command.DockerCli) *cobra.Command { NewWaitCommand(dockerCli), newListCommand(dockerCli), newInspectCommand(dockerCli), + NewPruneCommand(dockerCli), ) return cmd } diff --git a/components/cli/command/container/prune.go b/components/cli/command/container/prune.go new file mode 100644 index 0000000000..13e283a8b2 --- /dev/null +++ b/components/cli/command/container/prune.go @@ -0,0 +1,74 @@ +package container + +import ( + "fmt" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + units "github.com/docker/go-units" + "github.com/spf13/cobra" +) + +type pruneOptions struct { + force bool +} + +// NewPruneCommand returns a new cobra prune command for containers +func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts pruneOptions + + cmd := &cobra.Command{ + Use: "prune", + Short: "Remove all stopped containers", + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + spaceReclaimed, output, err := runPrune(dockerCli, opts) + if err != nil { + return err + } + if output != "" { + fmt.Fprintln(dockerCli.Out(), output) + } + fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) + return nil + }, + } + + flags := cmd.Flags() + flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation") + + return cmd +} + +const warning = `WARNING! This will remove all stopped containers. +Are you sure you want to continue? [y/N] ` + +func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed uint64, output string, err error) { + if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) { + return + } + + report, err := dockerCli.Client().ContainersPrune(context.Background(), types.ContainersPruneConfig{}) + if err != nil { + return + } + + if len(report.ContainersDeleted) > 0 { + output = "Deleted Containers:" + for _, id := range report.ContainersDeleted { + output += id + "\n" + } + spaceReclaimed = report.SpaceReclaimed + } + + return +} + +// RunPrune call the Container Prune API +// This returns the amount of space reclaimed and a detailed output string +func RunPrune(dockerCli *command.DockerCli) (uint64, string, error) { + return runPrune(dockerCli, pruneOptions{force: true}) +} diff --git a/components/cli/command/container/stats.go b/components/cli/command/container/stats.go index 394302d087..2e3714486b 100644 --- a/components/cli/command/container/stats.go +++ b/components/cli/command/container/stats.go @@ -15,7 +15,6 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/formatter" - "github.com/docker/docker/cli/command/system" "github.com/spf13/cobra" ) @@ -110,7 +109,7 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error { // retrieving the list of running containers to avoid a race where we // would "miss" a creation. started := make(chan struct{}) - eh := system.InitEventHandler() + eh := command.InitEventHandler() eh.Handle("create", func(e events.Message) { if opts.all { s := formatter.NewContainerStats(e.ID[:12], daemonOSType) diff --git a/components/cli/command/system/events_utils.go b/components/cli/command/events_utils.go similarity index 98% rename from components/cli/command/system/events_utils.go rename to components/cli/command/events_utils.go index b0dd909d15..e710c97576 100644 --- a/components/cli/command/system/events_utils.go +++ b/components/cli/command/events_utils.go @@ -1,4 +1,4 @@ -package system +package command import ( "sync" diff --git a/components/cli/command/image/cmd.go b/components/cli/command/image/cmd.go index f60ffeeb8f..6f8e7b7d4b 100644 --- a/components/cli/command/image/cmd.go +++ b/components/cli/command/image/cmd.go @@ -31,6 +31,8 @@ func NewImageCommand(dockerCli *command.DockerCli) *cobra.Command { newListCommand(dockerCli), newRemoveCommand(dockerCli), newInspectCommand(dockerCli), + NewPruneCommand(dockerCli), ) + return cmd } diff --git a/components/cli/command/image/prune.go b/components/cli/command/image/prune.go new file mode 100644 index 0000000000..6944664a54 --- /dev/null +++ b/components/cli/command/image/prune.go @@ -0,0 +1,90 @@ +package image + +import ( + "fmt" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + units "github.com/docker/go-units" + "github.com/spf13/cobra" +) + +type pruneOptions struct { + force bool + all bool +} + +// NewPruneCommand returns a new cobra prune command for images +func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts pruneOptions + + cmd := &cobra.Command{ + Use: "prune", + Short: "Remove unused images", + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + spaceReclaimed, output, err := runPrune(dockerCli, opts) + if err != nil { + return err + } + if output != "" { + fmt.Fprintln(dockerCli.Out(), output) + } + fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) + return nil + }, + } + + flags := cmd.Flags() + flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation") + flags.BoolVarP(&opts.all, "all", "a", false, "Remove all unused images, not just dangling ones") + + return cmd +} + +const ( + allImageWarning = `WARNING! This will remove all images without at least one container associated to them. +Are you sure you want to continue?` + danglingWarning = `WARNING! This will remove all dangling images. +Are you sure you want to continue?` +) + +func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed uint64, output string, err error) { + warning := danglingWarning + if opts.all { + warning = allImageWarning + } + if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) { + return + } + + report, err := dockerCli.Client().ImagesPrune(context.Background(), types.ImagesPruneConfig{ + DanglingOnly: !opts.all, + }) + if err != nil { + return + } + + if len(report.ImagesDeleted) > 0 { + output = "Deleted Images:\n" + for _, st := range report.ImagesDeleted { + if st.Untagged != "" { + output += fmt.Sprintln("untagged:", st.Untagged) + } else { + output += fmt.Sprintln("deleted:", st.Deleted) + } + } + spaceReclaimed = report.SpaceReclaimed + } + + return +} + +// RunPrune call the Image Prune API +// This returns the amount of space reclaimed and a detailed output string +func RunPrune(dockerCli *command.DockerCli, all bool) (uint64, string, error) { + return runPrune(dockerCli, pruneOptions{force: true, all: all}) +} diff --git a/components/cli/command/prune/prune.go b/components/cli/command/prune/prune.go new file mode 100644 index 0000000000..0b1374eda9 --- /dev/null +++ b/components/cli/command/prune/prune.go @@ -0,0 +1,39 @@ +package prune + +import ( + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/container" + "github.com/docker/docker/cli/command/image" + "github.com/docker/docker/cli/command/volume" + "github.com/spf13/cobra" +) + +// NewContainerPruneCommand return a cobra prune command for containers +func NewContainerPruneCommand(dockerCli *command.DockerCli) *cobra.Command { + return container.NewPruneCommand(dockerCli) +} + +// NewVolumePruneCommand return a cobra prune command for volumes +func NewVolumePruneCommand(dockerCli *command.DockerCli) *cobra.Command { + return volume.NewPruneCommand(dockerCli) +} + +// NewImagePruneCommand return a cobra prune command for images +func NewImagePruneCommand(dockerCli *command.DockerCli) *cobra.Command { + return image.NewPruneCommand(dockerCli) +} + +// RunContainerPrune execute a prune command for containers +func RunContainerPrune(dockerCli *command.DockerCli) (uint64, string, error) { + return container.RunPrune(dockerCli) +} + +// RunVolumePrune execute a prune command for volumes +func RunVolumePrune(dockerCli *command.DockerCli) (uint64, string, error) { + return volume.RunPrune(dockerCli) +} + +// RunImagePrune execute a prune command for images +func RunImagePrune(dockerCli *command.DockerCli, all bool) (uint64, string, error) { + return image.RunPrune(dockerCli, all) +} diff --git a/components/cli/command/system/cmd.go b/components/cli/command/system/cmd.go index 8ce9d93ae7..f967c1b72e 100644 --- a/components/cli/command/system/cmd.go +++ b/components/cli/command/system/cmd.go @@ -22,6 +22,7 @@ func NewSystemCommand(dockerCli *command.DockerCli) *cobra.Command { cmd.AddCommand( NewEventsCommand(dockerCli), NewInfoCommand(dockerCli), + NewPruneCommand(dockerCli), ) return cmd } diff --git a/components/cli/command/system/prune.go b/components/cli/command/system/prune.go new file mode 100644 index 0000000000..4a9e952ada --- /dev/null +++ b/components/cli/command/system/prune.go @@ -0,0 +1,90 @@ +package system + +import ( + "fmt" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/prune" + units "github.com/docker/go-units" + "github.com/spf13/cobra" +) + +type pruneOptions struct { + force bool + all bool +} + +// NewPruneCommand creates a new cobra.Command for `docker du` +func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts pruneOptions + + cmd := &cobra.Command{ + Use: "prune [COMMAND]", + Short: "Remove unused data.", + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runPrune(dockerCli, opts) + }, + } + + flags := cmd.Flags() + flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation") + flags.BoolVarP(&opts.all, "all", "a", false, "Remove all unused images not just dangling ones") + + return cmd +} + +const ( + warning = `WARNING! This will remove: + - all stopped containers + - all volumes not used by at least one container + %s +Are you sure you want to continue?` + + danglingImageDesc = "- all dangling images" + allImageDesc = `- all images without at least one container associated to them` +) + +func runPrune(dockerCli *command.DockerCli, opts pruneOptions) error { + var message string + + if opts.all { + message = fmt.Sprintf(warning, allImageDesc) + } else { + message = fmt.Sprintf(warning, danglingImageDesc) + } + + if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), message) { + return nil + } + + var spaceReclaimed uint64 + + for _, pruneFn := range []func(dockerCli *command.DockerCli) (uint64, string, error){ + prune.RunContainerPrune, + prune.RunVolumePrune, + } { + spc, output, err := pruneFn(dockerCli) + if err != nil { + return err + } + if spc > 0 { + spaceReclaimed += spc + fmt.Fprintln(dockerCli.Out(), output) + } + } + + spc, output, err := prune.RunImagePrune(dockerCli, opts.all) + if err != nil { + return err + } + if spc > 0 { + spaceReclaimed += spc + fmt.Fprintln(dockerCli.Out(), output) + } + + fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) + + return nil +} diff --git a/components/cli/command/utils.go b/components/cli/command/utils.go index bceb7b335c..e768cf770d 100644 --- a/components/cli/command/utils.go +++ b/components/cli/command/utils.go @@ -57,3 +57,25 @@ func PrettyPrint(i interface{}) string { return capitalizeFirst(fmt.Sprintf("%s", t)) } } + +// PromptForConfirmation request and check confirmation from user. +// This will display the provided message followed by ' [y/N] '. If +// the user input 'y' or 'Y' it returns true other false. If no +// message is provided "Are you sure you want to proceeed? [y/N] " +// will be used instead. +func PromptForConfirmation(ins *InStream, outs *OutStream, message string) bool { + if message == "" { + message = "Are you sure you want to proceeed?" + } + message += " [y/N] " + + fmt.Fprintf(outs, message) + + answer := "" + n, _ := fmt.Fscan(ins, &answer) + if n != 1 || (answer != "y" && answer != "Y") { + return false + } + + return true +} diff --git a/components/cli/command/volume/cmd.go b/components/cli/command/volume/cmd.go index caf6afcaa3..5f39d3cf33 100644 --- a/components/cli/command/volume/cmd.go +++ b/components/cli/command/volume/cmd.go @@ -25,6 +25,7 @@ func NewVolumeCommand(dockerCli *command.DockerCli) *cobra.Command { newInspectCommand(dockerCli), newListCommand(dockerCli), newRemoveCommand(dockerCli), + NewPruneCommand(dockerCli), ) return cmd } diff --git a/components/cli/command/volume/prune.go b/components/cli/command/volume/prune.go new file mode 100644 index 0000000000..59f3c94635 --- /dev/null +++ b/components/cli/command/volume/prune.go @@ -0,0 +1,74 @@ +package volume + +import ( + "fmt" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + units "github.com/docker/go-units" + "github.com/spf13/cobra" +) + +type pruneOptions struct { + force bool +} + +// NewPruneCommand returns a new cobra prune command for volumes +func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts pruneOptions + + cmd := &cobra.Command{ + Use: "prune", + Short: "Remove all unused volumes", + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + spaceReclaimed, output, err := runPrune(dockerCli, opts) + if err != nil { + return err + } + if output != "" { + fmt.Fprintln(dockerCli.Out(), output) + } + fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) + return nil + }, + } + + flags := cmd.Flags() + flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation") + + return cmd +} + +const warning = `WARNING! This will remove all volumes not used by at least one container. +Are you sure you want to continue?` + +func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed uint64, output string, err error) { + if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) { + return + } + + report, err := dockerCli.Client().VolumesPrune(context.Background(), types.VolumesPruneConfig{}) + if err != nil { + return + } + + if len(report.VolumesDeleted) > 0 { + output = "Deleted Volumes:\n" + for _, id := range report.VolumesDeleted { + output += id + "\n" + } + spaceReclaimed = report.SpaceReclaimed + } + + return +} + +// RunPrune call the Volume Prune API +// This returns the amount of space reclaimed and a detailed output string +func RunPrune(dockerCli *command.DockerCli) (uint64, string, error) { + return runPrune(dockerCli, pruneOptions{force: true}) +} From bf09a06c6f1129ffef992d517d4b45f7c17e3688 Mon Sep 17 00:00:00 2001 From: Kenfe-Mickael Laventure Date: Thu, 8 Sep 2016 13:45:05 -0700 Subject: [PATCH 177/978] Add DiskUsage method to SystemApiclient Signed-off-by: Kenfe-Mickael Laventure Upstream-commit: d7efdb095ed8c738f2c734cbd36102f97ec68d6f Component: cli --- components/cli/disk_usage.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 components/cli/disk_usage.go diff --git a/components/cli/disk_usage.go b/components/cli/disk_usage.go new file mode 100644 index 0000000000..03c80b39af --- /dev/null +++ b/components/cli/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 8d6b5149c7c8159db6c6324223f2d41a6a9b07cc Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 29 Sep 2016 17:59:52 -0400 Subject: [PATCH 178/978] Use ListOpt for labels. Signed-off-by: Daniel Nephin Upstream-commit: d6b5a807d7a89738e6d44d4053669cc40549ab6a Component: cli --- components/cli/command/image/build.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go index 51d0ea9f08..fa7abe4023 100644 --- a/components/cli/command/image/build.go +++ b/components/cli/command/image/build.go @@ -37,7 +37,7 @@ type buildOptions struct { context string dockerfileName string tags opts.ListOpts - labels []string + labels opts.ListOpts buildArgs opts.ListOpts ulimits *runconfigopts.UlimitOpt memory string @@ -65,6 +65,7 @@ func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command { tags: opts.NewListOpts(validateTag), buildArgs: opts.NewListOpts(runconfigopts.ValidateArg), ulimits: runconfigopts.NewUlimitOpt(&ulimits), + labels: opts.NewListOpts(runconfigopts.ValidateEnv), } cmd := &cobra.Command{ @@ -93,7 +94,7 @@ func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command { flags.StringVar(&options.cpuSetMems, "cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)") flags.StringVar(&options.cgroupParent, "cgroup-parent", "", "Optional parent cgroup for the container") flags.StringVar(&options.isolation, "isolation", "", "Container isolation technology") - flags.StringSliceVar(&options.labels, "label", []string{}, "Set metadata for an image") + flags.Var(&options.labels, "label", "Set metadata for an image") flags.BoolVar(&options.noCache, "no-cache", false, "Do not use cache when building the image") flags.BoolVar(&options.rm, "rm", true, "Remove intermediate containers after a successful build") flags.BoolVar(&options.forceRm, "force-rm", false, "Always remove intermediate containers") @@ -290,7 +291,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { Ulimits: options.ulimits.GetList(), BuildArgs: runconfigopts.ConvertKVStringsToMap(options.buildArgs.GetAll()), AuthConfigs: authConfig, - Labels: runconfigopts.ConvertKVStringsToMap(options.labels), + Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()), CacheFrom: options.cacheFrom, } From 326cca001f3627798ccfa41a365f8c7d814717a0 Mon Sep 17 00:00:00 2001 From: Kenfe-Mickael Laventure Date: Tue, 23 Aug 2016 16:37:37 -0700 Subject: [PATCH 179/978] Add new df subcomand to the system command This command display the state of the data usage of the docker daemon. Signed-off-by: Kenfe-Mickael Laventure Upstream-commit: 3f8c4be283305f9654235e66f1d60b25deb0f898 Component: cli --- components/cli/command/formatter/container.go | 14 + .../cli/command/formatter/disk_usage.go | 331 ++++++++++++++++++ components/cli/command/formatter/image.go | 30 ++ .../cli/command/formatter/image_test.go | 2 +- components/cli/command/formatter/volume.go | 18 + components/cli/command/system/cmd.go | 1 + components/cli/command/system/df.go | 55 +++ 7 files changed, 450 insertions(+), 1 deletion(-) create mode 100644 components/cli/command/formatter/disk_usage.go create mode 100644 components/cli/command/system/df.go diff --git a/components/cli/command/formatter/container.go b/components/cli/command/formatter/container.go index 30a6492476..ceef75890f 100644 --- a/components/cli/command/formatter/container.go +++ b/components/cli/command/formatter/container.go @@ -23,6 +23,7 @@ const ( statusHeader = "STATUS" portsHeader = "PORTS" mountsHeader = "MOUNTS" + localVolumes = "LOCAL VOLUMES" ) // NewContainerFormat returns a Format for rendering using a Context @@ -199,3 +200,16 @@ func (c *containerContext) Mounts() string { } return strings.Join(mounts, ",") } + +func (c *containerContext) LocalVolumes() string { + c.AddHeader(localVolumes) + + count := 0 + for _, m := range c.c.Mounts { + if m.Driver == "local" { + count++ + } + } + + return fmt.Sprintf("%d", count) +} diff --git a/components/cli/command/formatter/disk_usage.go b/components/cli/command/formatter/disk_usage.go new file mode 100644 index 0000000000..866e9bd04a --- /dev/null +++ b/components/cli/command/formatter/disk_usage.go @@ -0,0 +1,331 @@ +package formatter + +import ( + "bytes" + "fmt" + "strings" + "text/template" + + "github.com/docker/distribution/reference" + "github.com/docker/docker/api/types" + units "github.com/docker/go-units" +) + +const ( + defaultDiskUsageImageTableFormat = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.VirtualSize}}\t{{.SharedSize}}\t{{.UniqueSize}}\t{{.Containers}}" + defaultDiskUsageContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.LocalVolumes}}\t{{.Size}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Names}}" + defaultDiskUsageVolumeTableFormat = "table {{.Name}}\t{{.Links}}\t{{.Size}}" + defaultDiskUsageTableFormat = "table {{.Type}}\t{{.TotalCount}}\t{{.Active}}\t{{.Size}}\t{{.Reclaimable}}" + + typeHeader = "TYPE" + totalHeader = "TOTAL" + activeHeader = "ACTIVE" + reclaimableHeader = "RECLAIMABLE" + containersHeader = "CONTAINERS" + sharedSizeHeader = "SHARED SIZE" + uniqueSizeHeader = "UNIQUE SiZE" +) + +// DiskUsageContext contains disk usage specific information required by the formater, encapsulate a Context struct. +type DiskUsageContext struct { + Context + Verbose bool + LayersSize int64 + Images []*types.Image + Containers []*types.Container + Volumes []*types.Volume +} + +func (ctx *DiskUsageContext) startSubsection(format string) (*template.Template, error) { + ctx.buffer = bytes.NewBufferString("") + ctx.header = "" + ctx.Format = Format(format) + ctx.preFormat() + + return ctx.parseFormat() +} + +func (ctx *DiskUsageContext) Write() { + if ctx.Verbose == false { + ctx.buffer = bytes.NewBufferString("") + ctx.Format = defaultDiskUsageTableFormat + ctx.preFormat() + + tmpl, err := ctx.parseFormat() + if err != nil { + return + } + + err = ctx.contextFormat(tmpl, &diskUsageImagesContext{ + totalSize: ctx.LayersSize, + images: ctx.Images, + }) + if err != nil { + return + } + err = ctx.contextFormat(tmpl, &diskUsageContainersContext{ + containers: ctx.Containers, + }) + if err != nil { + return + } + + err = ctx.contextFormat(tmpl, &diskUsageVolumesContext{ + volumes: ctx.Volumes, + }) + if err != nil { + return + } + + ctx.postFormat(tmpl, &diskUsageContainersContext{containers: []*types.Container{}}) + + return + } + + // First images + tmpl, err := ctx.startSubsection(defaultDiskUsageImageTableFormat) + if err != nil { + return + } + + ctx.Output.Write([]byte("Images space usage:\n\n")) + for _, i := range ctx.Images { + repo := "" + tag := "" + if len(i.RepoTags) > 0 && !isDangling(*i) { + // Only show the first tag + ref, err := reference.ParseNamed(i.RepoTags[0]) + if err != nil { + continue + } + if nt, ok := ref.(reference.NamedTagged); ok { + repo = ref.Name() + tag = nt.Tag() + } + } + + err = ctx.contextFormat(tmpl, &imageContext{ + repo: repo, + tag: tag, + trunc: true, + i: *i, + }) + if err != nil { + return + } + } + ctx.postFormat(tmpl, &imageContext{}) + + // Now containers + ctx.Output.Write([]byte("\nContainers space usage:\n\n")) + tmpl, err = ctx.startSubsection(defaultDiskUsageContainerTableFormat) + if err != nil { + return + } + for _, c := range ctx.Containers { + // Don't display the virtual size + c.SizeRootFs = 0 + err = ctx.contextFormat(tmpl, &containerContext{ + trunc: true, + c: *c, + }) + if err != nil { + return + } + } + ctx.postFormat(tmpl, &containerContext{}) + + // And volumes + ctx.Output.Write([]byte("\nLocal Volumes space usage:\n\n")) + tmpl, err = ctx.startSubsection(defaultDiskUsageVolumeTableFormat) + if err != nil { + return + } + for _, v := range ctx.Volumes { + err = ctx.contextFormat(tmpl, &volumeContext{ + v: *v, + }) + if err != nil { + return + } + } + ctx.postFormat(tmpl, &volumeContext{v: types.Volume{}}) +} + +type diskUsageImagesContext struct { + HeaderContext + totalSize int64 + images []*types.Image +} + +func (c *diskUsageImagesContext) Type() string { + c.AddHeader(typeHeader) + return "Images" +} + +func (c *diskUsageImagesContext) TotalCount() string { + c.AddHeader(totalHeader) + return fmt.Sprintf("%d", len(c.images)) +} + +func (c *diskUsageImagesContext) Active() string { + c.AddHeader(activeHeader) + used := 0 + for _, i := range c.images { + if i.Containers > 0 { + used++ + } + } + + return fmt.Sprintf("%d", used) +} + +func (c *diskUsageImagesContext) Size() string { + c.AddHeader(sizeHeader) + return units.HumanSize(float64(c.totalSize)) + +} + +func (c *diskUsageImagesContext) Reclaimable() string { + var used int64 + + c.AddHeader(reclaimableHeader) + for _, i := range c.images { + if i.Containers != 0 { + used += i.Size + } + } + + reclaimable := c.totalSize - used + if c.totalSize > 0 { + return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/c.totalSize) + } + return fmt.Sprintf("%s", units.HumanSize(float64(reclaimable))) +} + +type diskUsageContainersContext struct { + HeaderContext + verbose bool + containers []*types.Container +} + +func (c *diskUsageContainersContext) Type() string { + c.AddHeader(typeHeader) + return "Containers" +} + +func (c *diskUsageContainersContext) TotalCount() string { + c.AddHeader(totalHeader) + return fmt.Sprintf("%d", len(c.containers)) +} + +func (c *diskUsageContainersContext) isActive(container types.Container) bool { + return strings.Contains(container.State, "running") || + strings.Contains(container.State, "paused") || + strings.Contains(container.State, "restarting") +} + +func (c *diskUsageContainersContext) Active() string { + c.AddHeader(activeHeader) + used := 0 + for _, container := range c.containers { + if c.isActive(*container) { + used++ + } + } + + return fmt.Sprintf("%d", used) +} + +func (c *diskUsageContainersContext) Size() string { + var size int64 + + c.AddHeader(sizeHeader) + for _, container := range c.containers { + size += container.SizeRw + } + + return units.HumanSize(float64(size)) +} + +func (c *diskUsageContainersContext) Reclaimable() string { + var reclaimable int64 + var totalSize int64 + + c.AddHeader(reclaimableHeader) + for _, container := range c.containers { + if !c.isActive(*container) { + reclaimable += container.SizeRw + } + totalSize += container.SizeRw + } + + if totalSize > 0 { + return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/totalSize) + } + + return fmt.Sprintf("%s", units.HumanSize(float64(reclaimable))) +} + +type diskUsageVolumesContext struct { + HeaderContext + verbose bool + volumes []*types.Volume +} + +func (c *diskUsageVolumesContext) Type() string { + c.AddHeader(typeHeader) + return "Local Volumes" +} + +func (c *diskUsageVolumesContext) TotalCount() string { + c.AddHeader(totalHeader) + return fmt.Sprintf("%d", len(c.volumes)) +} + +func (c *diskUsageVolumesContext) Active() string { + c.AddHeader(activeHeader) + + used := 0 + for _, v := range c.volumes { + if v.RefCount > 0 { + used++ + } + } + + return fmt.Sprintf("%d", used) +} + +func (c *diskUsageVolumesContext) Size() string { + var size int64 + + c.AddHeader(sizeHeader) + for _, v := range c.volumes { + if v.Size != -1 { + size += v.Size + } + } + + return units.HumanSize(float64(size)) +} + +func (c *diskUsageVolumesContext) Reclaimable() string { + var reclaimable int64 + var totalSize int64 + + c.AddHeader(reclaimableHeader) + for _, v := range c.volumes { + if v.Size != -1 { + if v.RefCount == 0 { + reclaimable += v.Size + } + totalSize += v.Size + } + } + + if totalSize > 0 { + return fmt.Sprintf("%s (%v%%)", units.HumanSize(float64(reclaimable)), (reclaimable*100)/totalSize) + } + + return fmt.Sprintf("%s", units.HumanSize(float64(reclaimable))) +} diff --git a/components/cli/command/formatter/image.go b/components/cli/command/formatter/image.go index 39e05378c7..1e71bda3aa 100644 --- a/components/cli/command/formatter/image.go +++ b/components/cli/command/formatter/image.go @@ -1,6 +1,7 @@ package formatter import ( + "fmt" "time" "github.com/docker/docker/api/types" @@ -228,3 +229,32 @@ func (c *imageContext) Size() string { //NOTE: For backward compatibility we need to return VirtualSize return units.HumanSizeWithPrecision(float64(c.i.VirtualSize), 3) } + +func (c *imageContext) Containers() string { + c.AddHeader(containersHeader) + if c.i.Containers == -1 { + return "N/A" + } + return fmt.Sprintf("%d", c.i.Containers) +} + +func (c *imageContext) VirtualSize() string { + c.AddHeader(sizeHeader) + return units.HumanSize(float64(c.i.VirtualSize)) +} + +func (c *imageContext) SharedSize() string { + c.AddHeader(sharedSizeHeader) + if c.i.SharedSize == -1 { + return "N/A" + } + return units.HumanSize(float64(c.i.SharedSize)) +} + +func (c *imageContext) UniqueSize() string { + c.AddHeader(uniqueSizeHeader) + if c.i.Size == -1 { + return "N/A" + } + return units.HumanSize(float64(c.i.Size)) +} diff --git a/components/cli/command/formatter/image_test.go b/components/cli/command/formatter/image_test.go index 6dc7f73db3..73b3c3f2e9 100644 --- a/components/cli/command/formatter/image_test.go +++ b/components/cli/command/formatter/image_test.go @@ -32,7 +32,7 @@ func TestImageContext(t *testing.T) { trunc: false, }, imageID, imageIDHeader, ctx.ID}, {imageContext{ - i: types.Image{Size: 10}, + i: types.Image{Size: 10, VirtualSize: 10}, trunc: true, }, "10 B", sizeHeader, ctx.Size}, {imageContext{ diff --git a/components/cli/command/formatter/volume.go b/components/cli/command/formatter/volume.go index e41ee266bf..8fb11732e3 100644 --- a/components/cli/command/formatter/volume.go +++ b/components/cli/command/formatter/volume.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/docker/docker/api/types" + units "github.com/docker/go-units" ) const ( @@ -12,6 +13,7 @@ const ( defaultVolumeTableFormat = "table {{.Driver}}\t{{.Name}}" mountpointHeader = "MOUNTPOINT" + linksHeader = "LINKS" // Status header ? ) @@ -96,3 +98,19 @@ func (c *volumeContext) Label(name string) string { } return c.v.Labels[name] } + +func (c *volumeContext) Links() string { + c.AddHeader(linksHeader) + if c.v.Size == -1 { + return "N/A" + } + return fmt.Sprintf("%d", c.v.RefCount) +} + +func (c *volumeContext) Size() string { + c.AddHeader(sizeHeader) + if c.v.Size == -1 { + return "N/A" + } + return units.HumanSize(float64(c.v.Size)) +} diff --git a/components/cli/command/system/cmd.go b/components/cli/command/system/cmd.go index f967c1b72e..46caa2491c 100644 --- a/components/cli/command/system/cmd.go +++ b/components/cli/command/system/cmd.go @@ -22,6 +22,7 @@ func NewSystemCommand(dockerCli *command.DockerCli) *cobra.Command { cmd.AddCommand( NewEventsCommand(dockerCli), NewInfoCommand(dockerCli), + NewDiskUsageCommand(dockerCli), NewPruneCommand(dockerCli), ) return cmd diff --git a/components/cli/command/system/df.go b/components/cli/command/system/df.go new file mode 100644 index 0000000000..085d680fe8 --- /dev/null +++ b/components/cli/command/system/df.go @@ -0,0 +1,55 @@ +package system + +import ( + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/formatter" + "github.com/spf13/cobra" + "golang.org/x/net/context" +) + +type diskUsageOptions struct { + verbose bool +} + +// NewDiskUsageCommand creates a new cobra.Command for `docker df` +func NewDiskUsageCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts diskUsageOptions + + cmd := &cobra.Command{ + Use: "df [OPTIONS]", + Short: "Show docker disk usage", + Args: cli.RequiresMaxArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runDiskUsage(dockerCli, opts) + }, + } + + flags := cmd.Flags() + + flags.BoolVarP(&opts.verbose, "verbose", "v", false, "Show detailed information on space usage") + + return cmd +} + +func runDiskUsage(dockerCli *command.DockerCli, opts diskUsageOptions) error { + du, err := dockerCli.Client().DiskUsage(context.Background()) + if err != nil { + return err + } + + duCtx := formatter.DiskUsageContext{ + Context: formatter.Context{ + Output: dockerCli.Out(), + }, + LayersSize: du.LayersSize, + Images: du.Images, + Containers: du.Containers, + Volumes: du.Volumes, + Verbose: opts.verbose, + } + + duCtx.Write() + + return nil +} From ad88944cf17b1b6851dae2d5cf11bdd5af936bdc Mon Sep 17 00:00:00 2001 From: Paul Kehrer Date: Thu, 18 Aug 2016 16:35:23 +0800 Subject: [PATCH 180/978] Add support for compressing build context during image build When sending a build context to a remote server it may be (significantly) advantageous to compress the build context. This commit adds support for gz compression when constructing a build context using a command like "docker build --compress ." Signed-off-by: Paul Kehrer Upstream-commit: e25646bbc039d5b428be77a7bf9742279fec9180 Component: cli --- components/cli/command/image/build.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go index 51d0ea9f08..2bd80f7082 100644 --- a/components/cli/command/image/build.go +++ b/components/cli/command/image/build.go @@ -56,6 +56,7 @@ type buildOptions struct { forceRm bool pull bool cacheFrom []string + compress bool } // NewBuildCommand creates a new `docker build` command @@ -100,6 +101,7 @@ func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command { flags.BoolVarP(&options.quiet, "quiet", "q", false, "Suppress the build output and print image ID on success") flags.BoolVar(&options.pull, "pull", false, "Always attempt to pull a newer version of the image") flags.StringSliceVar(&options.cacheFrom, "cache-from", []string{}, "Images to consider as cache sources") + flags.BoolVar(&options.compress, "compress", false, "Compress the build context using gzip") command.AddTrustedFlags(flags, true) @@ -208,8 +210,12 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { includes = append(includes, ".dockerignore", relDockerfile) } + compression := archive.Uncompressed + if options.compress { + compression = archive.Gzip + } buildCtx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{ - Compression: archive.Uncompressed, + Compression: compression, ExcludePatterns: excludes, IncludeFiles: includes, }) From 38c3916d9938a87b0f78e70fd9423b194014861d Mon Sep 17 00:00:00 2001 From: Sean Rodman Date: Wed, 21 Sep 2016 16:04:44 -0500 Subject: [PATCH 181/978] 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 Upstream-commit: a318ab842a3dfa7988ada3e81893c92340308bd6 Component: cli --- components/cli/request.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/components/cli/request.go b/components/cli/request.go index f5c239bf25..0749ae3254 100644 --- a/components/cli/request.go +++ b/components/cli/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 ae30bfae31626611b40c655ac4ce32db9b44193c Mon Sep 17 00:00:00 2001 From: John Howard Date: Tue, 7 Jun 2016 12:15:50 -0700 Subject: [PATCH 182/978] Windows: Support credential specs Signed-off-by: John Howard Upstream-commit: 6bc667128a8565ed33e19170a2802e218b2d13c0 Component: cli --- components/cli/image_build.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/cli/image_build.go b/components/cli/image_build.go index 0094602a6e..3abd87025e 100644 --- a/components/cli/image_build.go +++ b/components/cli/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 9d0867981a7d90da5d1c647b872c648737bdb6f7 Mon Sep 17 00:00:00 2001 From: John Howard Date: Tue, 7 Jun 2016 12:15:50 -0700 Subject: [PATCH 183/978] Windows: Support credential specs Signed-off-by: John Howard Upstream-commit: e307da732af29d6b96950290697567c758bdf11c Component: cli --- components/cli/command/image/build.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go index ccfebb9834..19fd4aa709 100644 --- a/components/cli/command/image/build.go +++ b/components/cli/command/image/build.go @@ -57,6 +57,7 @@ type buildOptions struct { pull bool cacheFrom []string compress bool + securityOpt []string } // NewBuildCommand creates a new `docker build` command @@ -103,6 +104,7 @@ func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command { flags.BoolVar(&options.pull, "pull", false, "Always attempt to pull a newer version of the image") flags.StringSliceVar(&options.cacheFrom, "cache-from", []string{}, "Images to consider as cache sources") flags.BoolVar(&options.compress, "compress", false, "Compress the build context using gzip") + flags.StringSliceVar(&options.securityOpt, "security-opt", []string{}, "Security options") command.AddTrustedFlags(flags, true) @@ -299,6 +301,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { AuthConfigs: authConfig, Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()), CacheFrom: options.cacheFrom, + SecurityOpt: options.securityOpt, } response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions) From 3347e290aa9cccec0b85667b1c4cda84d73cd103 Mon Sep 17 00:00:00 2001 From: Deng Guangxing Date: Sat, 8 Oct 2016 15:29:32 +0800 Subject: [PATCH 184/978] fix typo in client/errors.go comments Signed-off-by: Deng Guangxing Upstream-commit: afb60b86d7f2b32ac22f0fc14ca08e3a97fea74e Component: cli --- components/cli/errors.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/components/cli/errors.go b/components/cli/errors.go index 71e25a7ae1..ad1dadabb6 100644 --- a/components/cli/errors.go +++ b/components/cli/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 26359b845215ad0072bac0776730debfecd25e00 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Sat, 8 Oct 2016 14:34:37 +0100 Subject: [PATCH 185/978] 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 Upstream-commit: a41ec7d802267adedcdd1cdca49c6911d3739c4f Component: cli --- components/cli/README.md | 24 ++++++++++----------- components/cli/client.go | 45 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 13 deletions(-) diff --git a/components/cli/README.md b/components/cli/README.md index 34cf7372db..2b7d81fada 100644 --- a/components/cli/README.md +++ b/components/cli/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/components/cli/client.go b/components/cli/client.go index deccb4ab74..58e8430cf2 100644 --- a/components/cli/client.go +++ b/components/cli/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 7f7622756e57fddc56e82bfb87f10176c4a39c50 Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Mon, 10 Oct 2016 23:07:32 +0800 Subject: [PATCH 186/978] Add the OPTIONS and Fix the links for contain prune Signed-off-by: yuexiao-wang Upstream-commit: 80bc9172264426bae397d9f5b449c231bd5f4aa4 Component: cli --- components/cli/command/container/prune.go | 2 +- components/cli/command/image/prune.go | 2 +- components/cli/command/system/prune.go | 2 +- components/cli/command/volume/prune.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/components/cli/command/container/prune.go b/components/cli/command/container/prune.go index 13e283a8b2..7088038614 100644 --- a/components/cli/command/container/prune.go +++ b/components/cli/command/container/prune.go @@ -21,7 +21,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { var opts pruneOptions cmd := &cobra.Command{ - Use: "prune", + Use: "prune [OPTIONS]", Short: "Remove all stopped containers", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { diff --git a/components/cli/command/image/prune.go b/components/cli/command/image/prune.go index 6944664a54..e5ad573130 100644 --- a/components/cli/command/image/prune.go +++ b/components/cli/command/image/prune.go @@ -22,7 +22,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { var opts pruneOptions cmd := &cobra.Command{ - Use: "prune", + Use: "prune [OPTIONS]", Short: "Remove unused images", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { diff --git a/components/cli/command/system/prune.go b/components/cli/command/system/prune.go index 4a9e952ada..6a36fdd890 100644 --- a/components/cli/command/system/prune.go +++ b/components/cli/command/system/prune.go @@ -20,7 +20,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { var opts pruneOptions cmd := &cobra.Command{ - Use: "prune [COMMAND]", + Use: "prune [OPTIONS]", Short: "Remove unused data.", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { diff --git a/components/cli/command/volume/prune.go b/components/cli/command/volume/prune.go index 59f3c94635..dc2d3e25bc 100644 --- a/components/cli/command/volume/prune.go +++ b/components/cli/command/volume/prune.go @@ -21,7 +21,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { var opts pruneOptions cmd := &cobra.Command{ - Use: "prune", + Use: "prune [OPTIONS]", Short: "Remove all unused volumes", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { From b9c5442c91030fa29f832c2505c389d83abf722f Mon Sep 17 00:00:00 2001 From: allencloud Date: Sat, 8 Oct 2016 18:38:25 +0800 Subject: [PATCH 187/978] better prune and system df Signed-off-by: allencloud Upstream-commit: 871b6928337440a692ee45a91f701fbceb3f41f4 Component: cli --- components/cli/command/container/prune.go | 2 +- components/cli/command/image/prune.go | 2 +- components/cli/command/prune/prune.go | 12 ++++++------ components/cli/command/system/df.go | 2 +- components/cli/command/system/prune.go | 4 ++-- components/cli/command/volume/prune.go | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/components/cli/command/container/prune.go b/components/cli/command/container/prune.go index 7088038614..be67fe4ca9 100644 --- a/components/cli/command/container/prune.go +++ b/components/cli/command/container/prune.go @@ -67,7 +67,7 @@ func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed u return } -// RunPrune call the Container Prune API +// RunPrune calls the Container Prune API // This returns the amount of space reclaimed and a detailed output string func RunPrune(dockerCli *command.DockerCli) (uint64, string, error) { return runPrune(dockerCli, pruneOptions{force: true}) diff --git a/components/cli/command/image/prune.go b/components/cli/command/image/prune.go index e5ad573130..46bd56cb10 100644 --- a/components/cli/command/image/prune.go +++ b/components/cli/command/image/prune.go @@ -83,7 +83,7 @@ func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed u return } -// RunPrune call the Image Prune API +// RunPrune calls the Image Prune API // This returns the amount of space reclaimed and a detailed output string func RunPrune(dockerCli *command.DockerCli, all bool) (uint64, string, error) { return runPrune(dockerCli, pruneOptions{force: true, all: all}) diff --git a/components/cli/command/prune/prune.go b/components/cli/command/prune/prune.go index 0b1374eda9..fd04c590b6 100644 --- a/components/cli/command/prune/prune.go +++ b/components/cli/command/prune/prune.go @@ -8,32 +8,32 @@ import ( "github.com/spf13/cobra" ) -// NewContainerPruneCommand return a cobra prune command for containers +// NewContainerPruneCommand returns a cobra prune command for containers func NewContainerPruneCommand(dockerCli *command.DockerCli) *cobra.Command { return container.NewPruneCommand(dockerCli) } -// NewVolumePruneCommand return a cobra prune command for volumes +// NewVolumePruneCommand returns a cobra prune command for volumes func NewVolumePruneCommand(dockerCli *command.DockerCli) *cobra.Command { return volume.NewPruneCommand(dockerCli) } -// NewImagePruneCommand return a cobra prune command for images +// NewImagePruneCommand returns a cobra prune command for images func NewImagePruneCommand(dockerCli *command.DockerCli) *cobra.Command { return image.NewPruneCommand(dockerCli) } -// RunContainerPrune execute a prune command for containers +// RunContainerPrune executes a prune command for containers func RunContainerPrune(dockerCli *command.DockerCli) (uint64, string, error) { return container.RunPrune(dockerCli) } -// RunVolumePrune execute a prune command for volumes +// RunVolumePrune executes a prune command for volumes func RunVolumePrune(dockerCli *command.DockerCli) (uint64, string, error) { return volume.RunPrune(dockerCli) } -// RunImagePrune execute a prune command for images +// RunImagePrune executes a prune command for images func RunImagePrune(dockerCli *command.DockerCli, all bool) (uint64, string, error) { return image.RunPrune(dockerCli, all) } diff --git a/components/cli/command/system/df.go b/components/cli/command/system/df.go index 085d680fe8..293946c188 100644 --- a/components/cli/command/system/df.go +++ b/components/cli/command/system/df.go @@ -19,7 +19,7 @@ func NewDiskUsageCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := &cobra.Command{ Use: "df [OPTIONS]", Short: "Show docker disk usage", - Args: cli.RequiresMaxArgs(1), + Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { return runDiskUsage(dockerCli, opts) }, diff --git a/components/cli/command/system/prune.go b/components/cli/command/system/prune.go index 6a36fdd890..ea8a41380f 100644 --- a/components/cli/command/system/prune.go +++ b/components/cli/command/system/prune.go @@ -15,13 +15,13 @@ type pruneOptions struct { all bool } -// NewPruneCommand creates a new cobra.Command for `docker du` +// NewPruneCommand creates a new cobra.Command for `docker prune` func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { var opts pruneOptions cmd := &cobra.Command{ Use: "prune [OPTIONS]", - Short: "Remove unused data.", + Short: "Remove unused data", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { return runPrune(dockerCli, opts) diff --git a/components/cli/command/volume/prune.go b/components/cli/command/volume/prune.go index dc2d3e25bc..a4bb0092d6 100644 --- a/components/cli/command/volume/prune.go +++ b/components/cli/command/volume/prune.go @@ -67,7 +67,7 @@ func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed u return } -// RunPrune call the Volume Prune API +// RunPrune calls the Volume Prune API // This returns the amount of space reclaimed and a detailed output string func RunPrune(dockerCli *command.DockerCli) (uint64, string, error) { return runPrune(dockerCli, pruneOptions{force: true}) From 1c680357027e86aec75fcc144d4d2700dad3ebd8 Mon Sep 17 00:00:00 2001 From: Boaz Shuster Date: Thu, 22 Sep 2016 15:54:41 +0300 Subject: [PATCH 188/978] Hide the mutex in formatter.ContainerStats The formatter.ContainerStats struct exposes its Mutex. This is a bad design and should be fixed. To fix that, I separated the statistics attributes from ContainerStats to StatsEntry and hid the mutex. Notice that the mutex protects both the `err` field and the statistics attributes. Then, implemented SetStatistics, SetError, GetStatistics and GetError to avoid races. Moreover, to make this less granular, I decided to replace the read-write mutex with the regular mutex and to pass a StatsEntry slice to formatter.ContainerStatsWrite Signed-off-by: Boaz Shuster Upstream-commit: 3bc50c45ba84c4d177a2a58afec8538268ce08fd Component: cli --- components/cli/command/container/stats.go | 24 ++-- .../cli/command/container/stats_helpers.go | 57 +++----- components/cli/command/formatter/stats.go | 132 +++++++++++++----- 3 files changed, 132 insertions(+), 81 deletions(-) diff --git a/components/cli/command/container/stats.go b/components/cli/command/container/stats.go index 2bd5e3db75..f60224796b 100644 --- a/components/cli/command/container/stats.go +++ b/components/cli/command/container/stats.go @@ -166,11 +166,10 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error { var errs []string cStats.mu.Lock() for _, c := range cStats.cs { - c.Mu.Lock() - if c.Err != nil { - errs = append(errs, fmt.Sprintf("%s: %v", c.Name, c.Err)) + cErr := c.GetError() + if cErr != nil { + errs = append(errs, fmt.Sprintf("%s: %v", c.Name, cErr)) } - c.Mu.Unlock() } cStats.mu.Unlock() if len(errs) > 0 { @@ -189,7 +188,7 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error { Format: formatter.NewStatsFormat(f, daemonOSType), } - cleanHeader := func() { + cleanScreen := func() { if !opts.noStream { fmt.Fprint(dockerCli.Out(), "\033[2J") fmt.Fprint(dockerCli.Out(), "\033[H") @@ -198,14 +197,17 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error { var err error for range time.Tick(500 * time.Millisecond) { - cleanHeader() - cStats.mu.RLock() - csLen := len(cStats.cs) - if err = formatter.ContainerStatsWrite(statsCtx, cStats.cs); err != nil { + cleanScreen() + ccstats := []formatter.StatsEntry{} + cStats.mu.Lock() + for _, c := range cStats.cs { + ccstats = append(ccstats, c.GetStatistics()) + } + cStats.mu.Unlock() + if err = formatter.ContainerStatsWrite(statsCtx, ccstats); err != nil { break } - cStats.mu.RUnlock() - if csLen == 0 && !showAll { + if len(cStats.cs) == 0 && !showAll { break } if opts.noStream { diff --git a/components/cli/command/container/stats_helpers.go b/components/cli/command/container/stats_helpers.go index 2039d2ade6..32ad84841b 100644 --- a/components/cli/command/container/stats_helpers.go +++ b/components/cli/command/container/stats_helpers.go @@ -17,7 +17,7 @@ import ( type stats struct { ostype string - mu sync.RWMutex + mu sync.Mutex cs []*formatter.ContainerStats } @@ -72,9 +72,7 @@ func collect(s *formatter.ContainerStats, ctx context.Context, cli client.APICli response, err := cli.ContainerStats(ctx, s.Name, streamStats) if err != nil { - s.Mu.Lock() - s.Err = err - s.Mu.Unlock() + s.SetError(err) return } defer response.Body.Close() @@ -88,6 +86,9 @@ func collect(s *formatter.ContainerStats, ctx context.Context, cli client.APICli cpuPercent = 0.0 blkRead, blkWrite uint64 // Only used on Linux mem = 0.0 + memLimit = 0.0 + memPerc = 0.0 + pidsStatsCurrent uint64 ) if err := dec.Decode(&v); err != nil { @@ -113,26 +114,27 @@ func collect(s *formatter.ContainerStats, ctx context.Context, cli client.APICli cpuPercent = calculateCPUPercentUnix(previousCPU, previousSystem, v) blkRead, blkWrite = calculateBlockIO(v.BlkioStats) mem = float64(v.MemoryStats.Usage) - + memLimit = float64(v.MemoryStats.Limit) + memPerc = memPercent + pidsStatsCurrent = v.PidsStats.Current } else { cpuPercent = calculateCPUPercentWindows(v) blkRead = v.StorageStats.ReadSizeBytes blkWrite = v.StorageStats.WriteSizeBytes mem = float64(v.MemoryStats.PrivateWorkingSet) } - - s.Mu.Lock() - s.CPUPercentage = cpuPercent - s.Memory = mem - s.NetworkRx, s.NetworkTx = calculateNetwork(v.Networks) - s.BlockRead = float64(blkRead) - s.BlockWrite = float64(blkWrite) - if daemonOSType != "windows" { - s.MemoryLimit = float64(v.MemoryStats.Limit) - s.MemoryPercentage = memPercent - s.PidsCurrent = v.PidsStats.Current - } - s.Mu.Unlock() + netRx, netTx := calculateNetwork(v.Networks) + s.SetStatistics(formatter.StatsEntry{ + CPUPercentage: cpuPercent, + Memory: mem, + MemoryPercentage: memPerc, + MemoryLimit: memLimit, + NetworkRx: netRx, + NetworkTx: netTx, + BlockRead: float64(blkRead), + BlockWrite: float64(blkWrite), + PidsCurrent: pidsStatsCurrent, + }) u <- nil if !streamStats { return @@ -144,18 +146,7 @@ func collect(s *formatter.ContainerStats, ctx context.Context, cli client.APICli case <-time.After(2 * time.Second): // zero out the values if we have not received an update within // the specified duration. - s.Mu.Lock() - s.CPUPercentage = 0 - s.Memory = 0 - s.MemoryPercentage = 0 - s.MemoryLimit = 0 - s.NetworkRx = 0 - s.NetworkTx = 0 - s.BlockRead = 0 - s.BlockWrite = 0 - s.PidsCurrent = 0 - s.Err = errors.New("timeout waiting for stats") - s.Mu.Unlock() + s.SetErrorAndReset(errors.New("timeout waiting for stats")) // if this is the first stat you get, release WaitGroup if !getFirst { getFirst = true @@ -163,12 +154,10 @@ func collect(s *formatter.ContainerStats, ctx context.Context, cli client.APICli } case err := <-u: if err != nil { - s.Mu.Lock() - s.Err = err - s.Mu.Unlock() + s.SetError(err) continue } - s.Err = nil + s.SetError(nil) // if this is the first stat you get, release WaitGroup if !getFirst { getFirst = true diff --git a/components/cli/command/formatter/stats.go b/components/cli/command/formatter/stats.go index 939431da1c..c7b30c9f39 100644 --- a/components/cli/command/formatter/stats.go +++ b/components/cli/command/formatter/stats.go @@ -4,27 +4,27 @@ import ( "fmt" "sync" - "github.com/docker/go-units" + units "src/github.com/docker/go-units" ) const ( - defaultStatsTableFormat = "table {{.Container}}\t{{.CPUPrec}}\t{{.MemUsage}}\t{{.MemPrec}}\t{{.NetIO}}\t{{.BlockIO}}\t{{.PIDs}}" - winDefaultStatsTableFormat = "table {{.Container}}\t{{.CPUPrec}}\t{{{.MemUsage}}\t{.NetIO}}\t{{.BlockIO}}" + winOSType = "windows" + defaultStatsTableFormat = "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}\t{{.BlockIO}}\t{{.PIDs}}" + winDefaultStatsTableFormat = "table {{.Container}}\t{{.CPUPerc}}\t{{{.MemUsage}}\t{.NetIO}}\t{{.BlockIO}}" emptyStatsTableFormat = "Waiting for statistics..." containerHeader = "CONTAINER" - cpuPrecHeader = "CPU %" + cpuPercHeader = "CPU %" netIOHeader = "NET I/O" blockIOHeader = "BLOCK I/O" - winMemPrecHeader = "PRIV WORKING SET" // Used only on Window - memPrecHeader = "MEM %" // Used only on Linux + winMemPercHeader = "PRIV WORKING SET" // Used only on Window + memPercHeader = "MEM %" // Used only on Linux memUseHeader = "MEM USAGE / LIMIT" // Used only on Linux pidsHeader = "PIDS" // Used only on Linux ) -// ContainerStatsAttrs represents the statistics data collected from a container. -type ContainerStatsAttrs struct { - Windows bool +// StatsEntry represents represents the statistics data collected from a container +type StatsEntry struct { Name string CPUPercentage float64 Memory float64 // On Windows this is the private working set @@ -35,19 +35,73 @@ type ContainerStatsAttrs struct { BlockRead float64 BlockWrite float64 PidsCurrent uint64 // Not used on Windows + IsInvalid bool + OSType string } -// ContainerStats represents the containers statistics data. +// ContainerStats represents an entity to store containers statistics synchronously type ContainerStats struct { - Mu sync.RWMutex - ContainerStatsAttrs - Err error + mutex sync.Mutex + StatsEntry + err error +} + +// GetError returns the container statistics error. +// This is used to determine whether the statistics are valid or not +func (cs *ContainerStats) GetError() error { + cs.mutex.Lock() + defer cs.mutex.Unlock() + return cs.err +} + +// SetErrorAndReset zeroes all the container statistics and store the error. +// It is used when receiving time out error during statistics collecting to reduce lock overhead +func (cs *ContainerStats) SetErrorAndReset(err error) { + cs.mutex.Lock() + defer cs.mutex.Unlock() + cs.CPUPercentage = 0 + cs.Memory = 0 + cs.MemoryPercentage = 0 + cs.MemoryLimit = 0 + cs.NetworkRx = 0 + cs.NetworkTx = 0 + cs.BlockRead = 0 + cs.BlockWrite = 0 + cs.PidsCurrent = 0 + cs.err = err + cs.IsInvalid = true +} + +// SetError sets container statistics error +func (cs *ContainerStats) SetError(err error) { + cs.mutex.Lock() + defer cs.mutex.Unlock() + cs.err = err + if err != nil { + cs.IsInvalid = true + } +} + +// SetStatistics set the container statistics +func (cs *ContainerStats) SetStatistics(s StatsEntry) { + cs.mutex.Lock() + defer cs.mutex.Unlock() + s.Name = cs.Name + s.OSType = cs.OSType + cs.StatsEntry = s +} + +// GetStatistics returns container statistics with other meta data such as the container name +func (cs *ContainerStats) GetStatistics() StatsEntry { + cs.mutex.Lock() + defer cs.mutex.Unlock() + return cs.StatsEntry } // NewStatsFormat returns a format for rendering an CStatsContext func NewStatsFormat(source, osType string) Format { if source == TableFormatKey { - if osType == "windows" { + if osType == winOSType { return Format(winDefaultStatsTableFormat) } return Format(defaultStatsTableFormat) @@ -58,22 +112,16 @@ func NewStatsFormat(source, osType string) Format { // NewContainerStats returns a new ContainerStats entity and sets in it the given name func NewContainerStats(name, osType string) *ContainerStats { return &ContainerStats{ - ContainerStatsAttrs: ContainerStatsAttrs{ - Name: name, - Windows: (osType == "windows"), - }, + StatsEntry: StatsEntry{Name: name, OSType: osType}, } } // ContainerStatsWrite renders the context for a list of containers statistics -func ContainerStatsWrite(ctx Context, containerStats []*ContainerStats) error { +func ContainerStatsWrite(ctx Context, containerStats []StatsEntry) error { render := func(format func(subContext subContext) error) error { for _, cstats := range containerStats { - cstats.Mu.RLock() - cstatsAttrs := cstats.ContainerStatsAttrs - cstats.Mu.RUnlock() containerStatsCtx := &containerStatsContext{ - s: cstatsAttrs, + s: cstats, } if err := format(containerStatsCtx); err != nil { return err @@ -86,7 +134,7 @@ func ContainerStatsWrite(ctx Context, containerStats []*ContainerStats) error { type containerStatsContext struct { HeaderContext - s ContainerStatsAttrs + s StatsEntry } func (c *containerStatsContext) Container() string { @@ -94,42 +142,54 @@ func (c *containerStatsContext) Container() string { return c.s.Name } -func (c *containerStatsContext) CPUPrec() string { - c.AddHeader(cpuPrecHeader) +func (c *containerStatsContext) CPUPerc() string { + c.AddHeader(cpuPercHeader) + if c.s.IsInvalid { + return fmt.Sprintf("--") + } return fmt.Sprintf("%.2f%%", c.s.CPUPercentage) } func (c *containerStatsContext) MemUsage() string { c.AddHeader(memUseHeader) - if !c.s.Windows { - return fmt.Sprintf("%s / %s", units.BytesSize(c.s.Memory), units.BytesSize(c.s.MemoryLimit)) + if c.s.IsInvalid || c.s.OSType == winOSType { + return fmt.Sprintf("-- / --") } - return fmt.Sprintf("-- / --") + return fmt.Sprintf("%s / %s", units.BytesSize(c.s.Memory), units.BytesSize(c.s.MemoryLimit)) } -func (c *containerStatsContext) MemPrec() string { - header := memPrecHeader - if c.s.Windows { - header = winMemPrecHeader +func (c *containerStatsContext) MemPerc() string { + header := memPercHeader + if c.s.OSType == winOSType { + header = winMemPercHeader } c.AddHeader(header) + if c.s.IsInvalid { + return fmt.Sprintf("--") + } return fmt.Sprintf("%.2f%%", c.s.MemoryPercentage) } func (c *containerStatsContext) NetIO() string { c.AddHeader(netIOHeader) + if c.s.IsInvalid { + return fmt.Sprintf("--") + } return fmt.Sprintf("%s / %s", units.HumanSizeWithPrecision(c.s.NetworkRx, 3), units.HumanSizeWithPrecision(c.s.NetworkTx, 3)) } func (c *containerStatsContext) BlockIO() string { c.AddHeader(blockIOHeader) + if c.s.IsInvalid { + return fmt.Sprintf("--") + } return fmt.Sprintf("%s / %s", units.HumanSizeWithPrecision(c.s.BlockRead, 3), units.HumanSizeWithPrecision(c.s.BlockWrite, 3)) } func (c *containerStatsContext) PIDs() string { c.AddHeader(pidsHeader) - if !c.s.Windows { - return fmt.Sprintf("%d", c.s.PidsCurrent) + if c.s.IsInvalid || c.s.OSType == winOSType { + return fmt.Sprintf("--") } - return fmt.Sprintf("-") + return fmt.Sprintf("%d", c.s.PidsCurrent) } From 9fad962ff9768fe638140b3711aa208135aea928 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sun, 25 Sep 2016 16:26:46 +0200 Subject: [PATCH 189/978] Improve --log-level help text This information was added in 1efc940e6f547760e5e8f4648acb120ff19fdc58, but removed again in a271eaeba224652e3a12af0287afbae6f82a9333 to make the help-output fit in a 80-chars terminal. This adds the available options again in the help output, and updates the CLI reference documentation to match actual output. Signed-off-by: Sebastiaan van Stijn Upstream-commit: bed046666a4efa8e675f41bd4287ded917e440f2 Component: cli --- components/cli/flags/common.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/flags/common.go b/components/cli/flags/common.go index 2318b9d975..758e0a66cc 100644 --- a/components/cli/flags/common.go +++ b/components/cli/flags/common.go @@ -53,7 +53,7 @@ func (commonOpts *CommonOptions) InstallFlags(flags *pflag.FlagSet) { } flags.BoolVarP(&commonOpts.Debug, "debug", "D", false, "Enable debug mode") - flags.StringVarP(&commonOpts.LogLevel, "log-level", "l", "info", "Set the logging level") + flags.StringVarP(&commonOpts.LogLevel, "log-level", "l", "info", "Set the logging level (debug, info, warn, error, fatal)") flags.BoolVar(&commonOpts.TLS, "tls", false, "Use TLS; implied by --tlsverify") flags.BoolVar(&commonOpts.TLSVerify, FlagTLSVerify, dockerTLSVerify, "Use TLS and verify the remote") From fcc2ad15e70fdbef9530ea438552f6fc475d4bcd Mon Sep 17 00:00:00 2001 From: allencloud Date: Sun, 9 Oct 2016 10:29:58 +0800 Subject: [PATCH 190/978] return nil when no node or service to avoid additional api call Signed-off-by: allencloud Upstream-commit: 6ef1c7deaf031579a692d30d7601559a9c6e6109 Component: cli --- components/cli/command/node/list.go | 22 +++++++++++++--------- components/cli/command/service/list.go | 13 +++++++++---- components/cli/command/utils.go | 6 +++--- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/components/cli/command/node/list.go b/components/cli/command/node/list.go index bed4bc4965..d028d19147 100644 --- a/components/cli/command/node/list.go +++ b/components/cli/command/node/list.go @@ -45,6 +45,7 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command { func runList(dockerCli *command.DockerCli, opts listOptions) error { client := dockerCli.Client() + out := dockerCli.Out() ctx := context.Background() nodes, err := client.NodeList( @@ -54,17 +55,20 @@ func runList(dockerCli *command.DockerCli, opts listOptions) error { return err } - info, err := client.Info(ctx) - if err != nil { - return err + if len(nodes) > 0 && !opts.quiet { + // only non-empty nodes and not quiet, should we call /info api + info, err := client.Info(ctx) + if err != nil { + return err + } + printTable(out, nodes, info) + } else if !opts.quiet { + // no nodes and not quiet, print only one line with columns ID, HOSTNAME, ... + printTable(out, nodes, types.Info{}) + } else { + printQuiet(out, nodes) } - out := dockerCli.Out() - if opts.quiet { - printQuiet(out, nodes) - } else { - printTable(out, nodes, info) - } return nil } diff --git a/components/cli/command/service/list.go b/components/cli/command/service/list.go index 681acd3f25..2278643fbc 100644 --- a/components/cli/command/service/list.go +++ b/components/cli/command/service/list.go @@ -49,16 +49,15 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command { func runList(dockerCli *command.DockerCli, opts listOptions) error { ctx := context.Background() client := dockerCli.Client() + out := dockerCli.Out() services, err := client.ServiceList(ctx, types.ServiceListOptions{Filter: opts.filter.Value()}) if err != nil { return err } - out := dockerCli.Out() - if opts.quiet { - PrintQuiet(out, services) - } else { + if len(services) > 0 && !opts.quiet { + // only non-empty services and not quiet, should we call TaskList and NodeList api taskFilter := filters.NewArgs() for _, service := range services { taskFilter.Add("service", service.ID) @@ -75,7 +74,13 @@ func runList(dockerCli *command.DockerCli, opts listOptions) error { } PrintNotQuiet(out, services, nodes, tasks) + } else if !opts.quiet { + // no services and not quiet, print only one line with columns ID, NAME, REPLICAS... + PrintNotQuiet(out, services, []swarm.Node{}, []swarm.Task{}) + } else { + PrintQuiet(out, services) } + return nil } diff --git a/components/cli/command/utils.go b/components/cli/command/utils.go index e768cf770d..9f9a1ee80d 100644 --- a/components/cli/command/utils.go +++ b/components/cli/command/utils.go @@ -58,14 +58,14 @@ func PrettyPrint(i interface{}) string { } } -// PromptForConfirmation request and check confirmation from user. +// PromptForConfirmation requests and checks confirmation from user. // This will display the provided message followed by ' [y/N] '. If // the user input 'y' or 'Y' it returns true other false. If no -// message is provided "Are you sure you want to proceeed? [y/N] " +// message is provided "Are you sure you want to proceed? [y/N] " // will be used instead. func PromptForConfirmation(ins *InStream, outs *OutStream, message string) bool { if message == "" { - message = "Are you sure you want to proceeed?" + message = "Are you sure you want to proceed?" } message += " [y/N] " From cd04735ef76a7d9cf6439e51fd7e7703b064ed4f Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Tue, 11 Oct 2016 19:35:12 +0800 Subject: [PATCH 191/978] Modify function name from SetDaemonLogLevel to SetLogLevel Signed-off-by: yuexiao-wang Upstream-commit: f4267969c796bcacea13b0a293eed89423e1c58c Component: cli --- components/cli/flags/common.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/components/cli/flags/common.go b/components/cli/flags/common.go index 2318b9d975..074d53e315 100644 --- a/components/cli/flags/common.go +++ b/components/cli/flags/common.go @@ -101,9 +101,8 @@ func (commonOpts *CommonOptions) SetDefaultOptions(flags *pflag.FlagSet) { } } -// SetDaemonLogLevel sets the logrus logging level -// TODO: this is a bad name, it applies to the client as well. -func SetDaemonLogLevel(logLevel string) { +// SetLogLevel sets the logrus logging level +func SetLogLevel(logLevel string) { if logLevel != "" { lvl, err := logrus.ParseLevel(logLevel) if err != nil { From b77b8228bfcbd8ce795f3212abe1559f4ec481d1 Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Tue, 11 Oct 2016 19:35:12 +0800 Subject: [PATCH 192/978] Modify function name from SetDaemonLogLevel to SetLogLevel Signed-off-by: yuexiao-wang Upstream-commit: a0e694d7c04b927d664d384afb2ee27c373039ed Component: cli --- components/cli/docker.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/docker.go b/components/cli/docker.go index 969cd80876..d412a38b28 100644 --- a/components/cli/docker.go +++ b/components/cli/docker.go @@ -99,7 +99,7 @@ func showVersion() { } func dockerPreRun(opts *cliflags.ClientOptions) { - cliflags.SetDaemonLogLevel(opts.Common.LogLevel) + cliflags.SetLogLevel(opts.Common.LogLevel) if opts.ConfigDir != "" { cliconfig.SetConfigDir(opts.ConfigDir) From bf3fe75f3b9d0528dc4fe6fe2e7c4a41f4edd2f2 Mon Sep 17 00:00:00 2001 From: Kenfe-Mickael Laventure Date: Tue, 11 Oct 2016 11:49:26 -0700 Subject: [PATCH 193/978] Move types.Volumes optional fields under a new type This allows us to hide those fields when they are not filled. Signed-off-by: Kenfe-Mickael Laventure Upstream-commit: 5018781cab7b7458588ad28b639d6d0749a4da67 Component: cli --- components/cli/command/formatter/disk_usage.go | 14 +++++++------- components/cli/command/formatter/volume.go | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/components/cli/command/formatter/disk_usage.go b/components/cli/command/formatter/disk_usage.go index 866e9bd04a..acb210dbff 100644 --- a/components/cli/command/formatter/disk_usage.go +++ b/components/cli/command/formatter/disk_usage.go @@ -288,7 +288,7 @@ func (c *diskUsageVolumesContext) Active() string { used := 0 for _, v := range c.volumes { - if v.RefCount > 0 { + if v.UsageData.RefCount > 0 { used++ } } @@ -301,8 +301,8 @@ func (c *diskUsageVolumesContext) Size() string { c.AddHeader(sizeHeader) for _, v := range c.volumes { - if v.Size != -1 { - size += v.Size + if v.UsageData.Size != -1 { + size += v.UsageData.Size } } @@ -315,11 +315,11 @@ func (c *diskUsageVolumesContext) Reclaimable() string { c.AddHeader(reclaimableHeader) for _, v := range c.volumes { - if v.Size != -1 { - if v.RefCount == 0 { - reclaimable += v.Size + if v.UsageData.Size != -1 { + if v.UsageData.RefCount == 0 { + reclaimable += v.UsageData.Size } - totalSize += v.Size + totalSize += v.UsageData.Size } } diff --git a/components/cli/command/formatter/volume.go b/components/cli/command/formatter/volume.go index 8fb11732e3..7bc3537573 100644 --- a/components/cli/command/formatter/volume.go +++ b/components/cli/command/formatter/volume.go @@ -101,16 +101,16 @@ func (c *volumeContext) Label(name string) string { func (c *volumeContext) Links() string { c.AddHeader(linksHeader) - if c.v.Size == -1 { + if c.v.UsageData == nil { return "N/A" } - return fmt.Sprintf("%d", c.v.RefCount) + return fmt.Sprintf("%d", c.v.UsageData.RefCount) } func (c *volumeContext) Size() string { c.AddHeader(sizeHeader) - if c.v.Size == -1 { + if c.v.UsageData == nil { return "N/A" } - return units.HumanSize(float64(c.v.Size)) + return units.HumanSize(float64(c.v.UsageData.Size)) } From 1409d06aebb11d79516cfa8c084965efea9752f0 Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Tue, 11 Oct 2016 15:53:14 -0700 Subject: [PATCH 194/978] 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 Upstream-commit: 4d1a6a43cd5a67ef2f8ec79f827d8619a7cc79ad Component: cli --- components/cli/client.go | 14 ++++++++++++++ components/cli/request.go | 8 +++----- components/cli/transport.go | 17 ----------------- 3 files changed, 17 insertions(+), 22 deletions(-) diff --git a/components/cli/client.go b/components/cli/client.go index 025eaaf9a9..75073881c7 100644 --- a/components/cli/client.go +++ b/components/cli/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/components/cli/request.go b/components/cli/request.go index d585b46ab1..bfd62bad1e 100644 --- a/components/cli/request.go +++ b/components/cli/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/components/cli/transport.go b/components/cli/transport.go index 771d76f06b..f04e601649 100644 --- a/components/cli/transport.go +++ b/components/cli/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 c5a9a160567664729c41545ef7088da6991601b1 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 12 Oct 2016 16:06:34 -0700 Subject: [PATCH 195/978] Use ListOpt for `docker network create --label` and `docker volume create --label` This fix is related to 27049 and 27047. For `--label` flag, if string slice is used (like 27047), then quote can not be used in command and will result in an error : ``` line 1, column 14: bare " in non-quoted-field ``` The issue 27047 has been fixed by 27049. Recently I found out that both `docker network create --label` and `docker volume create --label` still use string slice and will return the same error when quotes are used. This fix fixes `docker network create --label` and `docker volume create --label` by using `ListOpt` (as 27049) as well. This fix has been tested and verified manually. Signed-off-by: Yong Tang Upstream-commit: 49e49e8e0069fd589c67d5538cd674ea15aa09c6 Component: cli --- components/cli/command/network/create.go | 7 ++++--- components/cli/command/volume/create.go | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/components/cli/command/network/create.go b/components/cli/command/network/create.go index 2ffd80548b..abc494e1e0 100644 --- a/components/cli/command/network/create.go +++ b/components/cli/command/network/create.go @@ -20,7 +20,7 @@ type createOptions struct { name string driver string driverOpts opts.MapOpts - labels []string + labels opts.ListOpts internal bool ipv6 bool attachable bool @@ -36,6 +36,7 @@ type createOptions struct { func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { opts := createOptions{ driverOpts: *opts.NewMapOpts(nil, nil), + labels: opts.NewListOpts(runconfigopts.ValidateEnv), ipamAux: *opts.NewMapOpts(nil, nil), ipamOpt: *opts.NewMapOpts(nil, nil), } @@ -53,7 +54,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() flags.StringVarP(&opts.driver, "driver", "d", "bridge", "Driver to manage the Network") flags.VarP(&opts.driverOpts, "opt", "o", "Set driver specific options") - flags.StringSliceVar(&opts.labels, "label", []string{}, "Set metadata on a network") + flags.Var(&opts.labels, "label", "Set metadata on a network") flags.BoolVar(&opts.internal, "internal", false, "Restrict external access to the network") flags.BoolVar(&opts.ipv6, "ipv6", false, "Enable IPv6 networking") flags.BoolVar(&opts.attachable, "attachable", false, "Enable manual container attachment") @@ -90,7 +91,7 @@ func runCreate(dockerCli *command.DockerCli, opts createOptions) error { Internal: opts.internal, EnableIPv6: opts.ipv6, Attachable: opts.attachable, - Labels: runconfigopts.ConvertKVStringsToMap(opts.labels), + Labels: runconfigopts.ConvertKVStringsToMap(opts.labels.GetAll()), } resp, err := client.NetworkCreate(context.Background(), opts.name, nc) diff --git a/components/cli/command/volume/create.go b/components/cli/command/volume/create.go index 4427ff1ea7..fbf62a5ef1 100644 --- a/components/cli/command/volume/create.go +++ b/components/cli/command/volume/create.go @@ -17,12 +17,13 @@ type createOptions struct { name string driver string driverOpts opts.MapOpts - labels []string + labels opts.ListOpts } func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { opts := createOptions{ driverOpts: *opts.NewMapOpts(nil, nil), + labels: opts.NewListOpts(runconfigopts.ValidateEnv), } cmd := &cobra.Command{ @@ -46,7 +47,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.StringVar(&opts.name, "name", "", "Specify volume name") flags.Lookup("name").Hidden = true flags.VarP(&opts.driverOpts, "opt", "o", "Set driver specific options") - flags.StringSliceVar(&opts.labels, "label", []string{}, "Set metadata for a volume") + flags.Var(&opts.labels, "label", "Set metadata for a volume") return cmd } @@ -58,7 +59,7 @@ func runCreate(dockerCli *command.DockerCli, opts createOptions) error { Driver: opts.driver, DriverOpts: opts.driverOpts.GetAll(), Name: opts.name, - Labels: runconfigopts.ConvertKVStringsToMap(opts.labels), + Labels: runconfigopts.ConvertKVStringsToMap(opts.labels.GetAll()), } vol, err := client.VolumeCreate(context.Background(), volReq) From 939ccb748346b65c98af9b19989454f8d6ba7505 Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Thu, 13 Oct 2016 19:35:10 +0800 Subject: [PATCH 196/978] Fix the incorrect description for NewInStream Signed-off-by: yuexiao-wang Upstream-commit: b6fbe832ac559d7a75862cfc8ed695fc64fc7764 Component: cli --- components/cli/command/in.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/in.go b/components/cli/command/in.go index c3ed70dc12..7204b7ad04 100644 --- a/components/cli/command/in.go +++ b/components/cli/command/in.go @@ -68,7 +68,7 @@ func (i *InStream) CheckTty(attachStdin, ttyMode bool) error { return nil } -// NewInStream returns a new OutStream object from a Writer +// NewInStream returns a new InStream object from a ReadCloser func NewInStream(in io.ReadCloser) *InStream { fd, isTerminal := term.GetFdInfo(in) return &InStream{in: in, fd: fd, isTerminal: isTerminal} From 9315c007691a86a3ea9e47c3b241d6d1dea1c080 Mon Sep 17 00:00:00 2001 From: John Howard Date: Fri, 14 Oct 2016 10:14:43 -0700 Subject: [PATCH 197/978] Windows: Hint to run client elevated Signed-off-by: John Howard Upstream-commit: 86322803158ed5db5a5148af1d228be668ca96f9 Component: cli --- components/cli/request.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/components/cli/request.go b/components/cli/request.go index bfd62bad1e..91a05824e1 100644 --- a/components/cli/request.go +++ b/components/cli/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 da9c666ce42f66d8975ca78731c09b8e4f8cf8b0 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 12 Oct 2016 10:24:19 -0700 Subject: [PATCH 198/978] Allow `docker deploy` command accept filename with/without extension This fix tries to address the issue raised in 25855 where the command `docker deploy` can only accept a STACK without extension of `.dab`. In other words, `docker deploy hellojavaee.dab` gives an error: ``` Bundle hellojavaee.dab.dab not found. Specify the path with --file ``` This fix updates the way namespace STACK is taken so that in case `STACK.dab` is provided with `docker deploy`: ``` $ docker deploy STACK.dab ``` The `STACK` is used as namespace (instead of `STACK.dab`). NOTE: This fix will only allows `.dab` extension in namespace, because it is not possible to have a namespace with `.` in the middle. In other words, a namespace `hello.java.ee` will not work anyway (whether the file `hello.java.ee` exists or not). An additional integration test has been added to cover the changes. This fix fixes 25855. Signed-off-by: Yong Tang Upstream-commit: 0cb01799e925c334508e9abe25f65af727ca7cdf Component: cli --- components/cli/command/stack/deploy.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index 6daf9500f0..bf31dd7753 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -4,6 +4,7 @@ package stack import ( "fmt" + "strings" "github.com/spf13/cobra" "golang.org/x/net/context" @@ -34,7 +35,7 @@ func newDeployCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Create and update a stack from a Distributed Application Bundle (DAB)", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - opts.namespace = args[0] + opts.namespace = strings.TrimSuffix(args[0], ".dab") return runDeploy(dockerCli, opts) }, } From 1ce580f929abbb956bb38f80499bdf5e6a2c5d66 Mon Sep 17 00:00:00 2001 From: cyli Date: Wed, 28 Sep 2016 12:49:47 -0700 Subject: [PATCH 199/978] Fix API incompatibilities between notary v0.3.0 and v0.4.2: - some function signatures have changed - use the new ones - re-generate the notary delegation key certs, since notary doesn't allow SHA1 - fix some error message mapping because now if a root rotation fails to validate trusted operations will fail Signed-off-by: cyli Upstream-commit: 43d7c0ed9af3a1ecf13c09544d664fb4ca154bc8 Component: cli --- components/cli/command/image/trust.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/components/cli/command/image/trust.go b/components/cli/command/image/trust.go index b08bd490cb..b8de6a5245 100644 --- a/components/cli/command/image/trust.go +++ b/components/cli/command/image/trust.go @@ -30,13 +30,14 @@ import ( "github.com/docker/docker/reference" "github.com/docker/docker/registry" "github.com/docker/go-connections/tlsconfig" + "github.com/docker/notary" "github.com/docker/notary/client" "github.com/docker/notary/passphrase" + "github.com/docker/notary/storage" "github.com/docker/notary/trustmanager" "github.com/docker/notary/trustpinning" "github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/signed" - "github.com/docker/notary/tuf/store" ) var ( @@ -144,7 +145,7 @@ func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry } // Initialize the notary repository with a remotely managed snapshot key - if err := repo.Initialize(rootKeyID, data.CanonicalSnapshotRole); err != nil { + if err := repo.Initialize([]string{rootKeyID}, data.CanonicalSnapshotRole); err != nil { return notaryError(repoInfo.FullName(), err) } fmt.Fprintf(cli.Out(), "Finished initializing %q\n", repoInfo.FullName()) @@ -464,7 +465,7 @@ func GetNotaryRepository(streams command.Streams, repoInfo *registry.RepositoryI trustpinning.TrustPinConfig{}) } -func getPassphraseRetriever(streams command.Streams) passphrase.Retriever { +func getPassphraseRetriever(streams command.Streams) notary.PassRetriever { aliasMap := map[string]string{ "root": "root", "snapshot": "repository", @@ -554,11 +555,11 @@ func notaryError(repoName string, err error) error { return fmt.Errorf("Error: remote repository %s out-of-date: %v", repoName, err) case trustmanager.ErrKeyNotFound: return fmt.Errorf("Error: signing keys for remote repository %s not found: %v", repoName, err) - case *net.OpError: + case storage.NetworkError: return fmt.Errorf("Error: error contacting notary server: %v", err) - case store.ErrMetaNotFound: + case storage.ErrMetaNotFound: return fmt.Errorf("Error: trust data missing for remote repository %s or remote repository not found: %v", repoName, err) - case signed.ErrInvalidKeyType: + case trustpinning.ErrRootRotationFail, trustpinning.ErrValidationFail, signed.ErrInvalidKeyType: return fmt.Errorf("Warning: potential malicious behavior - trust data mismatch for remote repository %s: %v", repoName, err) case signed.ErrNoKeys: return fmt.Errorf("Error: could not find signing keys for remote repository %s, or could not decrypt signing key: %v", repoName, err) From f0fd68b09c1954b4da5f9117f467f9af9ffc1526 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Tue, 13 Sep 2016 07:01:31 +0000 Subject: [PATCH 200/978] Fix broken JSON support in cli/command/formatter How to test: $ docker ps --format '{{json .}}' $ docker network ls --format '{{json .}}' $ docker volume ls --format '{{json .}}' Signed-off-by: Akihiro Suda Upstream-commit: c11155f0d121a44c1c6793945e39251ff260d1e4 Component: cli --- components/cli/command/formatter/container.go | 4 ++ .../cli/command/formatter/container_test.go | 47 +++++++++++++ components/cli/command/formatter/network.go | 4 ++ .../cli/command/formatter/network_test.go | 46 +++++++++++++ components/cli/command/formatter/reflect.go | 65 ++++++++++++++++++ .../cli/command/formatter/reflect_test.go | 66 +++++++++++++++++++ components/cli/command/formatter/service.go | 4 ++ components/cli/command/formatter/volume.go | 4 ++ .../cli/command/formatter/volume_test.go | 45 +++++++++++++ .../cli/command/service/inspect_test.go | 47 +++++++++++-- 10 files changed, 325 insertions(+), 7 deletions(-) create mode 100644 components/cli/command/formatter/reflect.go create mode 100644 components/cli/command/formatter/reflect_test.go diff --git a/components/cli/command/formatter/container.go b/components/cli/command/formatter/container.go index ceef75890f..094bc85447 100644 --- a/components/cli/command/formatter/container.go +++ b/components/cli/command/formatter/container.go @@ -79,6 +79,10 @@ type containerContext struct { c types.Container } +func (c *containerContext) MarshalJSON() ([]byte, error) { + return marshalJSON(c) +} + func (c *containerContext) ID() string { c.AddHeader(containerIDHeader) if c.trunc { diff --git a/components/cli/command/formatter/container_test.go b/components/cli/command/formatter/container_test.go index 1ef48ae2d2..4b520f94ba 100644 --- a/components/cli/command/formatter/container_test.go +++ b/components/cli/command/formatter/container_test.go @@ -2,6 +2,7 @@ package formatter import ( "bytes" + "encoding/json" "fmt" "strings" "testing" @@ -323,3 +324,49 @@ func TestContainerContextWriteWithNoContainers(t *testing.T) { out.Reset() } } + +func TestContainerContextWriteJSON(t *testing.T) { + unix := time.Now().Add(-65 * time.Second).Unix() + containers := []types.Container{ + {ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unix}, + {ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unix}, + } + expectedCreated := time.Unix(unix, 0).String() + expectedJSONs := []map[string]interface{}{ + {"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID1", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_baz", "Ports": "", "RunningFor": "About a minute", "Size": "0 B", "Status": ""}, + {"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID2", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_bar", "Ports": "", "RunningFor": "About a minute", "Size": "0 B", "Status": ""}, + } + out := bytes.NewBufferString("") + err := ContainerWrite(Context{Format: "{{json .}}", Output: out}, containers) + if err != nil { + t.Fatal(err) + } + for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { + t.Logf("Output: line %d: %s", i, line) + var m map[string]interface{} + if err := json.Unmarshal([]byte(line), &m); err != nil { + t.Fatal(err) + } + assert.DeepEqual(t, m, expectedJSONs[i]) + } +} + +func TestContainerContextWriteJSONField(t *testing.T) { + containers := []types.Container{ + {ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu"}, + {ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu"}, + } + out := bytes.NewBufferString("") + err := ContainerWrite(Context{Format: "{{json .ID}}", Output: out}, containers) + if err != nil { + t.Fatal(err) + } + for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { + t.Logf("Output: line %d: %s", i, line) + var s string + if err := json.Unmarshal([]byte(line), &s); err != nil { + t.Fatal(err) + } + assert.Equal(t, s, containers[i].ID) + } +} diff --git a/components/cli/command/formatter/network.go b/components/cli/command/formatter/network.go index d808fdc22d..7fbad7d2ab 100644 --- a/components/cli/command/formatter/network.go +++ b/components/cli/command/formatter/network.go @@ -53,6 +53,10 @@ type networkContext struct { n types.NetworkResource } +func (c *networkContext) MarshalJSON() ([]byte, error) { + return marshalJSON(c) +} + func (c *networkContext) ID() string { c.AddHeader(networkIDHeader) if c.trunc { diff --git a/components/cli/command/formatter/network_test.go b/components/cli/command/formatter/network_test.go index 28f078548f..b40a534eed 100644 --- a/components/cli/command/formatter/network_test.go +++ b/components/cli/command/formatter/network_test.go @@ -2,6 +2,7 @@ package formatter import ( "bytes" + "encoding/json" "strings" "testing" @@ -160,3 +161,48 @@ foobar_bar } } } + +func TestNetworkContextWriteJSON(t *testing.T) { + networks := []types.NetworkResource{ + {ID: "networkID1", Name: "foobar_baz"}, + {ID: "networkID2", Name: "foobar_bar"}, + } + expectedJSONs := []map[string]interface{}{ + {"Driver": "", "ID": "networkID1", "IPv6": "false", "Internal": "false", "Labels": "", "Name": "foobar_baz", "Scope": ""}, + {"Driver": "", "ID": "networkID2", "IPv6": "false", "Internal": "false", "Labels": "", "Name": "foobar_bar", "Scope": ""}, + } + + out := bytes.NewBufferString("") + err := NetworkWrite(Context{Format: "{{json .}}", Output: out}, networks) + if err != nil { + t.Fatal(err) + } + for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { + t.Logf("Output: line %d: %s", i, line) + var m map[string]interface{} + if err := json.Unmarshal([]byte(line), &m); err != nil { + t.Fatal(err) + } + assert.DeepEqual(t, m, expectedJSONs[i]) + } +} + +func TestNetworkContextWriteJSONField(t *testing.T) { + networks := []types.NetworkResource{ + {ID: "networkID1", Name: "foobar_baz"}, + {ID: "networkID2", Name: "foobar_bar"}, + } + out := bytes.NewBufferString("") + err := NetworkWrite(Context{Format: "{{json .ID}}", Output: out}, networks) + if err != nil { + t.Fatal(err) + } + for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { + t.Logf("Output: line %d: %s", i, line) + var s string + if err := json.Unmarshal([]byte(line), &s); err != nil { + t.Fatal(err) + } + assert.Equal(t, s, networks[i].ID) + } +} diff --git a/components/cli/command/formatter/reflect.go b/components/cli/command/formatter/reflect.go new file mode 100644 index 0000000000..d1d8737d21 --- /dev/null +++ b/components/cli/command/formatter/reflect.go @@ -0,0 +1,65 @@ +package formatter + +import ( + "encoding/json" + "fmt" + "reflect" + "unicode" +) + +func marshalJSON(x interface{}) ([]byte, error) { + m, err := marshalMap(x) + if err != nil { + return nil, err + } + return json.Marshal(m) +} + +// marshalMap marshals x to map[string]interface{} +func marshalMap(x interface{}) (map[string]interface{}, error) { + val := reflect.ValueOf(x) + if val.Kind() != reflect.Ptr { + return nil, fmt.Errorf("expected a pointer to a struct, got %v", val.Kind()) + } + if val.IsNil() { + return nil, fmt.Errorf("expxected a pointer to a struct, got nil pointer") + } + valElem := val.Elem() + if valElem.Kind() != reflect.Struct { + return nil, fmt.Errorf("expected a pointer to a struct, got a pointer to %v", valElem.Kind()) + } + typ := val.Type() + m := make(map[string]interface{}) + for i := 0; i < val.NumMethod(); i++ { + k, v, err := marshalForMethod(typ.Method(i), val.Method(i)) + if err != nil { + return nil, err + } + if k != "" { + m[k] = v + } + } + return m, nil +} + +var unmarshallableNames = map[string]struct{}{"FullHeader": {}} + +// marshalForMethod returns the map key and the map value for marshalling the method. +// It returns ("", nil, nil) for valid but non-marshallable parameter. (e.g. "unexportedFunc()") +func marshalForMethod(typ reflect.Method, val reflect.Value) (string, interface{}, error) { + if val.Kind() != reflect.Func { + return "", nil, fmt.Errorf("expected func, got %v", val.Kind()) + } + name, numIn, numOut := typ.Name, val.Type().NumIn(), val.Type().NumOut() + _, blackListed := unmarshallableNames[name] + // FIXME: In text/template, (numOut == 2) is marshallable, + // if the type of the second param is error. + marshallable := unicode.IsUpper(rune(name[0])) && !blackListed && + numIn == 0 && numOut == 1 + if !marshallable { + return "", nil, nil + } + result := val.Call(make([]reflect.Value, numIn)) + intf := result[0].Interface() + return name, intf, nil +} diff --git a/components/cli/command/formatter/reflect_test.go b/components/cli/command/formatter/reflect_test.go new file mode 100644 index 0000000000..e547b18411 --- /dev/null +++ b/components/cli/command/formatter/reflect_test.go @@ -0,0 +1,66 @@ +package formatter + +import ( + "reflect" + "testing" +) + +type dummy struct { +} + +func (d *dummy) Func1() string { + return "Func1" +} + +func (d *dummy) func2() string { + return "func2(should not be marshalled)" +} + +func (d *dummy) Func3() (string, int) { + return "Func3(should not be marshalled)", -42 +} + +func (d *dummy) Func4() int { + return 4 +} + +type dummyType string + +func (d *dummy) Func5() dummyType { + return dummyType("Func5") +} + +func (d *dummy) FullHeader() string { + return "FullHeader(should not be marshalled)" +} + +var dummyExpected = map[string]interface{}{ + "Func1": "Func1", + "Func4": 4, + "Func5": dummyType("Func5"), +} + +func TestMarshalMap(t *testing.T) { + d := dummy{} + m, err := marshalMap(&d) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(dummyExpected, m) { + t.Fatalf("expected %+v, got %+v", + dummyExpected, m) + } +} + +func TestMarshalMapBad(t *testing.T) { + if _, err := marshalMap(nil); err == nil { + t.Fatal("expected an error (argument is nil)") + } + if _, err := marshalMap(dummy{}); err == nil { + t.Fatal("expected an error (argument is non-pointer)") + } + x := 42 + if _, err := marshalMap(&x); err == nil { + t.Fatal("expected an error (argument is a pointer to non-struct)") + } +} diff --git a/components/cli/command/formatter/service.go b/components/cli/command/formatter/service.go index a92326e75d..71ee4d656a 100644 --- a/components/cli/command/formatter/service.go +++ b/components/cli/command/formatter/service.go @@ -139,6 +139,10 @@ type serviceInspectContext struct { subContext } +func (ctx *serviceInspectContext) MarshalJSON() ([]byte, error) { + return marshalJSON(ctx) +} + func (ctx *serviceInspectContext) ID() string { return ctx.Service.ID } diff --git a/components/cli/command/formatter/volume.go b/components/cli/command/formatter/volume.go index 7bc3537573..b76c8ba039 100644 --- a/components/cli/command/formatter/volume.go +++ b/components/cli/command/formatter/volume.go @@ -52,6 +52,10 @@ type volumeContext struct { v types.Volume } +func (c *volumeContext) MarshalJSON() ([]byte, error) { + return marshalJSON(c) +} + func (c *volumeContext) Name() string { c.AddHeader(nameHeader) return c.v.Name diff --git a/components/cli/command/formatter/volume_test.go b/components/cli/command/formatter/volume_test.go index 8c715b3438..4e1c8d3ab9 100644 --- a/components/cli/command/formatter/volume_test.go +++ b/components/cli/command/formatter/volume_test.go @@ -2,6 +2,7 @@ package formatter import ( "bytes" + "encoding/json" "strings" "testing" @@ -142,3 +143,47 @@ foobar_bar } } } + +func TestVolumeContextWriteJSON(t *testing.T) { + volumes := []*types.Volume{ + {Driver: "foo", Name: "foobar_baz"}, + {Driver: "bar", Name: "foobar_bar"}, + } + expectedJSONs := []map[string]interface{}{ + {"Driver": "foo", "Labels": "", "Links": "N/A", "Mountpoint": "", "Name": "foobar_baz", "Scope": "", "Size": "N/A"}, + {"Driver": "bar", "Labels": "", "Links": "N/A", "Mountpoint": "", "Name": "foobar_bar", "Scope": "", "Size": "N/A"}, + } + out := bytes.NewBufferString("") + err := VolumeWrite(Context{Format: "{{json .}}", Output: out}, volumes) + if err != nil { + t.Fatal(err) + } + for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { + t.Logf("Output: line %d: %s", i, line) + var m map[string]interface{} + if err := json.Unmarshal([]byte(line), &m); err != nil { + t.Fatal(err) + } + assert.DeepEqual(t, m, expectedJSONs[i]) + } +} + +func TestVolumeContextWriteJSONField(t *testing.T) { + volumes := []*types.Volume{ + {Driver: "foo", Name: "foobar_baz"}, + {Driver: "bar", Name: "foobar_bar"}, + } + out := bytes.NewBufferString("") + err := VolumeWrite(Context{Format: "{{json .Name}}", Output: out}, volumes) + if err != nil { + t.Fatal(err) + } + for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { + t.Logf("Output: line %d: %s", i, line) + var s string + if err := json.Unmarshal([]byte(line), &s); err != nil { + t.Fatal(err) + } + assert.Equal(t, s, volumes[i].Name) + } +} diff --git a/components/cli/command/service/inspect_test.go b/components/cli/command/service/inspect_test.go index 8e73a70efa..04a65080c7 100644 --- a/components/cli/command/service/inspect_test.go +++ b/components/cli/command/service/inspect_test.go @@ -2,15 +2,17 @@ package service import ( "bytes" + "encoding/json" "strings" "testing" "time" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli/command/formatter" + "github.com/docker/docker/pkg/testutil/assert" ) -func TestPrettyPrintWithNoUpdateConfig(t *testing.T) { +func formatServiceInspect(t *testing.T, format formatter.Format, now time.Time) string { b := new(bytes.Buffer) endpointSpec := &swarm.EndpointSpec{ @@ -29,8 +31,8 @@ func TestPrettyPrintWithNoUpdateConfig(t *testing.T) { ID: "de179gar9d0o7ltdybungplod", Meta: swarm.Meta{ Version: swarm.Version{Index: 315}, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), + CreatedAt: now, + UpdatedAt: now, }, Spec: swarm.ServiceSpec{ Annotations: swarm.Annotations{ @@ -73,14 +75,14 @@ func TestPrettyPrintWithNoUpdateConfig(t *testing.T) { }, }, UpdateStatus: swarm.UpdateStatus{ - StartedAt: time.Now(), - CompletedAt: time.Now(), + StartedAt: now, + CompletedAt: now, }, } ctx := formatter.Context{ Output: b, - Format: formatter.NewServiceFormat("pretty"), + Format: format, } err := formatter.ServiceInspectWrite(ctx, []string{"de179gar9d0o7ltdybungplod"}, func(ref string) (interface{}, []byte, error) { @@ -89,8 +91,39 @@ func TestPrettyPrintWithNoUpdateConfig(t *testing.T) { if err != nil { t.Fatal(err) } + return b.String() +} - if strings.Contains(b.String(), "UpdateStatus") { +func TestPrettyPrintWithNoUpdateConfig(t *testing.T) { + s := formatServiceInspect(t, formatter.NewServiceFormat("pretty"), time.Now()) + if strings.Contains(s, "UpdateStatus") { t.Fatal("Pretty print failed before parsing UpdateStatus") } } + +func TestJSONFormatWithNoUpdateConfig(t *testing.T) { + now := time.Now() + // s1: [{"ID":..}] + // s2: {"ID":..} + s1 := formatServiceInspect(t, formatter.NewServiceFormat(""), now) + t.Log("// s1") + t.Logf("%s", s1) + s2 := formatServiceInspect(t, formatter.NewServiceFormat("{{json .}}"), now) + t.Log("// s2") + t.Logf("%s", s2) + var m1Wrap []map[string]interface{} + if err := json.Unmarshal([]byte(s1), &m1Wrap); err != nil { + t.Fatal(err) + } + if len(m1Wrap) != 1 { + t.Fatalf("strange s1=%s", s1) + } + m1 := m1Wrap[0] + t.Logf("m1=%+v", m1) + var m2 map[string]interface{} + if err := json.Unmarshal([]byte(s2), &m2); err != nil { + t.Fatal(err) + } + t.Logf("m2=%+v", m2) + assert.DeepEqual(t, m2, m1) +} From fc18090688b48e7a002112d6833abb384d394c18 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 17 Oct 2016 20:03:31 +0200 Subject: [PATCH 201/978] Revert docker volume column name to VOLUME_NAME Signed-off-by: Vincent Demeester Upstream-commit: 6c3c3a87ba4b3e80aeca4faa64a6eed057395c06 Component: cli --- components/cli/command/formatter/volume.go | 3 ++- components/cli/command/formatter/volume_test.go | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/components/cli/command/formatter/volume.go b/components/cli/command/formatter/volume.go index 7bc3537573..695885b2fa 100644 --- a/components/cli/command/formatter/volume.go +++ b/components/cli/command/formatter/volume.go @@ -12,6 +12,7 @@ const ( defaultVolumeQuietFormat = "{{.Name}}" defaultVolumeTableFormat = "table {{.Driver}}\t{{.Name}}" + volumeNameHeader = "VOLUME NAME" mountpointHeader = "MOUNTPOINT" linksHeader = "LINKS" // Status header ? @@ -53,7 +54,7 @@ type volumeContext struct { } func (c *volumeContext) Name() string { - c.AddHeader(nameHeader) + c.AddHeader(volumeNameHeader) return c.v.Name } diff --git a/components/cli/command/formatter/volume_test.go b/components/cli/command/formatter/volume_test.go index 8c715b3438..8b92e8e164 100644 --- a/components/cli/command/formatter/volume_test.go +++ b/components/cli/command/formatter/volume_test.go @@ -22,7 +22,7 @@ func TestVolumeContext(t *testing.T) { }{ {volumeContext{ v: types.Volume{Name: volumeName}, - }, volumeName, nameHeader, ctx.Name}, + }, volumeName, volumeNameHeader, ctx.Name}, {volumeContext{ v: types.Volume{Driver: "driver_name"}, }, "driver_name", driverHeader, ctx.Driver}, @@ -76,7 +76,7 @@ func TestVolumeContextWrite(t *testing.T) { // Table format { Context{Format: NewVolumeFormat("table", false)}, - `DRIVER NAME + `DRIVER VOLUME NAME foo foobar_baz bar foobar_bar `, @@ -89,14 +89,14 @@ foobar_bar }, { Context{Format: NewVolumeFormat("table {{.Name}}", false)}, - `NAME + `VOLUME NAME foobar_baz foobar_bar `, }, { Context{Format: NewVolumeFormat("table {{.Name}}", true)}, - `NAME + `VOLUME NAME foobar_baz foobar_bar `, From a195f2d4336a7a84a4ff3a84aceeda2fd8a74315 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Mon, 6 Jun 2016 20:29:05 -0700 Subject: [PATCH 202/978] Update `docker stop` and `docker restart` to allow not specifying timeout and use the one specified at container creation time. Signed-off-by: Yong Tang Upstream-commit: c424fb0e3dd38209c0a089a34bedc4bd3a1a9702 Component: cli --- components/cli/command/container/restart.go | 13 ++++++++++--- components/cli/command/container/stop.go | 13 ++++++++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/components/cli/command/container/restart.go b/components/cli/command/container/restart.go index e370ef4010..fc3ba93c84 100644 --- a/components/cli/command/container/restart.go +++ b/components/cli/command/container/restart.go @@ -13,7 +13,8 @@ import ( ) type restartOptions struct { - nSeconds int + nSeconds int + nSecondsChanged bool containers []string } @@ -28,6 +29,7 @@ func NewRestartCommand(dockerCli *command.DockerCli) *cobra.Command { Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.containers = args + opts.nSecondsChanged = cmd.Flags().Changed("time") return runRestart(dockerCli, &opts) }, } @@ -40,9 +42,14 @@ func NewRestartCommand(dockerCli *command.DockerCli) *cobra.Command { func runRestart(dockerCli *command.DockerCli, opts *restartOptions) error { ctx := context.Background() var errs []string + var timeout *time.Duration + if opts.nSecondsChanged { + timeoutValue := time.Duration(opts.nSeconds) * time.Second + timeout = &timeoutValue + } + for _, name := range opts.containers { - timeout := time.Duration(opts.nSeconds) * time.Second - if err := dockerCli.Client().ContainerRestart(ctx, name, &timeout); err != nil { + if err := dockerCli.Client().ContainerRestart(ctx, name, timeout); err != nil { errs = append(errs, err.Error()) } else { fmt.Fprintf(dockerCli.Out(), "%s\n", name) diff --git a/components/cli/command/container/stop.go b/components/cli/command/container/stop.go index 2f22fd09a4..c68ede5368 100644 --- a/components/cli/command/container/stop.go +++ b/components/cli/command/container/stop.go @@ -13,7 +13,8 @@ import ( ) type stopOptions struct { - time int + time int + timeChanged bool containers []string } @@ -28,6 +29,7 @@ func NewStopCommand(dockerCli *command.DockerCli) *cobra.Command { Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.containers = args + opts.timeChanged = cmd.Flags().Changed("time") return runStop(dockerCli, &opts) }, } @@ -39,12 +41,17 @@ func NewStopCommand(dockerCli *command.DockerCli) *cobra.Command { func runStop(dockerCli *command.DockerCli, opts *stopOptions) error { ctx := context.Background() - timeout := time.Duration(opts.time) * time.Second + + var timeout *time.Duration + if opts.timeChanged { + timeoutValue := time.Duration(opts.time) * time.Second + timeout = &timeoutValue + } var errs []string errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, id string) error { - return dockerCli.Client().ContainerStop(ctx, id, &timeout) + return dockerCli.Client().ContainerStop(ctx, id, timeout) }) for _, container := range opts.containers { if err := <-errChan; err != nil { From 52dbab66cbca2b52ed035aa3e02e4cdaca561d56 Mon Sep 17 00:00:00 2001 From: allencloud Date: Tue, 18 Oct 2016 14:20:12 +0800 Subject: [PATCH 203/978] wrap line in deleted containers when pruning Signed-off-by: allencloud Upstream-commit: 093072cc187b2a38b48348fc6c142c8b296832f5 Component: cli --- components/cli/command/container/prune.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/container/prune.go b/components/cli/command/container/prune.go index be67fe4ca9..679471398a 100644 --- a/components/cli/command/container/prune.go +++ b/components/cli/command/container/prune.go @@ -57,7 +57,7 @@ func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed u } if len(report.ContainersDeleted) > 0 { - output = "Deleted Containers:" + output = "Deleted Containers:\n" for _, id := range report.ContainersDeleted { output += id + "\n" } From 655c82e562c8eadb9184ab30ef94a0145f47af9c Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Tue, 18 Oct 2016 18:50:11 +0800 Subject: [PATCH 204/978] Fix typs from go to Go Signed-off-by: yuexiao-wang Upstream-commit: 24d0191a3a73506532b835e87ba3c7925aa6ef90 Component: cli --- components/cli/command/container/inspect.go | 2 +- components/cli/command/image/inspect.go | 2 +- components/cli/command/network/inspect.go | 2 +- components/cli/command/node/inspect.go | 2 +- components/cli/command/plugin/inspect.go | 2 +- components/cli/command/service/inspect.go | 2 +- components/cli/command/system/events.go | 2 +- components/cli/command/system/info.go | 2 +- components/cli/command/system/inspect.go | 2 +- components/cli/command/system/version.go | 2 +- components/cli/command/volume/inspect.go | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/components/cli/command/container/inspect.go b/components/cli/command/container/inspect.go index 0bef51a61d..08a8d244df 100644 --- a/components/cli/command/container/inspect.go +++ b/components/cli/command/container/inspect.go @@ -30,7 +30,7 @@ func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { } flags := cmd.Flags() - flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template") + flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") flags.BoolVarP(&opts.size, "size", "s", false, "Display total file sizes") return cmd diff --git a/components/cli/command/image/inspect.go b/components/cli/command/image/inspect.go index 11c528ef2a..217863c772 100644 --- a/components/cli/command/image/inspect.go +++ b/components/cli/command/image/inspect.go @@ -29,7 +29,7 @@ func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { } flags := cmd.Flags() - flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template") + flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") return cmd } diff --git a/components/cli/command/network/inspect.go b/components/cli/command/network/inspect.go index f1f677db9c..1a86855f71 100644 --- a/components/cli/command/network/inspect.go +++ b/components/cli/command/network/inspect.go @@ -27,7 +27,7 @@ func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { }, } - cmd.Flags().StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template") + cmd.Flags().StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") return cmd } diff --git a/components/cli/command/node/inspect.go b/components/cli/command/node/inspect.go index a11182f082..0812ec5eab 100644 --- a/components/cli/command/node/inspect.go +++ b/components/cli/command/node/inspect.go @@ -36,7 +36,7 @@ func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { } flags := cmd.Flags() - flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template") + flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") flags.BoolVar(&opts.pretty, "pretty", false, "Print the information in a human friendly format.") return cmd } diff --git a/components/cli/command/plugin/inspect.go b/components/cli/command/plugin/inspect.go index a1cf1f7b0e..e5059629e5 100644 --- a/components/cli/command/plugin/inspect.go +++ b/components/cli/command/plugin/inspect.go @@ -32,7 +32,7 @@ func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { } flags := cmd.Flags() - flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template") + flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") return cmd } diff --git a/components/cli/command/service/inspect.go b/components/cli/command/service/inspect.go index 054c24383e..deb701bf6d 100644 --- a/components/cli/command/service/inspect.go +++ b/components/cli/command/service/inspect.go @@ -37,7 +37,7 @@ func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { } flags := cmd.Flags() - flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template") + flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") flags.BoolVar(&opts.pretty, "pretty", false, "Print the information in a human friendly format.") return cmd } diff --git a/components/cli/command/system/events.go b/components/cli/command/system/events.go index 7b5fb592cb..087523051a 100644 --- a/components/cli/command/system/events.go +++ b/components/cli/command/system/events.go @@ -45,7 +45,7 @@ func NewEventsCommand(dockerCli *command.DockerCli) *cobra.Command { flags.StringVar(&opts.since, "since", "", "Show all events created since timestamp") flags.StringVar(&opts.until, "until", "", "Stream events until this timestamp") flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") - flags.StringVar(&opts.format, "format", "", "Format the output using the given go template") + flags.StringVar(&opts.format, "format", "", "Format the output using the given Go template") return cmd } diff --git a/components/cli/command/system/info.go b/components/cli/command/system/info.go index 8116ae524a..fb5ea17d7b 100644 --- a/components/cli/command/system/info.go +++ b/components/cli/command/system/info.go @@ -37,7 +37,7 @@ func NewInfoCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() - flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template") + flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") return cmd } diff --git a/components/cli/command/system/inspect.go b/components/cli/command/system/inspect.go index 015c1b5c6d..d7a24854e7 100644 --- a/components/cli/command/system/inspect.go +++ b/components/cli/command/system/inspect.go @@ -35,7 +35,7 @@ func NewInspectCommand(dockerCli *command.DockerCli) *cobra.Command { } flags := cmd.Flags() - flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template") + flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") flags.StringVar(&opts.inspectType, "type", "", "Return JSON for specified type") flags.BoolVarP(&opts.size, "size", "s", false, "Display total file sizes if the type is container") diff --git a/components/cli/command/system/version.go b/components/cli/command/system/version.go index e77719ec3b..7959bf564f 100644 --- a/components/cli/command/system/version.go +++ b/components/cli/command/system/version.go @@ -52,7 +52,7 @@ func NewVersionCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() - flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template") + flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") return cmd } diff --git a/components/cli/command/volume/inspect.go b/components/cli/command/volume/inspect.go index ab06e03807..5eb8ad2516 100644 --- a/components/cli/command/volume/inspect.go +++ b/components/cli/command/volume/inspect.go @@ -28,7 +28,7 @@ func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { }, } - cmd.Flags().StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template") + cmd.Flags().StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") return cmd } From 43c228543ab0df0d4fa08d56ba89129b228938fc Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Tue, 12 Jul 2016 05:08:05 -0700 Subject: [PATCH 205/978] Remove duplicate keys in labels of `docker info` This fix tries to address the issue raised in 24392 where labels with duplicate keys exist in `docker info`, which contradicts with the specifications in the docs. The reason for duplicate keys is that labels are stored as slice of strings in the format of `A=B` (and the input/output). This fix tries to address this issue by checking conflict labels when daemon started, and remove duplicate labels (K-V). The existing `/info` API has not been changed. An additional integration test has been added to cover the changes in this fix. This fix fixes 24392. Signed-off-by: Yong Tang Upstream-commit: 805f66951279850688ee9a4478ad76445dea2c15 Component: cli --- components/cli/command/system/info.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/components/cli/command/system/info.go b/components/cli/command/system/info.go index fb5ea17d7b..300a3960bb 100644 --- a/components/cli/command/system/info.go +++ b/components/cli/command/system/info.go @@ -223,6 +223,21 @@ func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error { for _, attribute := range info.Labels { fmt.Fprintf(dockerCli.Out(), " %s\n", attribute) } + // TODO: Engine labels with duplicate keys has been deprecated in 1.13 and will be error out + // after 3 release cycles (1.16). For now, a WARNING will be generated. The following will + // be removed eventually. + labelMap := map[string]string{} + for _, label := range info.Labels { + stringSlice := strings.SplitN(label, "=", 2) + if len(stringSlice) > 1 { + // If there is a conflict we will throw out an warning + if v, ok := labelMap[stringSlice[0]]; ok && v != stringSlice[1] { + fmt.Fprintln(dockerCli.Err(), "WARNING: labels with duplicate keys and conflicting values have been deprecated") + break + } + labelMap[stringSlice[0]] = stringSlice[1] + } + } } ioutils.FprintfIfTrue(dockerCli.Out(), "Experimental: %v\n", info.ExperimentalBuild) From 58b052f94e58104b92fdeb3d294ca4ab65ca8276 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Wed, 7 Sep 2016 16:32:44 -0700 Subject: [PATCH 206/978] API changes for service rollback and failure threshold Signed-off-by: Aaron Lehmann Upstream-commit: 671fe5c051ebfdb7217dedc049c4acc7e8e6497e Component: cli --- components/cli/service_update.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/components/cli/service_update.go b/components/cli/service_update.go index c5d07e8394..8e03f7f483 100644 --- a/components/cli/service_update.go +++ b/components/cli/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 64a984b1c74f33d098abdf42c5bc569ef43bc50e Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Fri, 2 Sep 2016 14:12:05 -0700 Subject: [PATCH 207/978] Service update failure thresholds and rollback This adds support for two enhancements to swarm service rolling updates: - Failure thresholds: In Docker 1.12, a service update could be set up to either pause or continue after a single failure occurs. This adds an --update-max-failure-ratio flag that controls how many tasks need to fail to update for the update as a whole to be considered a failure. A counterpart flag, --update-monitor, controls how long to monitor each task for a failure after starting it during the update. - Rollback flag: service update --rollback reverts the service to its previous version. If a service update encounters task failures, or fails to function properly for some other reason, the user can roll back the update. SwarmKit also has the ability to roll back updates automatically after hitting the failure thresholds, but we've decided not to expose this in the Docker API/CLI for now, favoring a workflow where the decision to roll back is always made by an admin. Depending on user feedback, we may add a "rollback" option to --update-failure-action in the future. Signed-off-by: Aaron Lehmann Upstream-commit: 06ebd4517db1bdd4d4ca699d276a67f4de1071c3 Component: cli --- components/cli/command/formatter/service.go | 18 +++- components/cli/command/service/opts.go | 104 +++++++++++--------- components/cli/command/service/update.go | 34 ++++++- 3 files changed, 103 insertions(+), 53 deletions(-) diff --git a/components/cli/command/formatter/service.go b/components/cli/command/formatter/service.go index 71ee4d656a..1549047b72 100644 --- a/components/cli/command/formatter/service.go +++ b/components/cli/command/formatter/service.go @@ -41,10 +41,14 @@ Placement: {{- if .HasUpdateConfig }} UpdateConfig: Parallelism: {{ .UpdateParallelism }} -{{- if .HasUpdateDelay -}} +{{- if .HasUpdateDelay}} Delay: {{ .UpdateDelay }} {{- end }} On failure: {{ .UpdateOnFailure }} +{{- if .HasUpdateMonitor}} + Monitoring Period: {{ .UpdateMonitor }} +{{- end }} + Max failure ratio: {{ .UpdateMaxFailureRatio }} {{- end }} ContainerSpec: Image: {{ .ContainerImage }} @@ -218,6 +222,18 @@ func (ctx *serviceInspectContext) UpdateOnFailure() string { return ctx.Service.Spec.UpdateConfig.FailureAction } +func (ctx *serviceInspectContext) HasUpdateMonitor() bool { + return ctx.Service.Spec.UpdateConfig.Monitor.Nanoseconds() > 0 +} + +func (ctx *serviceInspectContext) UpdateMonitor() time.Duration { + return ctx.Service.Spec.UpdateConfig.Monitor +} + +func (ctx *serviceInspectContext) UpdateMaxFailureRatio() float32 { + return ctx.Service.Spec.UpdateConfig.MaxFailureRatio +} + func (ctx *serviceInspectContext) ContainerImage() string { return ctx.Service.Spec.TaskTemplate.ContainerSpec.Image } diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index 1e966f90c6..cf25b78273 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -267,9 +267,11 @@ func (m *MountOpt) Value() []mounttypes.Mount { } type updateOptions struct { - parallelism uint64 - delay time.Duration - onFailure string + parallelism uint64 + delay time.Duration + monitor time.Duration + onFailure string + maxFailureRatio float32 } type resourceOptions struct { @@ -458,9 +460,11 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { Networks: convertNetworks(opts.networks), Mode: swarm.ServiceMode{}, UpdateConfig: &swarm.UpdateConfig{ - Parallelism: opts.update.parallelism, - Delay: opts.update.delay, - FailureAction: opts.update.onFailure, + Parallelism: opts.update.parallelism, + Delay: opts.update.delay, + Monitor: opts.update.monitor, + FailureAction: opts.update.onFailure, + MaxFailureRatio: opts.update.maxFailureRatio, }, EndpointSpec: opts.endpoint.ToEndpointSpec(), } @@ -507,7 +511,9 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { flags.Uint64Var(&opts.update.parallelism, flagUpdateParallelism, 1, "Maximum number of tasks updated simultaneously (0 to update all at once)") flags.DurationVar(&opts.update.delay, flagUpdateDelay, time.Duration(0), "Delay between updates") + flags.DurationVar(&opts.update.monitor, flagUpdateMonitor, time.Duration(0), "Duration after each task update to monitor for failure") flags.StringVar(&opts.update.onFailure, flagUpdateFailureAction, "pause", "Action on update failure (pause|continue)") + flags.Float32Var(&opts.update.maxFailureRatio, flagUpdateMaxFailureRatio, 0, "Failure rate to tolerate during an update") flags.StringVar(&opts.endpoint.mode, flagEndpointMode, "", "Endpoint mode (vip or dnsrr)") @@ -518,46 +524,48 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { } const ( - flagConstraint = "constraint" - flagConstraintRemove = "constraint-rm" - flagConstraintAdd = "constraint-add" - flagContainerLabel = "container-label" - flagContainerLabelRemove = "container-label-rm" - flagContainerLabelAdd = "container-label-add" - flagEndpointMode = "endpoint-mode" - flagEnv = "env" - flagEnvRemove = "env-rm" - flagEnvAdd = "env-add" - flagGroupAdd = "group-add" - flagGroupRemove = "group-rm" - flagLabel = "label" - flagLabelRemove = "label-rm" - flagLabelAdd = "label-add" - flagLimitCPU = "limit-cpu" - flagLimitMemory = "limit-memory" - flagMode = "mode" - flagMount = "mount" - flagMountRemove = "mount-rm" - flagMountAdd = "mount-add" - flagName = "name" - flagNetwork = "network" - flagPublish = "publish" - flagPublishRemove = "publish-rm" - flagPublishAdd = "publish-add" - flagReplicas = "replicas" - flagReserveCPU = "reserve-cpu" - flagReserveMemory = "reserve-memory" - flagRestartCondition = "restart-condition" - flagRestartDelay = "restart-delay" - flagRestartMaxAttempts = "restart-max-attempts" - flagRestartWindow = "restart-window" - flagStopGracePeriod = "stop-grace-period" - flagUpdateDelay = "update-delay" - flagUpdateFailureAction = "update-failure-action" - flagUpdateParallelism = "update-parallelism" - flagUser = "user" - flagWorkdir = "workdir" - flagRegistryAuth = "with-registry-auth" - flagLogDriver = "log-driver" - flagLogOpt = "log-opt" + flagConstraint = "constraint" + flagConstraintRemove = "constraint-rm" + flagConstraintAdd = "constraint-add" + flagContainerLabel = "container-label" + flagContainerLabelRemove = "container-label-rm" + flagContainerLabelAdd = "container-label-add" + flagEndpointMode = "endpoint-mode" + flagEnv = "env" + flagEnvRemove = "env-rm" + flagEnvAdd = "env-add" + flagGroupAdd = "group-add" + flagGroupRemove = "group-rm" + flagLabel = "label" + flagLabelRemove = "label-rm" + flagLabelAdd = "label-add" + flagLimitCPU = "limit-cpu" + flagLimitMemory = "limit-memory" + flagMode = "mode" + flagMount = "mount" + flagMountRemove = "mount-rm" + flagMountAdd = "mount-add" + flagName = "name" + flagNetwork = "network" + flagPublish = "publish" + flagPublishRemove = "publish-rm" + flagPublishAdd = "publish-add" + flagReplicas = "replicas" + flagReserveCPU = "reserve-cpu" + flagReserveMemory = "reserve-memory" + flagRestartCondition = "restart-condition" + flagRestartDelay = "restart-delay" + flagRestartMaxAttempts = "restart-max-attempts" + flagRestartWindow = "restart-window" + flagStopGracePeriod = "stop-grace-period" + flagUpdateDelay = "update-delay" + flagUpdateFailureAction = "update-failure-action" + flagUpdateMaxFailureRatio = "update-max-failure-ratio" + flagUpdateMonitor = "update-monitor" + flagUpdateParallelism = "update-parallelism" + flagUser = "user" + flagWorkdir = "workdir" + flagRegistryAuth = "with-registry-auth" + flagLogDriver = "log-driver" + flagLogOpt = "log-opt" ) diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index be3218ed60..797c989271 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -36,6 +36,7 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() flags.String("image", "", "Service image tag") flags.String("args", "", "Service command args") + flags.Bool("rollback", false, "Rollback to previous specification") addServiceFlags(cmd, opts) flags.Var(newListOptsVar(), flagEnvRemove, "Remove an environment variable") @@ -68,7 +69,20 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID str return err } - err = updateService(flags, &service.Spec) + rollback, err := flags.GetBool("rollback") + if err != nil { + return err + } + + spec := &service.Spec + if rollback { + spec = service.PreviousSpec + if spec == nil { + return fmt.Errorf("service does not have a previous specification to roll back to") + } + } + + err = updateService(flags, spec) if err != nil { return err } @@ -81,15 +95,19 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID str if sendAuth { // Retrieve encoded auth token from the image reference // This would be the old image if it didn't change in this update - image := service.Spec.TaskTemplate.ContainerSpec.Image + image := spec.TaskTemplate.ContainerSpec.Image encodedAuth, err := command.RetrieveAuthTokenFromImage(ctx, dockerCli, image) if err != nil { return err } updateOpts.EncodedRegistryAuth = encodedAuth + } else if rollback { + updateOpts.RegistryAuthFrom = types.RegistryAuthFromPreviousSpec + } else { + updateOpts.RegistryAuthFrom = types.RegistryAuthFromSpec } - err = apiClient.ServiceUpdate(ctx, service.ID, service.Version, service.Spec, updateOpts) + err = apiClient.ServiceUpdate(ctx, service.ID, service.Version, *spec, updateOpts) if err != nil { return err } @@ -111,6 +129,12 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error { } } + updateFloat32 := func(flag string, field *float32) { + if flags.Changed(flag) { + *field, _ = flags.GetFloat32(flag) + } + } + updateDuration := func(flag string, field *time.Duration) { if flags.Changed(flag) { *field, _ = flags.GetDuration(flag) @@ -195,13 +219,15 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error { return err } - if anyChanged(flags, flagUpdateParallelism, flagUpdateDelay, flagUpdateFailureAction) { + if anyChanged(flags, flagUpdateParallelism, flagUpdateDelay, flagUpdateMonitor, flagUpdateFailureAction, flagUpdateMaxFailureRatio) { if spec.UpdateConfig == nil { spec.UpdateConfig = &swarm.UpdateConfig{} } updateUint64(flagUpdateParallelism, &spec.UpdateConfig.Parallelism) updateDuration(flagUpdateDelay, &spec.UpdateConfig.Delay) + updateDuration(flagUpdateMonitor, &spec.UpdateConfig.Monitor) updateString(flagUpdateFailureAction, &spec.UpdateConfig.FailureAction) + updateFloat32(flagUpdateMaxFailureRatio, &spec.UpdateConfig.MaxFailureRatio) } if flags.Changed(flagEndpointMode) { From 0cbbc04944fd815ff051fe3a4d897091b184a8f8 Mon Sep 17 00:00:00 2001 From: allencloud Date: Wed, 19 Oct 2016 06:29:27 +0800 Subject: [PATCH 208/978] make every node and plugin removal call api Signed-off-by: allencloud Upstream-commit: 49512f901c0d2f340debc4e22cb3dbb55b2d9af1 Component: cli --- components/cli/command/node/remove.go | 12 +++++++++++- components/cli/command/plugin/remove.go | 6 ++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/components/cli/command/node/remove.go b/components/cli/command/node/remove.go index 696cd58716..9ba21b44a2 100644 --- a/components/cli/command/node/remove.go +++ b/components/cli/command/node/remove.go @@ -2,6 +2,7 @@ package node import ( "fmt" + "strings" "golang.org/x/net/context" @@ -35,12 +36,21 @@ func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { func runRemove(dockerCli *command.DockerCli, args []string, opts removeOptions) error { client := dockerCli.Client() ctx := context.Background() + + var errs []string + for _, nodeID := range args { err := client.NodeRemove(ctx, nodeID, types.NodeRemoveOptions{Force: opts.force}) if err != nil { - return err + errs = append(errs, err.Error()) + continue } fmt.Fprintf(dockerCli.Out(), "%s\n", nodeID) } + + if len(errs) > 0 { + return fmt.Errorf("%s", strings.Join(errs, "\n")) + } + return nil } diff --git a/components/cli/command/plugin/remove.go b/components/cli/command/plugin/remove.go index 800fc1b97f..4222690a4f 100644 --- a/components/cli/command/plugin/remove.go +++ b/components/cli/command/plugin/remove.go @@ -45,14 +45,16 @@ func runRemove(dockerCli *command.DockerCli, opts *rmOptions) error { for _, name := range opts.plugins { named, err := reference.ParseNamed(name) // FIXME: validate if err != nil { - return err + errs = append(errs, err) + continue } if reference.IsNameOnly(named) { named = reference.WithDefaultTag(named) } ref, ok := named.(reference.NamedTagged) if !ok { - return fmt.Errorf("invalid name: %s", named.String()) + errs = append(errs, fmt.Errorf("invalid name: %s", named.String())) + continue } // TODO: pass names to api instead of making multiple api calls if err := dockerCli.Client().PluginRemove(ctx, ref.String(), types.PluginRemoveOptions{Force: opts.force}); err != nil { From 448fe45059c9adb01d6c10ec2dcc518a67cf20c3 Mon Sep 17 00:00:00 2001 From: allencloud Date: Wed, 19 Oct 2016 14:35:05 +0800 Subject: [PATCH 209/978] change join node role judge Signed-off-by: allencloud Upstream-commit: f2a6d37388a66faf37ccf037fbf9a906b9e5167b Component: cli --- components/cli/command/swarm/join.go | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/components/cli/command/swarm/join.go b/components/cli/command/swarm/join.go index 72f97c015e..004313b4c6 100644 --- a/components/cli/command/swarm/join.go +++ b/components/cli/command/swarm/join.go @@ -2,7 +2,6 @@ package swarm import ( "fmt" - "strings" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" @@ -61,15 +60,10 @@ func runJoin(dockerCli *command.DockerCli, opts joinOptions) error { return err } - _, _, err = client.NodeInspectWithRaw(ctx, info.Swarm.NodeID) - if err != nil { - // TODO(aaronl): is there a better way to do this? - if strings.Contains(err.Error(), "This node is not a swarm manager.") { - fmt.Fprintln(dockerCli.Out(), "This node joined a swarm as a worker.") - } - } else { + if info.Swarm.ControlAvailable { fmt.Fprintln(dockerCli.Out(), "This node joined a swarm as a manager.") + } else { + fmt.Fprintln(dockerCli.Out(), "This node joined a swarm as a worker.") } - return nil } From 3a00f9dfc73a31901e757ce2ae8bbc2c19608f1b Mon Sep 17 00:00:00 2001 From: Jonh Wendell Date: Wed, 13 Jul 2016 14:24:41 -0300 Subject: [PATCH 210/978] Exec: Add ability to set environment variables Keeping the current behavior for exec, i.e., inheriting variables from main process. New variables will be added to current ones. If there's already a variable with that name it will be overwritten. Example of usage: docker exec -it -e TERM=vt100 top Closes #24355. Signed-off-by: Jonh Wendell Upstream-commit: a528b05dab7e47fa8667ab3c96e7d60bc679d3b5 Component: cli --- components/cli/command/container/exec.go | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/components/cli/command/container/exec.go b/components/cli/command/container/exec.go index 1682a7ca64..48964693b2 100644 --- a/components/cli/command/container/exec.go +++ b/components/cli/command/container/exec.go @@ -11,7 +11,9 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" apiclient "github.com/docker/docker/client" + options "github.com/docker/docker/opts" "github.com/docker/docker/pkg/promise" + runconfigopts "github.com/docker/docker/runconfig/opts" "github.com/spf13/cobra" ) @@ -22,11 +24,19 @@ type execOptions struct { detach bool user string privileged bool + env *options.ListOpts +} + +func newExecOptions() *execOptions { + var values []string + return &execOptions{ + env: options.NewListOptsRef(&values, runconfigopts.ValidateEnv), + } } // NewExecCommand creats a new cobra.Command for `docker exec` func NewExecCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts execOptions + opts := newExecOptions() cmd := &cobra.Command{ Use: "exec [OPTIONS] CONTAINER COMMAND [ARG...]", @@ -35,7 +45,7 @@ func NewExecCommand(dockerCli *command.DockerCli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { container := args[0] execCmd := args[1:] - return runExec(dockerCli, &opts, container, execCmd) + return runExec(dockerCli, opts, container, execCmd) }, } @@ -48,6 +58,7 @@ func NewExecCommand(dockerCli *command.DockerCli) *cobra.Command { flags.BoolVarP(&opts.detach, "detach", "d", false, "Detached mode: run command in the background") flags.StringVarP(&opts.user, "user", "u", "", "Username or UID (format: [:])") flags.BoolVarP(&opts.privileged, "privileged", "", false, "Give extended privileges to the command") + flags.VarP(opts.env, "env", "e", "Set environment variables") return cmd } @@ -188,5 +199,9 @@ func parseExec(opts *execOptions, container string, execCmd []string) (*types.Ex } } + if opts.env != nil { + execConfig.Env = opts.env.GetAll() + } + return execConfig, nil } From 731dd426a811373ee1f55c4393e1944035549749 Mon Sep 17 00:00:00 2001 From: Andy Goldstein Date: Mon, 19 Sep 2016 14:55:52 -0400 Subject: [PATCH 211/978] Add Logs to ContainerAttachOptions Signed-off-by: Andy Goldstein Upstream-commit: 27bab36800713d248c1f7cb7f3666926426eabf2 Component: cli --- components/cli/container_attach.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/components/cli/container_attach.go b/components/cli/container_attach.go index 7cfc860fcc..eea4682158 100644 --- a/components/cli/container_attach.go +++ b/components/cli/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 b2507ea9730dd2fcc923bfaf7152e6d4beb7e953 Mon Sep 17 00:00:00 2001 From: Kenfe-Mickael Laventure Date: Wed, 19 Oct 2016 15:09:42 -0700 Subject: [PATCH 212/978] Add Networks placeholder to ps --format Passing {{.Networks}} to the format parameter will prompt ps to display all the networks the container is connected to. Signed-off-by: Kenfe-Mickael Laventure Upstream-commit: 08ac5a303969838178d344a457b0de60475cb73c Component: cli --- components/cli/command/container/list.go | 7 +++++++ components/cli/command/formatter/container.go | 16 ++++++++++++++++ .../cli/command/formatter/container_test.go | 4 ++-- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/components/cli/command/container/list.go b/components/cli/command/container/list.go index 7f10ce8bd1..2d46b6604e 100644 --- a/components/cli/command/container/list.go +++ b/components/cli/command/container/list.go @@ -70,6 +70,13 @@ func (p *preProcessor) Size() bool { return true } +// Networks does nothing but return true. +// It is needed to avoid the template check to fail as this field +// doesn't exist in `types.Container` +func (p *preProcessor) Networks() bool { + return true +} + func buildContainerListOptions(opts *psOptions) (*types.ContainerListOptions, error) { options := &types.ContainerListOptions{ All: opts.all, diff --git a/components/cli/command/formatter/container.go b/components/cli/command/formatter/container.go index 094bc85447..6273453355 100644 --- a/components/cli/command/formatter/container.go +++ b/components/cli/command/formatter/container.go @@ -24,6 +24,7 @@ const ( portsHeader = "PORTS" mountsHeader = "MOUNTS" localVolumes = "LOCAL VOLUMES" + networksHeader = "NETWORKS" ) // NewContainerFormat returns a Format for rendering using a Context @@ -217,3 +218,18 @@ func (c *containerContext) LocalVolumes() string { return fmt.Sprintf("%d", count) } + +func (c *containerContext) Networks() string { + c.AddHeader(networksHeader) + + if c.c.NetworkSettings == nil { + return "" + } + + networks := []string{} + for k := range c.c.NetworkSettings.Networks { + networks = append(networks, k) + } + + return strings.Join(networks, ",") +} diff --git a/components/cli/command/formatter/container_test.go b/components/cli/command/formatter/container_test.go index 4b520f94ba..0a844efb65 100644 --- a/components/cli/command/formatter/container_test.go +++ b/components/cli/command/formatter/container_test.go @@ -333,8 +333,8 @@ func TestContainerContextWriteJSON(t *testing.T) { } expectedCreated := time.Unix(unix, 0).String() expectedJSONs := []map[string]interface{}{ - {"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID1", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_baz", "Ports": "", "RunningFor": "About a minute", "Size": "0 B", "Status": ""}, - {"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID2", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_bar", "Ports": "", "RunningFor": "About a minute", "Size": "0 B", "Status": ""}, + {"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID1", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_baz", "Networks": "", "Ports": "", "RunningFor": "About a minute", "Size": "0 B", "Status": ""}, + {"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID2", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_bar", "Networks": "", "Ports": "", "RunningFor": "About a minute", "Size": "0 B", "Status": ""}, } out := bytes.NewBufferString("") err := ContainerWrite(Context{Format: "{{json .}}", Output: out}, containers) From a202fe77794f006d4f6e40985192dcd2f1adae7b Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 3 Oct 2016 15:17:39 -0400 Subject: [PATCH 213/978] Generate api/types:Image from the swagger spec and rename it to a more appropriate name ImageSummary. Signed-off-by: Daniel Nephin Upstream-commit: ef87035bbb83de19a4ca8118c2a8a10675ea3721 Component: cli --- .../cli/command/formatter/disk_usage.go | 4 ++-- components/cli/command/formatter/image.go | 8 ++++---- .../cli/command/formatter/image_test.go | 20 +++++++++---------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/components/cli/command/formatter/disk_usage.go b/components/cli/command/formatter/disk_usage.go index acb210dbff..6f97d3b0f9 100644 --- a/components/cli/command/formatter/disk_usage.go +++ b/components/cli/command/formatter/disk_usage.go @@ -31,7 +31,7 @@ type DiskUsageContext struct { Context Verbose bool LayersSize int64 - Images []*types.Image + Images []*types.ImageSummary Containers []*types.Container Volumes []*types.Volume } @@ -155,7 +155,7 @@ func (ctx *DiskUsageContext) Write() { type diskUsageImagesContext struct { HeaderContext totalSize int64 - images []*types.Image + images []*types.ImageSummary } func (c *diskUsageImagesContext) Type() string { diff --git a/components/cli/command/formatter/image.go b/components/cli/command/formatter/image.go index 1e71bda3aa..594b2f3926 100644 --- a/components/cli/command/formatter/image.go +++ b/components/cli/command/formatter/image.go @@ -26,7 +26,7 @@ type ImageContext struct { Digest bool } -func isDangling(image types.Image) bool { +func isDangling(image types.ImageSummary) bool { return len(image.RepoTags) == 1 && image.RepoTags[0] == ":" && len(image.RepoDigests) == 1 && image.RepoDigests[0] == "@" } @@ -72,14 +72,14 @@ virtual_size: {{.Size}} } // ImageWrite writes the formatter images using the ImageContext -func ImageWrite(ctx ImageContext, images []types.Image) error { +func ImageWrite(ctx ImageContext, images []types.ImageSummary) error { render := func(format func(subContext subContext) error) error { return imageFormat(ctx, images, format) } return ctx.Write(&imageContext{}, render) } -func imageFormat(ctx ImageContext, images []types.Image, format func(subContext subContext) error) error { +func imageFormat(ctx ImageContext, images []types.ImageSummary, format func(subContext subContext) error) error { for _, image := range images { images := []*imageContext{} if isDangling(image) { @@ -184,7 +184,7 @@ func imageFormat(ctx ImageContext, images []types.Image, format func(subContext type imageContext struct { HeaderContext trunc bool - i types.Image + i types.ImageSummary repo string tag string digest string diff --git a/components/cli/command/formatter/image_test.go b/components/cli/command/formatter/image_test.go index 73b3c3f2e9..ffe77f6677 100644 --- a/components/cli/command/formatter/image_test.go +++ b/components/cli/command/formatter/image_test.go @@ -24,36 +24,36 @@ func TestImageContext(t *testing.T) { call func() string }{ {imageContext{ - i: types.Image{ID: imageID}, + i: types.ImageSummary{ID: imageID}, trunc: true, }, stringid.TruncateID(imageID), imageIDHeader, ctx.ID}, {imageContext{ - i: types.Image{ID: imageID}, + i: types.ImageSummary{ID: imageID}, trunc: false, }, imageID, imageIDHeader, ctx.ID}, {imageContext{ - i: types.Image{Size: 10, VirtualSize: 10}, + i: types.ImageSummary{Size: 10, VirtualSize: 10}, trunc: true, }, "10 B", sizeHeader, ctx.Size}, {imageContext{ - i: types.Image{Created: unix}, + i: types.ImageSummary{Created: unix}, trunc: true, }, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt}, // FIXME // {imageContext{ - // i: types.Image{Created: unix}, + // i: types.ImageSummary{Created: unix}, // trunc: true, // }, units.HumanDuration(time.Unix(unix, 0)), createdSinceHeader, ctx.CreatedSince}, {imageContext{ - i: types.Image{}, + i: types.ImageSummary{}, repo: "busybox", }, "busybox", repositoryHeader, ctx.Repository}, {imageContext{ - i: types.Image{}, + i: types.ImageSummary{}, tag: "latest", }, "latest", tagHeader, ctx.Tag}, {imageContext{ - i: types.Image{}, + i: types.ImageSummary{}, digest: "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", }, "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", digestHeader, ctx.Digest}, } @@ -262,7 +262,7 @@ image_id: imageID3 } for _, testcase := range cases { - images := []types.Image{ + images := []types.ImageSummary{ {ID: "imageID1", RepoTags: []string{"image:tag1"}, RepoDigests: []string{"image@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf"}, Created: unixTime}, {ID: "imageID2", RepoTags: []string{"image:tag2"}, Created: unixTime}, {ID: "imageID3", RepoTags: []string{":"}, RepoDigests: []string{"@"}, Created: unixTime}, @@ -280,7 +280,7 @@ image_id: imageID3 func TestImageContextWriteWithNoImage(t *testing.T) { out := bytes.NewBufferString("") - images := []types.Image{} + images := []types.ImageSummary{} contexts := []struct { context ImageContext From ca3b5155d50ac8fb30ab0cb7c44a6ffd57cc6b10 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 3 Oct 2016 15:17:39 -0400 Subject: [PATCH 214/978] Generate api/types:Image from the swagger spec and rename it to a more appropriate name ImageSummary. Signed-off-by: Daniel Nephin Upstream-commit: a6a247fdf9b650c663f9ca58da28b6fd4f5dfc4d Component: cli --- components/cli/image_list.go | 4 ++-- components/cli/image_list_test.go | 2 +- components/cli/interface.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/components/cli/image_list.go b/components/cli/image_list.go index 00f27dc0c9..6ebb460541 100644 --- a/components/cli/image_list.go +++ b/components/cli/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/components/cli/image_list_test.go b/components/cli/image_list_test.go index 2a52279081..1ea6f1f05a 100644 --- a/components/cli/image_list_test.go +++ b/components/cli/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/components/cli/interface.go b/components/cli/interface.go index de06b848ae..4d450d8316 100644 --- a/components/cli/interface.go +++ b/components/cli/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 cfa1f345dc39c784d80bb57ff41cb091f16535c8 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Thu, 20 Oct 2016 12:04:01 -0700 Subject: [PATCH 215/978] Add force option to service update Currently, there's no way to restart the tasks of a service without making an actual change to the service. This leads to us giving awkward workarounds as in https://github.com/docker/docker.github.io/pull/178/files, where we tell people to scale a service up and down to restore balance, or make unnecessary changes to trigger a restart. This change adds a --force option to "docker service update", which forces the service to be updated even if no changes require that. Since rolling update parameters are respected, the user can use "docker service --force" to do a rolling restart. For example, the following is supported: docker service update --force --update-parallelism 2 \ --update-delay 5s myservice Since the default value of --update-parallelism is 1, the default behavior is to restart the service one task at a time. Signed-off-by: Aaron Lehmann Upstream-commit: dfed71a6ddf76afde84d3eb79e9c8cf3ab8635e3 Component: cli --- components/cli/command/service/update.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index 797c989271..6034979a66 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -37,6 +37,7 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.String("image", "", "Service image tag") flags.String("args", "", "Service command args") flags.Bool("rollback", false, "Rollback to previous specification") + flags.Bool("force", false, "Force update even if no changes require it") addServiceFlags(cmd, opts) flags.Var(newListOptsVar(), flagEnvRemove, "Remove an environment variable") @@ -257,6 +258,15 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error { return err } + force, err := flags.GetBool("force") + if err != nil { + return err + } + + if force { + spec.TaskTemplate.ForceUpdate++ + } + return nil } From a8ff9481eea268a78cd64cce80faec6b50cf46ea Mon Sep 17 00:00:00 2001 From: Amit Krishnan Date: Mon, 24 Oct 2016 15:06:33 -0700 Subject: [PATCH 216/978] Correct go-units import in cli/command/formatter/stats.go from src/github.com/docker/go-units -> github.com/docker/go-units Signed-off-by: Amit Krishnan Upstream-commit: 15bbb6171119aee288bd898d49f50d4ad560f692 Component: cli --- components/cli/command/formatter/stats.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/formatter/stats.go b/components/cli/command/formatter/stats.go index c7b30c9f39..212a1b4f5e 100644 --- a/components/cli/command/formatter/stats.go +++ b/components/cli/command/formatter/stats.go @@ -4,7 +4,7 @@ import ( "fmt" "sync" - units "src/github.com/docker/go-units" + units "github.com/docker/go-units" ) const ( From 2451915c27c8b995890aa847645c01e1a1fe2cd8 Mon Sep 17 00:00:00 2001 From: Kenfe-Mickael Laventure Date: Thu, 6 Oct 2016 07:09:54 -0700 Subject: [PATCH 217/978] Make experimental a runtime flag Signed-off-by: Kenfe-Mickael Laventure Upstream-commit: 66bd963b766062e328e38519a6c2279fc8299efa Component: cli --- .../cli/command/bundlefile/bundlefile.go | 2 - .../cli/command/bundlefile/bundlefile_test.go | 2 - components/cli/command/checkpoint/cmd.go | 20 ++++++++-- .../command/checkpoint/cmd_experimental.go | 30 -------------- components/cli/command/checkpoint/create.go | 2 - components/cli/command/checkpoint/list.go | 2 - components/cli/command/checkpoint/remove.go | 2 - components/cli/command/cli.go | 28 ++++++++++--- components/cli/command/commands/commands.go | 14 +++++-- components/cli/command/container/start.go | 4 +- .../cli/command/container/start_utils.go | 8 ---- .../container/start_utils_experimental.go | 9 ----- components/cli/command/plugin/cmd.go | 26 ++++++++++-- .../cli/command/plugin/cmd_experimental.go | 35 ---------------- components/cli/command/plugin/disable.go | 2 - components/cli/command/plugin/enable.go | 2 - components/cli/command/plugin/inspect.go | 2 - components/cli/command/plugin/install.go | 2 - components/cli/command/plugin/list.go | 2 - components/cli/command/plugin/push.go | 2 - components/cli/command/plugin/remove.go | 2 - components/cli/command/plugin/set.go | 2 - components/cli/command/stack/cmd.go | 32 ++++++++++++--- .../cli/command/stack/cmd_experimental.go | 40 ------------------- components/cli/command/stack/common.go | 2 - components/cli/command/stack/config.go | 2 - components/cli/command/stack/deploy.go | 2 - components/cli/command/stack/list.go | 2 - components/cli/command/stack/opts.go | 2 - components/cli/command/stack/ps.go | 2 - components/cli/command/stack/remove.go | 2 - components/cli/command/stack/services.go | 2 - components/cli/command/system/info.go | 2 +- components/cli/command/system/version.go | 23 +++++------ 34 files changed, 112 insertions(+), 201 deletions(-) delete mode 100644 components/cli/command/checkpoint/cmd_experimental.go delete mode 100644 components/cli/command/container/start_utils.go delete mode 100644 components/cli/command/container/start_utils_experimental.go delete mode 100644 components/cli/command/plugin/cmd_experimental.go delete mode 100644 components/cli/command/stack/cmd_experimental.go diff --git a/components/cli/command/bundlefile/bundlefile.go b/components/cli/command/bundlefile/bundlefile.go index 75c2d07433..7fd1e4f6c4 100644 --- a/components/cli/command/bundlefile/bundlefile.go +++ b/components/cli/command/bundlefile/bundlefile.go @@ -1,5 +1,3 @@ -// +build experimental - package bundlefile import ( diff --git a/components/cli/command/bundlefile/bundlefile_test.go b/components/cli/command/bundlefile/bundlefile_test.go index 1ff8235ff8..c343410df3 100644 --- a/components/cli/command/bundlefile/bundlefile_test.go +++ b/components/cli/command/bundlefile/bundlefile_test.go @@ -1,5 +1,3 @@ -// +build experimental - package bundlefile import ( diff --git a/components/cli/command/checkpoint/cmd.go b/components/cli/command/checkpoint/cmd.go index 7c3950bba8..84084ab716 100644 --- a/components/cli/command/checkpoint/cmd.go +++ b/components/cli/command/checkpoint/cmd.go @@ -1,13 +1,27 @@ -// +build !experimental - package checkpoint import ( + "fmt" + + "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/spf13/cobra" ) // NewCheckpointCommand returns the `checkpoint` subcommand (only in experimental) func NewCheckpointCommand(dockerCli *command.DockerCli) *cobra.Command { - return &cobra.Command{} + cmd := &cobra.Command{ + Use: "checkpoint", + Short: "Manage checkpoints", + Args: cli.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + }, + } + cmd.AddCommand( + newCreateCommand(dockerCli), + newListCommand(dockerCli), + newRemoveCommand(dockerCli), + ) + return cmd } diff --git a/components/cli/command/checkpoint/cmd_experimental.go b/components/cli/command/checkpoint/cmd_experimental.go deleted file mode 100644 index 3c89545778..0000000000 --- a/components/cli/command/checkpoint/cmd_experimental.go +++ /dev/null @@ -1,30 +0,0 @@ -// +build experimental - -package checkpoint - -import ( - "fmt" - - "github.com/spf13/cobra" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" -) - -// NewCheckpointCommand returns the `checkpoint` subcommand (only in experimental) -func NewCheckpointCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "checkpoint", - Short: "Manage checkpoints", - Args: cli.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) - }, - } - cmd.AddCommand( - newCreateCommand(dockerCli), - newListCommand(dockerCli), - newRemoveCommand(dockerCli), - ) - return cmd -} diff --git a/components/cli/command/checkpoint/create.go b/components/cli/command/checkpoint/create.go index f214574556..d369718119 100644 --- a/components/cli/command/checkpoint/create.go +++ b/components/cli/command/checkpoint/create.go @@ -1,5 +1,3 @@ -// +build experimental - package checkpoint import ( diff --git a/components/cli/command/checkpoint/list.go b/components/cli/command/checkpoint/list.go index 6d22531d45..7ba035890b 100644 --- a/components/cli/command/checkpoint/list.go +++ b/components/cli/command/checkpoint/list.go @@ -1,5 +1,3 @@ -// +build experimental - package checkpoint import ( diff --git a/components/cli/command/checkpoint/remove.go b/components/cli/command/checkpoint/remove.go index 6605c5e472..82ce62312b 100644 --- a/components/cli/command/checkpoint/remove.go +++ b/components/cli/command/checkpoint/remove.go @@ -1,5 +1,3 @@ -// +build experimental - package checkpoint import ( diff --git a/components/cli/command/cli.go b/components/cli/command/cli.go index 9ca28765cc..be82ecf6f3 100644 --- a/components/cli/command/cli.go +++ b/components/cli/command/cli.go @@ -19,6 +19,7 @@ import ( dopts "github.com/docker/docker/opts" "github.com/docker/go-connections/sockets" "github.com/docker/go-connections/tlsconfig" + "golang.org/x/net/context" ) // Streams is an interface which exposes the standard input and output streams @@ -31,12 +32,27 @@ type Streams interface { // DockerCli represents the docker command line client. // Instances of the client can be returned from NewDockerCli. type DockerCli struct { - configFile *configfile.ConfigFile - in *InStream - out *OutStream - err io.Writer - keyFile string - client client.APIClient + configFile *configfile.ConfigFile + in *InStream + out *OutStream + err io.Writer + keyFile string + client client.APIClient + hasExperimental *bool +} + +// HasExperimental returns true if experimental features are accessible +func (cli *DockerCli) HasExperimental() bool { + if cli.hasExperimental == nil { + if cli.client == nil { + cli.Initialize(cliflags.NewClientOptions()) + } + enabled := false + cli.hasExperimental = &enabled + enabled, _ = cli.client.Ping(context.Background()) + } + + return *cli.hasExperimental } // Client returns the APIClient diff --git a/components/cli/command/commands/commands.go b/components/cli/command/commands/commands.go index 6d0deb1d90..425f90ba7d 100644 --- a/components/cli/command/commands/commands.go +++ b/components/cli/command/commands/commands.go @@ -24,8 +24,6 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) { cmd.AddCommand( node.NewNodeCommand(dockerCli), service.NewServiceCommand(dockerCli), - stack.NewStackCommand(dockerCli), - stack.NewTopLevelDeployCommand(dockerCli), swarm.NewSwarmCommand(dockerCli), container.NewContainerCommand(dockerCli), image.NewImageCommand(dockerCli), @@ -72,9 +70,17 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) { hide(image.NewSaveCommand(dockerCli)), hide(image.NewTagCommand(dockerCli)), hide(system.NewInspectCommand(dockerCli)), - checkpoint.NewCheckpointCommand(dockerCli), - plugin.NewPluginCommand(dockerCli), ) + + if dockerCli.HasExperimental() { + cmd.AddCommand( + stack.NewStackCommand(dockerCli), + stack.NewTopLevelDeployCommand(dockerCli), + checkpoint.NewCheckpointCommand(dockerCli), + plugin.NewPluginCommand(dockerCli), + ) + } + } func hide(cmd *cobra.Command) *cobra.Command { diff --git a/components/cli/command/container/start.go b/components/cli/command/container/start.go index 4c31f9bf97..8693b3a550 100644 --- a/components/cli/command/container/start.go +++ b/components/cli/command/container/start.go @@ -44,7 +44,9 @@ func NewStartCommand(dockerCli *command.DockerCli) *cobra.Command { flags.BoolVarP(&opts.openStdin, "interactive", "i", false, "Attach container's STDIN") flags.StringVar(&opts.detachKeys, "detach-keys", "", "Override the key sequence for detaching a container") - addExperimentalStartFlags(flags, &opts) + if dockerCli.HasExperimental() { + flags.StringVar(&opts.checkpoint, "checkpoint", "", "Restore from this checkpoint") + } return cmd } diff --git a/components/cli/command/container/start_utils.go b/components/cli/command/container/start_utils.go deleted file mode 100644 index 689d742f06..0000000000 --- a/components/cli/command/container/start_utils.go +++ /dev/null @@ -1,8 +0,0 @@ -// +build !experimental - -package container - -import "github.com/spf13/pflag" - -func addExperimentalStartFlags(flags *pflag.FlagSet, opts *startOptions) { -} diff --git a/components/cli/command/container/start_utils_experimental.go b/components/cli/command/container/start_utils_experimental.go deleted file mode 100644 index 43c64f431c..0000000000 --- a/components/cli/command/container/start_utils_experimental.go +++ /dev/null @@ -1,9 +0,0 @@ -// +build experimental - -package container - -import "github.com/spf13/pflag" - -func addExperimentalStartFlags(flags *pflag.FlagSet, opts *startOptions) { - flags.StringVar(&opts.checkpoint, "checkpoint", "", "Restore from this checkpoint") -} diff --git a/components/cli/command/plugin/cmd.go b/components/cli/command/plugin/cmd.go index 10074218dd..80fa61cb1c 100644 --- a/components/cli/command/plugin/cmd.go +++ b/components/cli/command/plugin/cmd.go @@ -1,13 +1,33 @@ -// +build !experimental - package plugin import ( + "fmt" + + "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/spf13/cobra" ) // NewPluginCommand returns a cobra command for `plugin` subcommands func NewPluginCommand(dockerCli *command.DockerCli) *cobra.Command { - return &cobra.Command{} + cmd := &cobra.Command{ + Use: "plugin", + Short: "Manage plugins", + Args: cli.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + }, + } + + cmd.AddCommand( + newDisableCommand(dockerCli), + newEnableCommand(dockerCli), + newInspectCommand(dockerCli), + newInstallCommand(dockerCli), + newListCommand(dockerCli), + newRemoveCommand(dockerCli), + newSetCommand(dockerCli), + newPushCommand(dockerCli), + ) + return cmd } diff --git a/components/cli/command/plugin/cmd_experimental.go b/components/cli/command/plugin/cmd_experimental.go deleted file mode 100644 index 8bb3416097..0000000000 --- a/components/cli/command/plugin/cmd_experimental.go +++ /dev/null @@ -1,35 +0,0 @@ -// +build experimental - -package plugin - -import ( - "fmt" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" -) - -// NewPluginCommand returns a cobra command for `plugin` subcommands -func NewPluginCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "plugin", - Short: "Manage plugins", - Args: cli.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) - }, - } - - cmd.AddCommand( - newDisableCommand(dockerCli), - newEnableCommand(dockerCli), - newInspectCommand(dockerCli), - newInstallCommand(dockerCli), - newListCommand(dockerCli), - newRemoveCommand(dockerCli), - newSetCommand(dockerCli), - newPushCommand(dockerCli), - ) - return cmd -} diff --git a/components/cli/command/plugin/disable.go b/components/cli/command/plugin/disable.go index 3b5c69a018..9089a3cf68 100644 --- a/components/cli/command/plugin/disable.go +++ b/components/cli/command/plugin/disable.go @@ -1,5 +1,3 @@ -// +build experimental - package plugin import ( diff --git a/components/cli/command/plugin/enable.go b/components/cli/command/plugin/enable.go index cfc3580f43..0fd8f469dd 100644 --- a/components/cli/command/plugin/enable.go +++ b/components/cli/command/plugin/enable.go @@ -1,5 +1,3 @@ -// +build experimental - package plugin import ( diff --git a/components/cli/command/plugin/inspect.go b/components/cli/command/plugin/inspect.go index e5059629e5..13c7fa72d8 100644 --- a/components/cli/command/plugin/inspect.go +++ b/components/cli/command/plugin/inspect.go @@ -1,5 +1,3 @@ -// +build experimental - package plugin import ( diff --git a/components/cli/command/plugin/install.go b/components/cli/command/plugin/install.go index e90e8d1224..3989a35ce6 100644 --- a/components/cli/command/plugin/install.go +++ b/components/cli/command/plugin/install.go @@ -1,5 +1,3 @@ -// +build experimental - package plugin import ( diff --git a/components/cli/command/plugin/list.go b/components/cli/command/plugin/list.go index b8f5e5e08a..9d4b46d120 100644 --- a/components/cli/command/plugin/list.go +++ b/components/cli/command/plugin/list.go @@ -1,5 +1,3 @@ -// +build experimental - package plugin import ( diff --git a/components/cli/command/plugin/push.go b/components/cli/command/plugin/push.go index 360830902e..4e176bea3e 100644 --- a/components/cli/command/plugin/push.go +++ b/components/cli/command/plugin/push.go @@ -1,5 +1,3 @@ -// +build experimental - package plugin import ( diff --git a/components/cli/command/plugin/remove.go b/components/cli/command/plugin/remove.go index 4222690a4f..7a51dce06d 100644 --- a/components/cli/command/plugin/remove.go +++ b/components/cli/command/plugin/remove.go @@ -1,5 +1,3 @@ -// +build experimental - package plugin import ( diff --git a/components/cli/command/plugin/set.go b/components/cli/command/plugin/set.go index f2d3b082c6..e58ea63bc0 100644 --- a/components/cli/command/plugin/set.go +++ b/components/cli/command/plugin/set.go @@ -1,5 +1,3 @@ -// +build experimental - package plugin import ( diff --git a/components/cli/command/stack/cmd.go b/components/cli/command/stack/cmd.go index 51cb2d1bcf..49fcedf209 100644 --- a/components/cli/command/stack/cmd.go +++ b/components/cli/command/stack/cmd.go @@ -1,18 +1,38 @@ -// +build !experimental - package stack import ( + "fmt" + + "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/spf13/cobra" ) -// NewStackCommand returns no command +// NewStackCommand returns a cobra command for `stack` subcommands func NewStackCommand(dockerCli *command.DockerCli) *cobra.Command { - return &cobra.Command{} + cmd := &cobra.Command{ + Use: "stack", + Short: "Manage Docker stacks", + Args: cli.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + }, + } + cmd.AddCommand( + newConfigCommand(dockerCli), + newDeployCommand(dockerCli), + newListCommand(dockerCli), + newRemoveCommand(dockerCli), + newServicesCommand(dockerCli), + newPsCommand(dockerCli), + ) + return cmd } -// NewTopLevelDeployCommand returns no command +// NewTopLevelDeployCommand returns a command for `docker deploy` func NewTopLevelDeployCommand(dockerCli *command.DockerCli) *cobra.Command { - return &cobra.Command{} + cmd := newDeployCommand(dockerCli) + // Remove the aliases at the top level + cmd.Aliases = []string{} + return cmd } diff --git a/components/cli/command/stack/cmd_experimental.go b/components/cli/command/stack/cmd_experimental.go deleted file mode 100644 index b32d925330..0000000000 --- a/components/cli/command/stack/cmd_experimental.go +++ /dev/null @@ -1,40 +0,0 @@ -// +build experimental - -package stack - -import ( - "fmt" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/spf13/cobra" -) - -// NewStackCommand returns a cobra command for `stack` subcommands -func NewStackCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := &cobra.Command{ - Use: "stack", - Short: "Manage Docker stacks", - Args: cli.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) - }, - } - cmd.AddCommand( - newConfigCommand(dockerCli), - newDeployCommand(dockerCli), - newListCommand(dockerCli), - newRemoveCommand(dockerCli), - newServicesCommand(dockerCli), - newPsCommand(dockerCli), - ) - return cmd -} - -// NewTopLevelDeployCommand returns a command for `docker deploy` -func NewTopLevelDeployCommand(dockerCli *command.DockerCli) *cobra.Command { - cmd := newDeployCommand(dockerCli) - // Remove the aliases at the top level - cmd.Aliases = []string{} - return cmd -} diff --git a/components/cli/command/stack/common.go b/components/cli/command/stack/common.go index 2afdb5147d..3e3a35faac 100644 --- a/components/cli/command/stack/common.go +++ b/components/cli/command/stack/common.go @@ -1,5 +1,3 @@ -// +build experimental - package stack import ( diff --git a/components/cli/command/stack/config.go b/components/cli/command/stack/config.go index bdcf7d4835..56e554a86e 100644 --- a/components/cli/command/stack/config.go +++ b/components/cli/command/stack/config.go @@ -1,5 +1,3 @@ -// +build experimental - package stack import ( diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index bf31dd7753..fcf55fb7d2 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -1,5 +1,3 @@ -// +build experimental - package stack import ( diff --git a/components/cli/command/stack/list.go b/components/cli/command/stack/list.go index 9fe626d96d..5d87cecb5f 100644 --- a/components/cli/command/stack/list.go +++ b/components/cli/command/stack/list.go @@ -1,5 +1,3 @@ -// +build experimental - package stack import ( diff --git a/components/cli/command/stack/opts.go b/components/cli/command/stack/opts.go index eef4d0e45b..5f2d8b5d0a 100644 --- a/components/cli/command/stack/opts.go +++ b/components/cli/command/stack/opts.go @@ -1,5 +1,3 @@ -// +build experimental - package stack import ( diff --git a/components/cli/command/stack/ps.go b/components/cli/command/stack/ps.go index c4683b68a0..2fff3de1fa 100644 --- a/components/cli/command/stack/ps.go +++ b/components/cli/command/stack/ps.go @@ -1,5 +1,3 @@ -// +build experimental - package stack import ( diff --git a/components/cli/command/stack/remove.go b/components/cli/command/stack/remove.go index 6ab005d71d..8137903d47 100644 --- a/components/cli/command/stack/remove.go +++ b/components/cli/command/stack/remove.go @@ -1,5 +1,3 @@ -// +build experimental - package stack import ( diff --git a/components/cli/command/stack/services.go b/components/cli/command/stack/services.go index 60f52c30c7..50b50179de 100644 --- a/components/cli/command/stack/services.go +++ b/components/cli/command/stack/services.go @@ -1,5 +1,3 @@ -// +build experimental - package stack import ( diff --git a/components/cli/command/system/info.go b/components/cli/command/system/info.go index fb5ea17d7b..0f94373d35 100644 --- a/components/cli/command/system/info.go +++ b/components/cli/command/system/info.go @@ -225,7 +225,7 @@ func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error { } } - ioutils.FprintfIfTrue(dockerCli.Out(), "Experimental: %v\n", info.ExperimentalBuild) + fmt.Fprintf(dockerCli.Out(), "Experimental: %v\n", info.ExperimentalBuild) if info.ClusterStore != "" { fmt.Fprintf(dockerCli.Out(), "Cluster Store: %s\n", info.ClusterStore) } diff --git a/components/cli/command/system/version.go b/components/cli/command/system/version.go index 7959bf564f..0b484cb3b9 100644 --- a/components/cli/command/system/version.go +++ b/components/cli/command/system/version.go @@ -10,7 +10,6 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/dockerversion" - "github.com/docker/docker/utils" "github.com/docker/docker/utils/templates" "github.com/spf13/cobra" ) @@ -21,8 +20,7 @@ var versionTemplate = `Client: Go version: {{.Client.GoVersion}} Git commit: {{.Client.GitCommit}} Built: {{.Client.BuildTime}} - OS/Arch: {{.Client.Os}}/{{.Client.Arch}}{{if .Client.Experimental}} - Experimental: {{.Client.Experimental}}{{end}}{{if .ServerOK}} + OS/Arch: {{.Client.Os}}/{{.Client.Arch}}{{if .ServerOK}} Server: Version: {{.Server.Version}} @@ -30,8 +28,8 @@ Server: Go version: {{.Server.GoVersion}} Git commit: {{.Server.GitCommit}} Built: {{.Server.BuildTime}} - OS/Arch: {{.Server.Os}}/{{.Server.Arch}}{{if .Server.Experimental}} - Experimental: {{.Server.Experimental}}{{end}}{{end}}` + OS/Arch: {{.Server.Os}}/{{.Server.Arch}} + Experimental: {{.Server.Experimental}}{{end}}` type versionOptions struct { format string @@ -73,14 +71,13 @@ func runVersion(dockerCli *command.DockerCli, opts *versionOptions) error { vd := types.VersionResponse{ Client: &types.Version{ - Version: dockerversion.Version, - APIVersion: dockerCli.Client().ClientVersion(), - GoVersion: runtime.Version(), - GitCommit: dockerversion.GitCommit, - BuildTime: dockerversion.BuildTime, - Os: runtime.GOOS, - Arch: runtime.GOARCH, - Experimental: utils.ExperimentalBuild(), + Version: dockerversion.Version, + APIVersion: dockerCli.Client().ClientVersion(), + GoVersion: runtime.Version(), + GitCommit: dockerversion.GitCommit, + BuildTime: dockerversion.BuildTime, + Os: runtime.GOOS, + Arch: runtime.GOARCH, }, } From c2195d43eb6f7cd30503aa96c549bb137dd89de0 Mon Sep 17 00:00:00 2001 From: Kenfe-Mickael Laventure Date: Thu, 6 Oct 2016 07:09:54 -0700 Subject: [PATCH 218/978] Make experimental a runtime flag Signed-off-by: Kenfe-Mickael Laventure Upstream-commit: 31f5d9b5437ee8dcede311cdc1509cae94c5820b Component: cli --- components/cli/interface.go | 1 + components/cli/interface_experimental.go | 9 +-------- components/cli/interface_stable.go | 3 +-- components/cli/ping.go | 19 +++++++++++++++++++ components/cli/plugin_disable.go | 2 -- components/cli/plugin_disable_test.go | 2 -- components/cli/plugin_enable.go | 2 -- components/cli/plugin_enable_test.go | 2 -- components/cli/plugin_inspect.go | 2 -- components/cli/plugin_inspect_test.go | 2 -- components/cli/plugin_install.go | 2 -- components/cli/plugin_list.go | 2 -- components/cli/plugin_list_test.go | 2 -- components/cli/plugin_push.go | 2 -- components/cli/plugin_push_test.go | 2 -- components/cli/plugin_remove.go | 2 -- components/cli/plugin_remove_test.go | 2 -- components/cli/plugin_set.go | 2 -- components/cli/plugin_set_test.go | 2 -- 19 files changed, 22 insertions(+), 40 deletions(-) create mode 100644 components/cli/ping.go diff --git a/components/cli/interface.go b/components/cli/interface.go index 4d450d8316..f919612163 100644 --- a/components/cli/interface.go +++ b/components/cli/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/components/cli/interface_experimental.go b/components/cli/interface_experimental.go index 1ddc517c9a..ddb9f79b5a 100644 --- a/components/cli/interface_experimental.go +++ b/components/cli/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/components/cli/interface_stable.go b/components/cli/interface_stable.go index 496f522d51..cc90a3cbb9 100644 --- a/components/cli/interface_stable.go +++ b/components/cli/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/components/cli/ping.go b/components/cli/ping.go new file mode 100644 index 0000000000..5e99e1bba1 --- /dev/null +++ b/components/cli/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/components/cli/plugin_disable.go b/components/cli/plugin_disable.go index 893fc6e823..51e4565125 100644 --- a/components/cli/plugin_disable.go +++ b/components/cli/plugin_disable.go @@ -1,5 +1,3 @@ -// +build experimental - package client import ( diff --git a/components/cli/plugin_disable_test.go b/components/cli/plugin_disable_test.go index 7b50b25730..2818008ab9 100644 --- a/components/cli/plugin_disable_test.go +++ b/components/cli/plugin_disable_test.go @@ -1,5 +1,3 @@ -// +build experimental - package client import ( diff --git a/components/cli/plugin_enable.go b/components/cli/plugin_enable.go index 84422abc79..8109814ddb 100644 --- a/components/cli/plugin_enable.go +++ b/components/cli/plugin_enable.go @@ -1,5 +1,3 @@ -// +build experimental - package client import ( diff --git a/components/cli/plugin_enable_test.go b/components/cli/plugin_enable_test.go index a2b57be4c2..d919914e75 100644 --- a/components/cli/plugin_enable_test.go +++ b/components/cli/plugin_enable_test.go @@ -1,5 +1,3 @@ -// +build experimental - package client import ( diff --git a/components/cli/plugin_inspect.go b/components/cli/plugin_inspect.go index 7ba8db57a8..e9474b5a98 100644 --- a/components/cli/plugin_inspect.go +++ b/components/cli/plugin_inspect.go @@ -1,5 +1,3 @@ -// +build experimental - package client import ( diff --git a/components/cli/plugin_inspect_test.go b/components/cli/plugin_inspect_test.go index df4ca9c841..fae407eb9b 100644 --- a/components/cli/plugin_inspect_test.go +++ b/components/cli/plugin_inspect_test.go @@ -1,5 +1,3 @@ -// +build experimental - package client import ( diff --git a/components/cli/plugin_install.go b/components/cli/plugin_install.go index 9ee32eea92..636c95364d 100644 --- a/components/cli/plugin_install.go +++ b/components/cli/plugin_install.go @@ -1,5 +1,3 @@ -// +build experimental - package client import ( diff --git a/components/cli/plugin_list.go b/components/cli/plugin_list.go index 48b470247b..88c480a3e1 100644 --- a/components/cli/plugin_list.go +++ b/components/cli/plugin_list.go @@ -1,5 +1,3 @@ -// +build experimental - package client import ( diff --git a/components/cli/plugin_list_test.go b/components/cli/plugin_list_test.go index 95c51595ca..173e4b87f5 100644 --- a/components/cli/plugin_list_test.go +++ b/components/cli/plugin_list_test.go @@ -1,5 +1,3 @@ -// +build experimental - package client import ( diff --git a/components/cli/plugin_push.go b/components/cli/plugin_push.go index 3afea5ed79..d83bbdc358 100644 --- a/components/cli/plugin_push.go +++ b/components/cli/plugin_push.go @@ -1,5 +1,3 @@ -// +build experimental - package client import ( diff --git a/components/cli/plugin_push_test.go b/components/cli/plugin_push_test.go index ed685694ec..efdbdc6db1 100644 --- a/components/cli/plugin_push_test.go +++ b/components/cli/plugin_push_test.go @@ -1,5 +1,3 @@ -// +build experimental - package client import ( diff --git a/components/cli/plugin_remove.go b/components/cli/plugin_remove.go index 1483f2854d..b017e4d348 100644 --- a/components/cli/plugin_remove.go +++ b/components/cli/plugin_remove.go @@ -1,5 +1,3 @@ -// +build experimental - package client import ( diff --git a/components/cli/plugin_remove_test.go b/components/cli/plugin_remove_test.go index fc789fd04d..a15f1661f6 100644 --- a/components/cli/plugin_remove_test.go +++ b/components/cli/plugin_remove_test.go @@ -1,5 +1,3 @@ -// +build experimental - package client import ( diff --git a/components/cli/plugin_set.go b/components/cli/plugin_set.go index fb40f38b22..3260d2a90d 100644 --- a/components/cli/plugin_set.go +++ b/components/cli/plugin_set.go @@ -1,5 +1,3 @@ -// +build experimental - package client import ( diff --git a/components/cli/plugin_set_test.go b/components/cli/plugin_set_test.go index fa1cde044e..2450254463 100644 --- a/components/cli/plugin_set_test.go +++ b/components/cli/plugin_set_test.go @@ -1,5 +1,3 @@ -// +build experimental - package client import ( From 2ff5e5a0f89cc63c55c7d84880cbdb441ab1d8d1 Mon Sep 17 00:00:00 2001 From: Kenfe-Mickael Laventure Date: Thu, 6 Oct 2016 07:09:54 -0700 Subject: [PATCH 219/978] Make experimental a runtime flag Signed-off-by: Kenfe-Mickael Laventure Upstream-commit: 773a7f6cb8be730f37a486705075b1ad214c3d73 Component: cli --- components/cli/docker.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/components/cli/docker.go b/components/cli/docker.go index d412a38b28..e01e4ba5c7 100644 --- a/components/cli/docker.go +++ b/components/cli/docker.go @@ -91,11 +91,7 @@ func main() { } 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) - } + fmt.Printf("Docker version %s, build %s\n", dockerversion.Version, dockerversion.GitCommit) } func dockerPreRun(opts *cliflags.ClientOptions) { From 2cd222342322e50cc41e97bb01ee58ae79316aca Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Tue, 18 Oct 2016 04:36:52 +0000 Subject: [PATCH 220/978] 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 Upstream-commit: 171e533ba278de015984e88fa05effd11a5070b1 Component: cli --- components/cli/interface.go | 1 + components/cli/network_prune.go | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 components/cli/network_prune.go diff --git a/components/cli/interface.go b/components/cli/interface.go index f919612163..8abdb0f6fc 100644 --- a/components/cli/interface.go +++ b/components/cli/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/components/cli/network_prune.go b/components/cli/network_prune.go new file mode 100644 index 0000000000..01185f2e02 --- /dev/null +++ b/components/cli/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 c4d3e691e176f663beff98ddfb7b39054c3c537c Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Tue, 18 Oct 2016 04:36:52 +0000 Subject: [PATCH 221/978] 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 Upstream-commit: d5d520f0d739ae34355eb0483e5dac39ad8f8cea Component: cli --- components/cli/command/network/cmd.go | 1 + components/cli/command/network/prune.go | 72 +++++++++++++++++++++++++ components/cli/command/prune/prune.go | 11 ++++ components/cli/command/system/prune.go | 6 ++- 4 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 components/cli/command/network/prune.go diff --git a/components/cli/command/network/cmd.go b/components/cli/command/network/cmd.go index b33f98cd30..77c8e4908e 100644 --- a/components/cli/command/network/cmd.go +++ b/components/cli/command/network/cmd.go @@ -26,6 +26,7 @@ func NewNetworkCommand(dockerCli *command.DockerCli) *cobra.Command { newInspectCommand(dockerCli), newListCommand(dockerCli), newRemoveCommand(dockerCli), + NewPruneCommand(dockerCli), ) return cmd } diff --git a/components/cli/command/network/prune.go b/components/cli/command/network/prune.go new file mode 100644 index 0000000000..00e05d3bdf --- /dev/null +++ b/components/cli/command/network/prune.go @@ -0,0 +1,72 @@ +package network + +import ( + "fmt" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +type pruneOptions struct { + force bool +} + +// NewPruneCommand returns a new cobra prune command for networks +func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts pruneOptions + + cmd := &cobra.Command{ + Use: "prune [OPTIONS]", + Short: "Remove all unused networks", + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + output, err := runPrune(dockerCli, opts) + if err != nil { + return err + } + if output != "" { + fmt.Fprintln(dockerCli.Out(), output) + } + return nil + }, + } + + flags := cmd.Flags() + flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation") + + return cmd +} + +const warning = `WARNING! This will remove all networks not used by at least one container. +Are you sure you want to continue?` + +func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (output string, err error) { + if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) { + return + } + + report, err := dockerCli.Client().NetworksPrune(context.Background(), types.NetworksPruneConfig{}) + if err != nil { + return + } + + if len(report.NetworksDeleted) > 0 { + output = "Deleted Networks:\n" + for _, id := range report.NetworksDeleted { + output += id + "\n" + } + } + + return +} + +// RunPrune calls the Network Prune API +// This returns the amount of space reclaimed and a detailed output string +func RunPrune(dockerCli *command.DockerCli) (uint64, string, error) { + output, err := runPrune(dockerCli, pruneOptions{force: true}) + return 0, output, err +} diff --git a/components/cli/command/prune/prune.go b/components/cli/command/prune/prune.go index fd04c590b6..a022487fd6 100644 --- a/components/cli/command/prune/prune.go +++ b/components/cli/command/prune/prune.go @@ -4,6 +4,7 @@ import ( "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/container" "github.com/docker/docker/cli/command/image" + "github.com/docker/docker/cli/command/network" "github.com/docker/docker/cli/command/volume" "github.com/spf13/cobra" ) @@ -23,6 +24,11 @@ func NewImagePruneCommand(dockerCli *command.DockerCli) *cobra.Command { return image.NewPruneCommand(dockerCli) } +// NewNetworkPruneCommand returns a cobra prune command for Networks +func NewNetworkPruneCommand(dockerCli *command.DockerCli) *cobra.Command { + return network.NewPruneCommand(dockerCli) +} + // RunContainerPrune executes a prune command for containers func RunContainerPrune(dockerCli *command.DockerCli) (uint64, string, error) { return container.RunPrune(dockerCli) @@ -37,3 +43,8 @@ func RunVolumePrune(dockerCli *command.DockerCli) (uint64, string, error) { func RunImagePrune(dockerCli *command.DockerCli, all bool) (uint64, string, error) { return image.RunPrune(dockerCli, all) } + +// RunNetworkPrune executes a prune command for networks +func RunNetworkPrune(dockerCli *command.DockerCli) (uint64, string, error) { + return network.RunPrune(dockerCli) +} diff --git a/components/cli/command/system/prune.go b/components/cli/command/system/prune.go index ea8a41380f..c79bc6910e 100644 --- a/components/cli/command/system/prune.go +++ b/components/cli/command/system/prune.go @@ -39,6 +39,7 @@ const ( warning = `WARNING! This will remove: - all stopped containers - all volumes not used by at least one container + - all networks not used by at least one container %s Are you sure you want to continue?` @@ -64,13 +65,14 @@ func runPrune(dockerCli *command.DockerCli, opts pruneOptions) error { for _, pruneFn := range []func(dockerCli *command.DockerCli) (uint64, string, error){ prune.RunContainerPrune, prune.RunVolumePrune, + prune.RunNetworkPrune, } { spc, output, err := pruneFn(dockerCli) if err != nil { return err } - if spc > 0 { - spaceReclaimed += spc + spaceReclaimed += spc + if output != "" { fmt.Fprintln(dockerCli.Out(), output) } } From 1ba89e2590698184a9e3781e37c38941e5d5cda6 Mon Sep 17 00:00:00 2001 From: sandyskies Date: Sun, 6 Mar 2016 20:29:23 +0800 Subject: [PATCH 222/978] add --network option for docker build Signed-off-by: sandyskies Signed-off-by: Tonis Tiigi Upstream-commit: 01832cc0ab5741ba4caeeeb5666c302f235fb101 Component: cli --- components/cli/image_build.go | 1 + 1 file changed, 1 insertion(+) diff --git a/components/cli/image_build.go b/components/cli/image_build.go index 3abd87025e..4d611d5430 100644 --- a/components/cli/image_build.go +++ b/components/cli/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 8446b1acdfbe1ebacb13b1ac32867429e694d600 Mon Sep 17 00:00:00 2001 From: sandyskies Date: Sun, 6 Mar 2016 20:29:23 +0800 Subject: [PATCH 223/978] add --network option for docker build Signed-off-by: sandyskies Signed-off-by: Tonis Tiigi Upstream-commit: 3c9dff2f758f7f18f0af4f327b7407f9923af53b Component: cli --- components/cli/command/image/build.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go index 19fd4aa709..7db76a649f 100644 --- a/components/cli/command/image/build.go +++ b/components/cli/command/image/build.go @@ -58,6 +58,7 @@ type buildOptions struct { cacheFrom []string compress bool securityOpt []string + networkMode string } // NewBuildCommand creates a new `docker build` command @@ -105,6 +106,7 @@ func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command { flags.StringSliceVar(&options.cacheFrom, "cache-from", []string{}, "Images to consider as cache sources") flags.BoolVar(&options.compress, "compress", false, "Compress the build context using gzip") flags.StringSliceVar(&options.securityOpt, "security-opt", []string{}, "Security options") + flags.StringVar(&options.networkMode, "network", "default", "Connect a container to a network") command.AddTrustedFlags(flags, true) @@ -302,6 +304,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()), CacheFrom: options.cacheFrom, SecurityOpt: options.securityOpt, + NetworkMode: options.networkMode, } response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions) From 84e8a755f4a78f8fc259516bbe7376f9d9c78f15 Mon Sep 17 00:00:00 2001 From: John Howard Date: Tue, 25 Oct 2016 16:19:14 -0700 Subject: [PATCH 224/978] Windows: Fix stats CLI Signed-off-by: John Howard Upstream-commit: dff7842790555b7862f1bc32d2f932f8a36a18c4 Component: cli --- components/cli/command/container/stats.go | 11 +++++++- components/cli/command/formatter/stats.go | 33 ++++++++++++----------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/components/cli/command/container/stats.go b/components/cli/command/container/stats.go index 4d35e744c2..e7954e4b98 100644 --- a/components/cli/command/container/stats.go +++ b/components/cli/command/container/stats.go @@ -80,6 +80,16 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error { } } + // Get the daemonOSType if not set already + if daemonOSType == "" { + svctx := context.Background() + sv, err := dockerCli.Client().ServerVersion(svctx) + if err != nil { + return err + } + daemonOSType = sv.Os + } + // waitFirst is a WaitGroup to wait first stat data's reach for each container waitFirst := &sync.WaitGroup{} @@ -184,7 +194,6 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error { Output: dockerCli.Out(), Format: formatter.NewStatsFormat(f, daemonOSType), } - cleanScreen := func() { if !opts.noStream { fmt.Fprint(dockerCli.Out(), "\033[2J") diff --git a/components/cli/command/formatter/stats.go b/components/cli/command/formatter/stats.go index 212a1b4f5e..cc2588c392 100644 --- a/components/cli/command/formatter/stats.go +++ b/components/cli/command/formatter/stats.go @@ -10,17 +10,16 @@ import ( const ( winOSType = "windows" defaultStatsTableFormat = "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}\t{{.BlockIO}}\t{{.PIDs}}" - winDefaultStatsTableFormat = "table {{.Container}}\t{{.CPUPerc}}\t{{{.MemUsage}}\t{.NetIO}}\t{{.BlockIO}}" - emptyStatsTableFormat = "Waiting for statistics..." + winDefaultStatsTableFormat = "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}\t{{.BlockIO}}" - containerHeader = "CONTAINER" - cpuPercHeader = "CPU %" - netIOHeader = "NET I/O" - blockIOHeader = "BLOCK I/O" - winMemPercHeader = "PRIV WORKING SET" // Used only on Window - memPercHeader = "MEM %" // Used only on Linux - memUseHeader = "MEM USAGE / LIMIT" // Used only on Linux - pidsHeader = "PIDS" // Used only on Linux + containerHeader = "CONTAINER" + cpuPercHeader = "CPU %" + netIOHeader = "NET I/O" + blockIOHeader = "BLOCK I/O" + memPercHeader = "MEM %" // Used only on Linux + winMemUseHeader = "PRIV WORKING SET" // Used only on Windows + memUseHeader = "MEM USAGE / LIMIT" // Used only on Linux + pidsHeader = "PIDS" // Used only on Linux ) // StatsEntry represents represents the statistics data collected from a container @@ -151,18 +150,22 @@ func (c *containerStatsContext) CPUPerc() string { } func (c *containerStatsContext) MemUsage() string { - c.AddHeader(memUseHeader) - if c.s.IsInvalid || c.s.OSType == winOSType { + header := memUseHeader + if c.s.OSType == winOSType { + header = winMemUseHeader + } + c.AddHeader(header) + if c.s.IsInvalid { return fmt.Sprintf("-- / --") } + if c.s.OSType == winOSType { + return fmt.Sprintf("%s", units.BytesSize(c.s.Memory)) + } return fmt.Sprintf("%s / %s", units.BytesSize(c.s.Memory), units.BytesSize(c.s.MemoryLimit)) } func (c *containerStatsContext) MemPerc() string { header := memPercHeader - if c.s.OSType == winOSType { - header = winMemPercHeader - } c.AddHeader(header) if c.s.IsInvalid { return fmt.Sprintf("--") From 46742b9fba439a69977ca13467c7805e65ee110e Mon Sep 17 00:00:00 2001 From: "Erik St. Martin" Date: Tue, 7 Jun 2016 15:05:43 -0400 Subject: [PATCH 225/978] Implementing support for --cpu-rt-period and --cpu-rt-runtime so that containers may specify these cgroup values at runtime. This will allow processes to change their priority to real-time within the container when CONFIG_RT_GROUP_SCHED is enabled in the kernel. See #22380. Also added sanity checks for the new --cpu-rt-runtime and --cpu-rt-period flags to ensure that that the kernel supports these features and that runtime is not greater than period. Daemon will support a --cpu-rt-runtime flag to initialize the parent cgroup on startup, this prevents the administrator from alotting runtime to docker after each restart. There are additional checks that could be added but maybe too far? Check parent cgroups to ensure values are <= parent, inspecting rtprio ulimit and issuing a warning. Signed-off-by: Erik St. Martin Upstream-commit: 4f320d7c2a9fde514ae1ef4349087058a8b2dc83 Component: cli --- components/cli/command/container/update.go | 48 ++++++++++++---------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/components/cli/command/container/update.go b/components/cli/command/container/update.go index b5770c8997..5bacc9be75 100644 --- a/components/cli/command/container/update.go +++ b/components/cli/command/container/update.go @@ -15,17 +15,19 @@ import ( ) type updateOptions struct { - blkioWeight uint16 - cpuPeriod int64 - cpuQuota int64 - cpusetCpus string - cpusetMems string - cpuShares int64 - memoryString string - memoryReservation string - memorySwap string - kernelMemory string - restartPolicy string + blkioWeight uint16 + cpuPeriod int64 + cpuQuota int64 + cpuRealtimePeriod int64 + cpuRealtimeRuntime int64 + cpusetCpus string + cpusetMems string + cpuShares int64 + memoryString string + memoryReservation string + memorySwap string + kernelMemory string + restartPolicy string nFlag int @@ -51,6 +53,8 @@ func NewUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Uint16Var(&opts.blkioWeight, "blkio-weight", 0, "Block IO (relative weight), between 10 and 1000") flags.Int64Var(&opts.cpuPeriod, "cpu-period", 0, "Limit CPU CFS (Completely Fair Scheduler) period") flags.Int64Var(&opts.cpuQuota, "cpu-quota", 0, "Limit CPU CFS (Completely Fair Scheduler) quota") + flags.Int64Var(&opts.cpuRealtimePeriod, "cpu-rt-period", 0, "Limit the CPU real-time period in microseconds") + flags.Int64Var(&opts.cpuRealtimeRuntime, "cpu-rt-runtime", 0, "Limit the CPU real-time runtime in microseconds") flags.StringVar(&opts.cpusetCpus, "cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)") flags.StringVar(&opts.cpusetMems, "cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)") flags.Int64VarP(&opts.cpuShares, "cpu-shares", "c", 0, "CPU shares (relative weight)") @@ -115,16 +119,18 @@ func runUpdate(dockerCli *command.DockerCli, opts *updateOptions) error { } resources := containertypes.Resources{ - BlkioWeight: opts.blkioWeight, - CpusetCpus: opts.cpusetCpus, - CpusetMems: opts.cpusetMems, - CPUShares: opts.cpuShares, - Memory: memory, - MemoryReservation: memoryReservation, - MemorySwap: memorySwap, - KernelMemory: kernelMemory, - CPUPeriod: opts.cpuPeriod, - CPUQuota: opts.cpuQuota, + BlkioWeight: opts.blkioWeight, + CpusetCpus: opts.cpusetCpus, + CpusetMems: opts.cpusetMems, + CPUShares: opts.cpuShares, + Memory: memory, + MemoryReservation: memoryReservation, + MemorySwap: memorySwap, + KernelMemory: kernelMemory, + CPUPeriod: opts.cpuPeriod, + CPUQuota: opts.cpuQuota, + CPURealtimePeriod: opts.cpuRealtimePeriod, + CPURealtimeRuntime: opts.cpuRealtimeRuntime, } updateConfig := containertypes.UpdateConfig{ From 1ca668ac5526b4da7d5f4711b503be8e888f78da Mon Sep 17 00:00:00 2001 From: Boaz Shuster Date: Tue, 25 Oct 2016 01:55:29 +0300 Subject: [PATCH 226/978] Add unit tests to cli/command/formatter/stats.go Signed-off-by: Boaz Shuster Upstream-commit: c1da6dc7ac95804d6dd66ef0f1230a3521d120c3 Component: cli --- components/cli/command/formatter/stats.go | 2 +- .../cli/command/formatter/stats_test.go | 228 ++++++++++++++++++ 2 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 components/cli/command/formatter/stats_test.go diff --git a/components/cli/command/formatter/stats.go b/components/cli/command/formatter/stats.go index cc2588c392..b2c972251f 100644 --- a/components/cli/command/formatter/stats.go +++ b/components/cli/command/formatter/stats.go @@ -167,7 +167,7 @@ func (c *containerStatsContext) MemUsage() string { func (c *containerStatsContext) MemPerc() string { header := memPercHeader c.AddHeader(header) - if c.s.IsInvalid { + if c.s.IsInvalid || c.s.OSType == winOSType { return fmt.Sprintf("--") } return fmt.Sprintf("%.2f%%", c.s.MemoryPercentage) diff --git a/components/cli/command/formatter/stats_test.go b/components/cli/command/formatter/stats_test.go new file mode 100644 index 0000000000..f1f449e71a --- /dev/null +++ b/components/cli/command/formatter/stats_test.go @@ -0,0 +1,228 @@ +package formatter + +import ( + "bytes" + "testing" + + "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestContainerStatsContext(t *testing.T) { + containerID := stringid.GenerateRandomID() + + var ctx containerStatsContext + tt := []struct { + stats StatsEntry + expValue string + expHeader string + call func() string + }{ + {StatsEntry{Name: containerID}, containerID, containerHeader, ctx.Container}, + {StatsEntry{CPUPercentage: 5.5}, "5.50%", cpuPercHeader, ctx.CPUPerc}, + {StatsEntry{CPUPercentage: 5.5, IsInvalid: true}, "--", cpuPercHeader, ctx.CPUPerc}, + {StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3}, "0.31 B / 12.3 B", netIOHeader, ctx.NetIO}, + {StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3, IsInvalid: true}, "--", netIOHeader, ctx.NetIO}, + {StatsEntry{BlockRead: 0.1, BlockWrite: 2.3}, "0.1 B / 2.3 B", blockIOHeader, ctx.BlockIO}, + {StatsEntry{BlockRead: 0.1, BlockWrite: 2.3, IsInvalid: true}, "--", blockIOHeader, ctx.BlockIO}, + {StatsEntry{MemoryPercentage: 10.2}, "10.20%", memPercHeader, ctx.MemPerc}, + {StatsEntry{MemoryPercentage: 10.2, IsInvalid: true}, "--", memPercHeader, ctx.MemPerc}, + {StatsEntry{MemoryPercentage: 10.2, OSType: "windows"}, "--", memPercHeader, ctx.MemPerc}, + {StatsEntry{Memory: 24, MemoryLimit: 30}, "24 B / 30 B", memUseHeader, ctx.MemUsage}, + {StatsEntry{Memory: 24, MemoryLimit: 30, IsInvalid: true}, "-- / --", memUseHeader, ctx.MemUsage}, + {StatsEntry{Memory: 24, MemoryLimit: 30, OSType: "windows"}, "24 B", winMemUseHeader, ctx.MemUsage}, + {StatsEntry{PidsCurrent: 10}, "10", pidsHeader, ctx.PIDs}, + {StatsEntry{PidsCurrent: 10, IsInvalid: true}, "--", pidsHeader, ctx.PIDs}, + {StatsEntry{PidsCurrent: 10, OSType: "windows"}, "--", pidsHeader, ctx.PIDs}, + } + + for _, te := range tt { + ctx = containerStatsContext{s: te.stats} + if v := te.call(); v != te.expValue { + t.Fatalf("Expected %q, got %q", te.expValue, v) + } + + h := ctx.FullHeader() + if h != te.expHeader { + t.Fatalf("Expected %q, got %q", te.expHeader, h) + } + } +} + +func TestContainerStatsContextWrite(t *testing.T) { + tt := []struct { + context Context + expected string + }{ + { + Context{Format: "{{InvalidFunction}}"}, + `Template parsing error: template: :1: function "InvalidFunction" not defined +`, + }, + { + Context{Format: "{{nil}}"}, + `Template parsing error: template: :1:2: executing "" at : nil is not a command +`, + }, + { + Context{Format: "table {{.MemUsage}}"}, + `MEM USAGE / LIMIT +20 B / 20 B +-- / -- +`, + }, + { + Context{Format: "{{.Container}} {{.CPUPerc}}"}, + `container1 20.00% +container2 -- +`, + }, + } + + for _, te := range tt { + stats := []StatsEntry{ + { + Name: "container1", + CPUPercentage: 20, + Memory: 20, + MemoryLimit: 20, + MemoryPercentage: 20, + NetworkRx: 20, + NetworkTx: 20, + BlockRead: 20, + BlockWrite: 20, + PidsCurrent: 2, + IsInvalid: false, + OSType: "linux", + }, + { + Name: "container2", + CPUPercentage: 30, + Memory: 30, + MemoryLimit: 30, + MemoryPercentage: 30, + NetworkRx: 30, + NetworkTx: 30, + BlockRead: 30, + BlockWrite: 30, + PidsCurrent: 3, + IsInvalid: true, + OSType: "linux", + }, + } + var out bytes.Buffer + te.context.Output = &out + err := ContainerStatsWrite(te.context, stats) + if err != nil { + assert.Error(t, err, te.expected) + } else { + assert.Equal(t, out.String(), te.expected) + } + } +} + +func TestContainerStatsContextWriteWindows(t *testing.T) { + tt := []struct { + context Context + expected string + }{ + { + Context{Format: "table {{.MemUsage}}"}, + `PRIV WORKING SET +20 B +-- / -- +`, + }, + { + Context{Format: "{{.Container}} {{.CPUPerc}}"}, + `container1 20.00% +container2 -- +`, + }, + { + Context{Format: "{{.Container}} {{.MemPerc}} {{.PIDs}}"}, + `container1 -- -- +container2 -- -- +`, + }, + } + + for _, te := range tt { + stats := []StatsEntry{ + { + Name: "container1", + CPUPercentage: 20, + Memory: 20, + MemoryLimit: 20, + MemoryPercentage: 20, + NetworkRx: 20, + NetworkTx: 20, + BlockRead: 20, + BlockWrite: 20, + PidsCurrent: 2, + IsInvalid: false, + OSType: "windows", + }, + { + Name: "container2", + CPUPercentage: 30, + Memory: 30, + MemoryLimit: 30, + MemoryPercentage: 30, + NetworkRx: 30, + NetworkTx: 30, + BlockRead: 30, + BlockWrite: 30, + PidsCurrent: 3, + IsInvalid: true, + OSType: "windows", + }, + } + var out bytes.Buffer + te.context.Output = &out + err := ContainerStatsWrite(te.context, stats) + if err != nil { + assert.Error(t, err, te.expected) + } else { + assert.Equal(t, out.String(), te.expected) + } + } +} + +func TestContainerStatsContextWriteWithNoStats(t *testing.T) { + var out bytes.Buffer + + contexts := []struct { + context Context + expected string + }{ + { + Context{ + Format: "{{.Container}}", + Output: &out, + }, + "", + }, + { + Context{ + Format: "table {{.Container}}", + Output: &out, + }, + "CONTAINER\n", + }, + { + Context{ + Format: "table {{.Container}}\t{{.CPUPerc}}", + Output: &out, + }, + "CONTAINER CPU %\n", + }, + } + + for _, context := range contexts { + ContainerStatsWrite(context.context, []StatsEntry{}) + assert.Equal(t, context.expected, out.String()) + // Clean buffer + out.Reset() + } +} From 9dc55b4321a09e8140ee033cf089650f8db55719 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Wed, 28 Sep 2016 12:34:31 +0200 Subject: [PATCH 227/978] Remove --name flag from service update The --name flag was inadvertently added to docker service update, but is not supported, as it has various side-effects (e.g., existing tasks are not renamed). This removes the flag from the service update command. Signed-off-by: Sebastiaan van Stijn Upstream-commit: 6c80d2bb83ed05d98b65591b5eb64c46934a86f4 Component: cli --- components/cli/command/service/create.go | 2 ++ components/cli/command/service/opts.go | 1 - components/cli/command/service/update.go | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/command/service/create.go b/components/cli/command/service/create.go index bc5576b1ad..bb7af41f94 100644 --- a/components/cli/command/service/create.go +++ b/components/cli/command/service/create.go @@ -27,6 +27,8 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { } flags := cmd.Flags() flags.StringVar(&opts.mode, flagMode, "replicated", "Service mode (replicated or global)") + flags.StringVar(&opts.name, flagName, "", "Service name") + addServiceFlags(cmd, opts) flags.VarP(&opts.labels, flagLabel, "l", "Service labels") diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index cf25b78273..d0d383a7a1 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -490,7 +490,6 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { // Any flags that are not common are added separately in the individual command func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { flags := cmd.Flags() - flags.StringVar(&opts.name, flagName, "", "Service name") flags.StringVarP(&opts.workdir, flagWorkdir, "w", "", "Working directory inside the container") flags.StringVarP(&opts.user, flagUser, "u", "", "Username or UID (format: [:])") diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index 6034979a66..e1f7cad66b 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -172,7 +172,6 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error { return task.Resources } - updateString(flagName, &spec.Name) updateLabels(flags, &spec.Labels) updateContainerLabels(flags, &cspec.Labels) updateString("image", &cspec.Image) From 01d713986a09eb975c9da60fd1787b00be112566 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Tue, 19 Jul 2016 23:58:32 -0700 Subject: [PATCH 228/978] Add `--env-file` flag to `docker create service` This fix tries to address the issue in 24712 and add `--env-file` file to `docker create service`. Related documentation has been updated. An additional integration has been added. This fix fixes 24712. Signed-off-by: Yong Tang Upstream-commit: 1250d2afae8d0196f1a3d6c77c001f3e6c3f55a3 Component: cli --- components/cli/command/service/create.go | 1 + components/cli/command/service/opts.go | 24 +++++++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/components/cli/command/service/create.go b/components/cli/command/service/create.go index bc5576b1ad..59e838ca8f 100644 --- a/components/cli/command/service/create.go +++ b/components/cli/command/service/create.go @@ -32,6 +32,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.VarP(&opts.labels, flagLabel, "l", "Service labels") flags.Var(&opts.containerLabels, flagContainerLabel, "Container labels") flags.VarP(&opts.env, flagEnv, "e", "Set environment variables") + flags.Var(&opts.envFile, flagEnvFile, "Read in a file of environment variables") flags.Var(&opts.mounts, flagMount, "Attach a mount to the service") flags.StringSliceVar(&opts.constraints, flagConstraint, []string{}, "Placement constraints") flags.StringSliceVar(&opts.networks, flagNetwork, []string{}, "Network attachments") diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index cf25b78273..87968fd1b4 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -395,6 +395,7 @@ type serviceOptions struct { image string args []string env opts.ListOpts + envFile opts.ListOpts workdir string user string groups []string @@ -422,6 +423,7 @@ func newServiceOptions() *serviceOptions { labels: opts.NewListOpts(runconfigopts.ValidateEnv), containerLabels: opts.NewListOpts(runconfigopts.ValidateEnv), env: opts.NewListOpts(runconfigopts.ValidateEnv), + envFile: opts.NewListOpts(nil), endpoint: endpointOptions{ ports: opts.NewListOpts(ValidatePort), }, @@ -432,6 +434,25 @@ func newServiceOptions() *serviceOptions { func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { var service swarm.ServiceSpec + envVariables, err := runconfigopts.ReadKVStrings(opts.envFile.GetAll(), opts.env.GetAll()) + if err != nil { + return service, err + } + + currentEnv := make([]string, 0, len(envVariables)) + for _, env := range envVariables { // need to process each var, in order + k := strings.SplitN(env, "=", 2)[0] + for i, current := range currentEnv { // remove duplicates + if current == env { + continue // no update required, may hide this behind flag to preserve order of envVariables + } + if strings.HasPrefix(current, k+"=") { + currentEnv = append(currentEnv[:i], currentEnv[i+1:]...) + } + } + currentEnv = append(currentEnv, env) + } + service = swarm.ServiceSpec{ Annotations: swarm.Annotations{ Name: opts.name, @@ -441,7 +462,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { ContainerSpec: swarm.ContainerSpec{ Image: opts.image, Args: opts.args, - Env: opts.env.GetAll(), + Env: currentEnv, Labels: runconfigopts.ConvertKVStringsToMap(opts.containerLabels.GetAll()), Dir: opts.workdir, User: opts.user, @@ -532,6 +553,7 @@ const ( flagContainerLabelAdd = "container-label-add" flagEndpointMode = "endpoint-mode" flagEnv = "env" + flagEnvFile = "env-file" flagEnvRemove = "env-rm" flagEnvAdd = "env-add" flagGroupAdd = "group-add" From 64087d642d8956adac899abf475aba71b4b29d0b Mon Sep 17 00:00:00 2001 From: Ce Gao Date: Fri, 28 Oct 2016 08:02:57 +0800 Subject: [PATCH 229/978] fixes #27643 Signed-off-by: Ce Gao Upstream-commit: 4c1560e9879673b0a9159fb9049e20768e390eff Component: cli --- components/cli/command/service/ps.go | 5 +++++ components/cli/command/task/print.go | 29 +++++++++++++++++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/components/cli/command/service/ps.go b/components/cli/command/service/ps.go index 23c3679d7a..55f837ba8e 100644 --- a/components/cli/command/service/ps.go +++ b/components/cli/command/service/ps.go @@ -14,6 +14,7 @@ import ( type psOptions struct { serviceID string + quiet bool noResolve bool noTrunc bool filter opts.FilterOpt @@ -32,6 +33,7 @@ func newPsCommand(dockerCli *command.DockerCli) *cobra.Command { }, } flags := cmd.Flags() + flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display task IDs") flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output") flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names") flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") @@ -67,5 +69,8 @@ func runPS(dockerCli *command.DockerCli, opts psOptions) error { return err } + if opts.quiet { + return task.PrintQuiet(dockerCli, tasks) + } return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), opts.noTrunc) } diff --git a/components/cli/command/task/print.go b/components/cli/command/task/print.go index b9d6b3eaf4..b3cdcbe533 100644 --- a/components/cli/command/task/print.go +++ b/components/cli/command/task/print.go @@ -2,6 +2,7 @@ package task import ( "fmt" + "io" "sort" "strings" "text/tabwriter" @@ -40,7 +41,9 @@ func (t tasksBySlot) Less(i, j int) bool { return t[j].Meta.CreatedAt.Before(t[i].CreatedAt) } -// Print task information in a table format +// Print task information in a table format. +// Besides this, command `docker node ps ` +// and `docker stack ps` will call this, too. func Print(dockerCli *command.DockerCli, ctx context.Context, tasks []swarm.Task, resolver *idresolver.IDResolver, noTrunc bool) error { sort.Stable(tasksBySlot(tasks)) @@ -50,6 +53,27 @@ func Print(dockerCli *command.DockerCli, ctx context.Context, tasks []swarm.Task defer writer.Flush() fmt.Fprintln(writer, strings.Join([]string{"NAME", "IMAGE", "NODE", "DESIRED STATE", "CURRENT STATE", "ERROR"}, "\t")) + if err := print(writer, ctx, tasks, resolver, noTrunc); err != nil { + return err + } + + return nil +} + +// PrintQuiet shows task list in a quiet way. +func PrintQuiet(dockerCli *command.DockerCli, tasks []swarm.Task) error { + sort.Stable(tasksBySlot(tasks)) + + out := dockerCli.Out() + + for _, task := range tasks { + fmt.Fprintln(out, task.ID) + } + + return nil +} + +func print(out io.Writer, ctx context.Context, tasks []swarm.Task, resolver *idresolver.IDResolver, noTrunc bool) error { prevServiceName := "" prevSlot := 0 for _, task := range tasks { @@ -94,7 +118,7 @@ func Print(dockerCli *command.DockerCli, ctx context.Context, tasks []swarm.Task } fmt.Fprintf( - writer, + out, psTaskItemFmt, indentedName, task.Spec.ContainerSpec.Image, @@ -105,6 +129,5 @@ func Print(dockerCli *command.DockerCli, ctx context.Context, tasks []swarm.Task taskErr, ) } - return nil } From 40e9e05c74dc1a7097084b5ac2397e9d278df1c4 Mon Sep 17 00:00:00 2001 From: boucher Date: Mon, 19 Sep 2016 12:01:16 -0400 Subject: [PATCH 230/978] Allow providing a custom storage directory for docker checkpoints Signed-off-by: boucher Upstream-commit: a38761aba448f948231bbb20452332d9b7524bf5 Component: cli --- components/cli/checkpoint_delete.go | 12 ++++++++++-- components/cli/checkpoint_delete_test.go | 11 +++++++++-- components/cli/checkpoint_list.go | 10 ++++++++-- components/cli/checkpoint_list_test.go | 4 ++-- components/cli/container_start.go | 3 +++ components/cli/interface_experimental.go | 4 ++-- 6 files changed, 34 insertions(+), 10 deletions(-) diff --git a/components/cli/checkpoint_delete.go b/components/cli/checkpoint_delete.go index a4e9ed0c06..e6e75588b1 100644 --- a/components/cli/checkpoint_delete.go +++ b/components/cli/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/components/cli/checkpoint_delete_test.go b/components/cli/checkpoint_delete_test.go index 23931c6523..a78b050487 100644 --- a/components/cli/checkpoint_delete_test.go +++ b/components/cli/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/components/cli/checkpoint_list.go b/components/cli/checkpoint_list.go index bb471e0056..8eb720a6b2 100644 --- a/components/cli/checkpoint_list.go +++ b/components/cli/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/components/cli/checkpoint_list_test.go b/components/cli/checkpoint_list_test.go index e636995bc1..6c90f61e8c 100644 --- a/components/cli/checkpoint_list_test.go +++ b/components/cli/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/components/cli/container_start.go b/components/cli/container_start.go index 44bb0080c0..b1f08de416 100644 --- a/components/cli/container_start.go +++ b/components/cli/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/components/cli/interface_experimental.go b/components/cli/interface_experimental.go index ddb9f79b5a..4f5cf853b8 100644 --- a/components/cli/interface_experimental.go +++ b/components/cli/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 5099913cf928f37b6ab71b34e81d265419efd3be Mon Sep 17 00:00:00 2001 From: boucher Date: Mon, 19 Sep 2016 12:01:16 -0400 Subject: [PATCH 231/978] Allow providing a custom storage directory for docker checkpoints Signed-off-by: boucher Upstream-commit: 5ddcbc3c0058d1982f73305db1ceb372bcf1b229 Component: cli --- components/cli/command/checkpoint/create.go | 13 +++++++---- components/cli/command/checkpoint/list.go | 25 ++++++++++++++++---- components/cli/command/checkpoint/remove.go | 26 +++++++++++++++++---- components/cli/command/container/start.go | 16 ++++++++----- 4 files changed, 61 insertions(+), 19 deletions(-) diff --git a/components/cli/command/checkpoint/create.go b/components/cli/command/checkpoint/create.go index d369718119..646901ccd6 100644 --- a/components/cli/command/checkpoint/create.go +++ b/components/cli/command/checkpoint/create.go @@ -10,9 +10,10 @@ import ( ) type createOptions struct { - container string - checkpoint string - leaveRunning bool + container string + checkpoint string + checkpointDir string + leaveRunning bool } func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { @@ -31,6 +32,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() flags.BoolVar(&opts.leaveRunning, "leave-running", false, "leave the container running after checkpoint") + flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "use a custom checkpoint storage directory") return cmd } @@ -39,8 +41,9 @@ func runCreate(dockerCli *command.DockerCli, opts createOptions) error { client := dockerCli.Client() checkpointOpts := types.CheckpointCreateOptions{ - CheckpointID: opts.checkpoint, - Exit: !opts.leaveRunning, + CheckpointID: opts.checkpoint, + CheckpointDir: opts.checkpointDir, + Exit: !opts.leaveRunning, } err := client.CheckpointCreate(context.Background(), opts.container, checkpointOpts) diff --git a/components/cli/command/checkpoint/list.go b/components/cli/command/checkpoint/list.go index 7ba035890b..fef91a4ccd 100644 --- a/components/cli/command/checkpoint/list.go +++ b/components/cli/command/checkpoint/list.go @@ -6,27 +6,44 @@ import ( "golang.org/x/net/context" + "github.com/docker/docker/api/types" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/spf13/cobra" ) +type listOptions struct { + checkpointDir string +} + func newListCommand(dockerCli *command.DockerCli) *cobra.Command { - return &cobra.Command{ + var opts listOptions + + cmd := &cobra.Command{ Use: "ls CONTAINER", Aliases: []string{"list"}, Short: "List checkpoints for a container", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return runList(dockerCli, args[0]) + return runList(dockerCli, args[0], opts) }, } + + flags := cmd.Flags() + flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "use a custom checkpoint storage directory") + + return cmd + } -func runList(dockerCli *command.DockerCli, container string) error { +func runList(dockerCli *command.DockerCli, container string, opts listOptions) error { client := dockerCli.Client() - checkpoints, err := client.CheckpointList(context.Background(), container) + listOpts := types.CheckpointListOptions{ + CheckpointDir: opts.checkpointDir, + } + + checkpoints, err := client.CheckpointList(context.Background(), container, listOpts) if err != nil { return err } diff --git a/components/cli/command/checkpoint/remove.go b/components/cli/command/checkpoint/remove.go index 82ce62312b..c6ec56df84 100644 --- a/components/cli/command/checkpoint/remove.go +++ b/components/cli/command/checkpoint/remove.go @@ -3,24 +3,42 @@ package checkpoint import ( "golang.org/x/net/context" + "github.com/docker/docker/api/types" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/spf13/cobra" ) +type removeOptions struct { + checkpointDir string +} + func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { - return &cobra.Command{ + var opts removeOptions + + cmd := &cobra.Command{ Use: "rm CONTAINER CHECKPOINT", Aliases: []string{"remove"}, Short: "Remove a checkpoint", Args: cli.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - return runRemove(dockerCli, args[0], args[1]) + return runRemove(dockerCli, args[0], args[1], opts) }, } + + flags := cmd.Flags() + flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "use a custom checkpoint storage directory") + + return cmd } -func runRemove(dockerCli *command.DockerCli, container string, checkpoint string) error { +func runRemove(dockerCli *command.DockerCli, container string, checkpoint string, opts removeOptions) error { client := dockerCli.Client() - return client.CheckpointDelete(context.Background(), container, checkpoint) + + removeOpts := types.CheckpointDeleteOptions{ + CheckpointID: checkpoint, + CheckpointDir: opts.checkpointDir, + } + + return client.CheckpointDelete(context.Background(), container, removeOpts) } diff --git a/components/cli/command/container/start.go b/components/cli/command/container/start.go index 8693b3a550..8e0654da37 100644 --- a/components/cli/command/container/start.go +++ b/components/cli/command/container/start.go @@ -17,10 +17,11 @@ import ( ) type startOptions struct { - attach bool - openStdin bool - detachKeys string - checkpoint string + attach bool + openStdin bool + detachKeys string + checkpoint string + checkpointDir string containers []string } @@ -46,6 +47,7 @@ func NewStartCommand(dockerCli *command.DockerCli) *cobra.Command { if dockerCli.HasExperimental() { flags.StringVar(&opts.checkpoint, "checkpoint", "", "Restore from this checkpoint") + flags.StringVar(&opts.checkpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory") } return cmd @@ -112,7 +114,8 @@ func runStart(dockerCli *command.DockerCli, opts *startOptions) error { // no matter it's detached, removed on daemon side(--rm) or exit normally. statusChan := waitExitOrRemoved(dockerCli, ctx, c.ID, c.HostConfig.AutoRemove) startOptions := types.ContainerStartOptions{ - CheckpointID: opts.checkpoint, + CheckpointID: opts.checkpoint, + CheckpointDir: opts.checkpointDir, } // 4. Start the container. @@ -145,7 +148,8 @@ func runStart(dockerCli *command.DockerCli, opts *startOptions) error { } container := opts.containers[0] startOptions := types.ContainerStartOptions{ - CheckpointID: opts.checkpoint, + CheckpointID: opts.checkpoint, + CheckpointDir: opts.checkpointDir, } return dockerCli.Client().ContainerStart(ctx, container, startOptions) From 2d1410ba927a1749add8f006be78e756436bd6c3 Mon Sep 17 00:00:00 2001 From: Cezar Sa Espinola Date: Thu, 13 Oct 2016 15:28:32 -0300 Subject: [PATCH 232/978] Add --health-* commands to service create and update A HealthConfig entry was added to the ContainerSpec associated with the service being created or updated. Signed-off-by: Cezar Sa Espinola Upstream-commit: 87e916a171eca346506a735d7b604150e5ecbad0 Component: cli --- components/cli/command/service/opts.go | 80 +++++++++++++++++++ components/cli/command/service/opts_test.go | 49 ++++++++++++ components/cli/command/service/update.go | 50 ++++++++++++ components/cli/command/service/update_test.go | 79 ++++++++++++++++++ 4 files changed, 258 insertions(+) diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index 5fdc56de05..0c91360c6e 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/docker/docker/api/types/container" mounttypes "github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/opts" @@ -68,6 +69,25 @@ func (c *nanoCPUs) Value() int64 { return int64(*c) } +// PositiveDurationOpt is an option type for time.Duration that uses a pointer. +// It bahave similarly to DurationOpt but only allows positive duration values. +type PositiveDurationOpt struct { + DurationOpt +} + +// Set a new value on the option. Setting a negative duration value will cause +// an error to be returned. +func (d *PositiveDurationOpt) Set(s string) error { + err := d.DurationOpt.Set(s) + if err != nil { + return err + } + if *d.DurationOpt.value < 0 { + return fmt.Errorf("duration cannot be negative") + } + return nil +} + // DurationOpt is an option type for time.Duration that uses a pointer. This // allows us to get nil values outside, instead of defaulting to 0 type DurationOpt struct { @@ -377,6 +397,47 @@ func (ldo *logDriverOptions) toLogDriver() *swarm.Driver { } } +type healthCheckOptions struct { + cmd string + interval PositiveDurationOpt + timeout PositiveDurationOpt + retries int + noHealthcheck bool +} + +func (opts *healthCheckOptions) toHealthConfig() (*container.HealthConfig, error) { + var healthConfig *container.HealthConfig + haveHealthSettings := opts.cmd != "" || + opts.interval.Value() != nil || + opts.timeout.Value() != nil || + opts.retries != 0 + if opts.noHealthcheck { + if haveHealthSettings { + return nil, fmt.Errorf("--%s conflicts with --health-* options", flagNoHealthcheck) + } + healthConfig = &container.HealthConfig{Test: []string{"NONE"}} + } else if haveHealthSettings { + var test []string + if opts.cmd != "" { + test = []string{"CMD-SHELL", opts.cmd} + } + var interval, timeout time.Duration + if ptr := opts.interval.Value(); ptr != nil { + interval = *ptr + } + if ptr := opts.timeout.Value(); ptr != nil { + timeout = *ptr + } + healthConfig = &container.HealthConfig{ + Test: test, + Interval: interval, + Timeout: timeout, + Retries: opts.retries, + } + } + return healthConfig, nil +} + // ValidatePort validates a string is in the expected format for a port definition func ValidatePort(value string) (string, error) { portMappings, err := nat.ParsePortSpec(value) @@ -416,6 +477,8 @@ type serviceOptions struct { registryAuth bool logDriver logDriverOptions + + healthcheck healthCheckOptions } func newServiceOptions() *serviceOptions { @@ -490,6 +553,12 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { EndpointSpec: opts.endpoint.ToEndpointSpec(), } + healthConfig, err := opts.healthcheck.toHealthConfig() + if err != nil { + return service, err + } + service.TaskTemplate.ContainerSpec.Healthcheck = healthConfig + switch opts.mode { case "global": if opts.replicas.Value() != nil { @@ -541,6 +610,12 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { flags.StringVar(&opts.logDriver.name, flagLogDriver, "", "Logging driver for service") flags.Var(&opts.logDriver.opts, flagLogOpt, "Logging driver options") + + flags.StringVar(&opts.healthcheck.cmd, flagHealthCmd, "", "Command to run to check health") + flags.Var(&opts.healthcheck.interval, flagHealthInterval, "Time between running the check") + flags.Var(&opts.healthcheck.timeout, flagHealthTimeout, "Maximum time to allow one check to run") + flags.IntVar(&opts.healthcheck.retries, flagHealthRetries, 0, "Consecutive failures needed to report unhealthy") + flags.BoolVar(&opts.healthcheck.noHealthcheck, flagNoHealthcheck, false, "Disable any container-specified HEALTHCHECK") } const ( @@ -589,4 +664,9 @@ const ( flagRegistryAuth = "with-registry-auth" flagLogDriver = "log-driver" flagLogOpt = "log-opt" + flagHealthCmd = "health-cmd" + flagHealthInterval = "health-interval" + flagHealthRetries = "health-retries" + flagHealthTimeout = "health-timeout" + flagNoHealthcheck = "no-healthcheck" ) diff --git a/components/cli/command/service/opts_test.go b/components/cli/command/service/opts_test.go index 8ef3cacb45..52016cbfc5 100644 --- a/components/cli/command/service/opts_test.go +++ b/components/cli/command/service/opts_test.go @@ -1,9 +1,11 @@ package service import ( + "reflect" "testing" "time" + "github.com/docker/docker/api/types/container" mounttypes "github.com/docker/docker/api/types/mount" "github.com/docker/docker/pkg/testutil/assert" ) @@ -40,6 +42,15 @@ func TestDurationOptSetAndValue(t *testing.T) { var duration DurationOpt assert.NilError(t, duration.Set("300s")) assert.Equal(t, *duration.Value(), time.Duration(300*10e8)) + assert.NilError(t, duration.Set("-300s")) + assert.Equal(t, *duration.Value(), time.Duration(-300*10e8)) +} + +func TestPositiveDurationOptSetAndValue(t *testing.T) { + var duration PositiveDurationOpt + assert.NilError(t, duration.Set("300s")) + assert.Equal(t, *duration.Value(), time.Duration(300*10e8)) + assert.Error(t, duration.Set("-300s"), "cannot be negative") } func TestUint64OptString(t *testing.T) { @@ -201,3 +212,41 @@ func TestMountOptTypeConflict(t *testing.T) { assert.Error(t, m.Set("type=bind,target=/foo,source=/foo,volume-nocopy=true"), "cannot mix") assert.Error(t, m.Set("type=volume,target=/foo,source=/foo,bind-propagation=rprivate"), "cannot mix") } + +func TestHealthCheckOptionsToHealthConfig(t *testing.T) { + dur := time.Second + opt := healthCheckOptions{ + cmd: "curl", + interval: PositiveDurationOpt{DurationOpt{value: &dur}}, + timeout: PositiveDurationOpt{DurationOpt{value: &dur}}, + retries: 10, + } + config, err := opt.toHealthConfig() + assert.NilError(t, err) + assert.Equal(t, reflect.DeepEqual(config, &container.HealthConfig{ + Test: []string{"CMD-SHELL", "curl"}, + Interval: time.Second, + Timeout: time.Second, + Retries: 10, + }), true) +} + +func TestHealthCheckOptionsToHealthConfigNoHealthcheck(t *testing.T) { + opt := healthCheckOptions{ + noHealthcheck: true, + } + config, err := opt.toHealthConfig() + assert.NilError(t, err) + assert.Equal(t, reflect.DeepEqual(config, &container.HealthConfig{ + Test: []string{"NONE"}, + }), true) +} + +func TestHealthCheckOptionsToHealthConfigConflict(t *testing.T) { + opt := healthCheckOptions{ + cmd: "curl", + noHealthcheck: true, + } + _, err := opt.toHealthConfig() + assert.Error(t, err, "--no-healthcheck conflicts with --health-* options") +} diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index e1f7cad66b..356c27a5c6 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -9,6 +9,7 @@ import ( "golang.org/x/net/context" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" mounttypes "github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" @@ -266,6 +267,10 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error { spec.TaskTemplate.ForceUpdate++ } + if err := updateHealthcheck(flags, cspec); err != nil { + return err + } + return nil } @@ -537,3 +542,48 @@ func updateLogDriver(flags *pflag.FlagSet, taskTemplate *swarm.TaskSpec) error { return nil } + +func updateHealthcheck(flags *pflag.FlagSet, containerSpec *swarm.ContainerSpec) error { + if !anyChanged(flags, flagNoHealthcheck, flagHealthCmd, flagHealthInterval, flagHealthRetries, flagHealthTimeout) { + return nil + } + if containerSpec.Healthcheck == nil { + containerSpec.Healthcheck = &container.HealthConfig{} + } + noHealthcheck, err := flags.GetBool(flagNoHealthcheck) + if err != nil { + return err + } + if noHealthcheck { + if !anyChanged(flags, flagHealthCmd, flagHealthInterval, flagHealthRetries, flagHealthTimeout) { + containerSpec.Healthcheck = &container.HealthConfig{ + Test: []string{"NONE"}, + } + return nil + } + return fmt.Errorf("--%s conflicts with --health-* options", flagNoHealthcheck) + } + if len(containerSpec.Healthcheck.Test) > 0 && containerSpec.Healthcheck.Test[0] == "NONE" { + containerSpec.Healthcheck.Test = nil + } + if flags.Changed(flagHealthInterval) { + val := *flags.Lookup(flagHealthInterval).Value.(*PositiveDurationOpt).Value() + containerSpec.Healthcheck.Interval = val + } + if flags.Changed(flagHealthTimeout) { + val := *flags.Lookup(flagHealthTimeout).Value.(*PositiveDurationOpt).Value() + containerSpec.Healthcheck.Timeout = val + } + if flags.Changed(flagHealthRetries) { + containerSpec.Healthcheck.Retries, _ = flags.GetInt(flagHealthRetries) + } + if flags.Changed(flagHealthCmd) { + cmd, _ := flags.GetString(flagHealthCmd) + if cmd != "" { + containerSpec.Healthcheck.Test = []string{"CMD-SHELL", cmd} + } else { + containerSpec.Healthcheck.Test = nil + } + } + return nil +} diff --git a/components/cli/command/service/update_test.go b/components/cli/command/service/update_test.go index 6e68e977ac..731358753e 100644 --- a/components/cli/command/service/update_test.go +++ b/components/cli/command/service/update_test.go @@ -1,9 +1,12 @@ package service import ( + "reflect" "sort" "testing" + "time" + "github.com/docker/docker/api/types/container" mounttypes "github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/pkg/testutil/assert" @@ -196,3 +199,79 @@ func TestUpdatePortsConflictingFlags(t *testing.T) { err := updatePorts(flags, &portConfigs) assert.Error(t, err, "conflicting port mapping") } + +func TestUpdateHealthcheckTable(t *testing.T) { + type test struct { + flags [][2]string + initial *container.HealthConfig + expected *container.HealthConfig + err string + } + testCases := []test{ + { + flags: [][2]string{{"no-healthcheck", "true"}}, + initial: &container.HealthConfig{Test: []string{"CMD-SHELL", "cmd1"}, Retries: 10}, + expected: &container.HealthConfig{Test: []string{"NONE"}}, + }, + { + flags: [][2]string{{"health-cmd", "cmd1"}}, + initial: &container.HealthConfig{Test: []string{"NONE"}}, + expected: &container.HealthConfig{Test: []string{"CMD-SHELL", "cmd1"}}, + }, + { + flags: [][2]string{{"health-retries", "10"}}, + initial: &container.HealthConfig{Test: []string{"NONE"}}, + expected: &container.HealthConfig{Retries: 10}, + }, + { + flags: [][2]string{{"health-retries", "10"}}, + initial: &container.HealthConfig{Test: []string{"CMD", "cmd1"}}, + expected: &container.HealthConfig{Test: []string{"CMD", "cmd1"}, Retries: 10}, + }, + { + flags: [][2]string{{"health-interval", "1m"}}, + initial: &container.HealthConfig{Test: []string{"CMD", "cmd1"}}, + expected: &container.HealthConfig{Test: []string{"CMD", "cmd1"}, Interval: time.Minute}, + }, + { + flags: [][2]string{{"health-cmd", ""}}, + initial: &container.HealthConfig{Test: []string{"CMD", "cmd1"}, Retries: 10}, + expected: &container.HealthConfig{Retries: 10}, + }, + { + flags: [][2]string{{"health-retries", "0"}}, + initial: &container.HealthConfig{Test: []string{"CMD", "cmd1"}, Retries: 10}, + expected: &container.HealthConfig{Test: []string{"CMD", "cmd1"}}, + }, + { + flags: [][2]string{{"health-cmd", "cmd1"}, {"no-healthcheck", "true"}}, + err: "--no-healthcheck conflicts with --health-* options", + }, + { + flags: [][2]string{{"health-interval", "10m"}, {"no-healthcheck", "true"}}, + err: "--no-healthcheck conflicts with --health-* options", + }, + { + flags: [][2]string{{"health-timeout", "1m"}, {"no-healthcheck", "true"}}, + err: "--no-healthcheck conflicts with --health-* options", + }, + } + for i, c := range testCases { + flags := newUpdateCommand(nil).Flags() + for _, flag := range c.flags { + flags.Set(flag[0], flag[1]) + } + cspec := &swarm.ContainerSpec{ + Healthcheck: c.initial, + } + err := updateHealthcheck(flags, cspec) + if c.err != "" { + assert.Error(t, err, c.err) + } else { + assert.NilError(t, err) + if !reflect.DeepEqual(cspec.Healthcheck, c.expected) { + t.Errorf("incorrect result for test %d, expected health config:\n\t%#v\ngot:\n\t%#v", i, c.expected, cspec.Healthcheck) + } + } + } +} From e527c561311e3415c49beb9fb0b5f791c2964fda Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 28 Oct 2016 11:48:25 -0700 Subject: [PATCH 233/978] Add StatsFormat to the config.json file As for `ps`, `images`, `network ls` and `volume ls`, this makes it possible to define a custom default format. Signed-off-by: Vincent Demeester Upstream-commit: 908aa5b4087ec5a4a257b9403e62e2850526f7a0 Component: cli --- components/cli/command/container/stats.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/components/cli/command/container/stats.go b/components/cli/command/container/stats.go index e7954e4b98..5e743a4832 100644 --- a/components/cli/command/container/stats.go +++ b/components/cli/command/container/stats.go @@ -186,13 +186,17 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error { // before print to screen, make sure each container get at least one valid stat data waitFirst.Wait() - f := "table" - if len(opts.format) > 0 { - f = opts.format + format := opts.format + if len(format) == 0 { + if len(dockerCli.ConfigFile().StatsFormat) > 0 { + format = dockerCli.ConfigFile().StatsFormat + } else { + format = formatter.TableFormatKey + } } statsCtx := formatter.Context{ Output: dockerCli.Out(), - Format: formatter.NewStatsFormat(f, daemonOSType), + Format: formatter.NewStatsFormat(format, daemonOSType), } cleanScreen := func() { if !opts.noStream { From f65dcbf8f4816b46690e7f110bcd49855fa5c238 Mon Sep 17 00:00:00 2001 From: Lily Guo Date: Wed, 26 Oct 2016 12:46:40 -0700 Subject: [PATCH 234/978] Service create --group param --group-add was used for specifying groups for both service create and service update. For create it was confusing since we don't have an existing set of groups. Instead I added --group to create, and moved --group-add to service update only, like --group-rm This deals with issue 27646 Signed-off-by: Lily Guo Update flag documentation Specify that --group, --group-add and --groupd-rm refers to supplementary user groups Signed-off-by: Lily Guo Fix docs for groups and update completion scripts Signed-off-by: Lily Guo Upstream-commit: 378ae7234a303310f3bc1a6b9e814aefad1dad86 Component: cli --- components/cli/command/service/create.go | 1 + components/cli/command/service/opts.go | 2 +- components/cli/command/service/update.go | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/components/cli/command/service/create.go b/components/cli/command/service/create.go index d2925b42db..28790ec8e6 100644 --- a/components/cli/command/service/create.go +++ b/components/cli/command/service/create.go @@ -39,6 +39,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.StringSliceVar(&opts.constraints, flagConstraint, []string{}, "Placement constraints") flags.StringSliceVar(&opts.networks, flagNetwork, []string{}, "Network attachments") flags.VarP(&opts.endpoint.ports, flagPublish, "p", "Publish a port as a node port") + flags.StringSliceVar(&opts.groups, flagGroup, []string{}, "Set one or more supplementary user groups for the container") flags.SetInterspersed(false) return cmd diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index 0c91360c6e..43b7b671cb 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -583,7 +583,6 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { flags.StringVarP(&opts.workdir, flagWorkdir, "w", "", "Working directory inside the container") flags.StringVarP(&opts.user, flagUser, "u", "", "Username or UID (format: [:])") - flags.StringSliceVar(&opts.groups, flagGroupAdd, []string{}, "Add additional user groups to the container") flags.Var(&opts.resources.limitCPU, flagLimitCPU, "Limit CPUs") flags.Var(&opts.resources.limitMemBytes, flagLimitMemory, "Limit Memory") @@ -630,6 +629,7 @@ const ( flagEnvFile = "env-file" flagEnvRemove = "env-rm" flagEnvAdd = "env-add" + flagGroup = "group" flagGroupAdd = "group-add" flagGroupRemove = "group-rm" flagLabel = "label" diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index 356c27a5c6..b76a20e97c 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -42,7 +42,7 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { addServiceFlags(cmd, opts) flags.Var(newListOptsVar(), flagEnvRemove, "Remove an environment variable") - flags.Var(newListOptsVar(), flagGroupRemove, "Remove previously added user groups from the container") + flags.Var(newListOptsVar(), flagGroupRemove, "Remove previously added supplementary user groups from the container") flags.Var(newListOptsVar(), flagLabelRemove, "Remove a label by its key") flags.Var(newListOptsVar(), flagContainerLabelRemove, "Remove a container label by its key") flags.Var(newListOptsVar(), flagMountRemove, "Remove a mount by its target path") @@ -54,6 +54,7 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Var(&opts.mounts, flagMountAdd, "Add or update a mount on a service") flags.StringSliceVar(&opts.constraints, flagConstraintAdd, []string{}, "Add or update placement constraints") flags.Var(&opts.endpoint.ports, flagPublishAdd, "Add or update a published port") + flags.StringSliceVar(&opts.groups, flagGroupAdd, []string{}, "Add additional supplementary user groups to the container") return cmd } From f6256ed04b9256ace207c18eb9d119942b3fc175 Mon Sep 17 00:00:00 2001 From: Qiang Huang Date: Sat, 29 Oct 2016 15:03:26 +0800 Subject: [PATCH 235/978] Fix bunch of typos Signed-off-by: Qiang Huang Upstream-commit: faac17728583bfd6342603addb0937d86cd430ca Component: cli --- components/cli/command/registry/logout.go | 2 +- components/cli/command/swarm/join_token.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/command/registry/logout.go b/components/cli/command/registry/logout.go index 5d80595ff0..a735818049 100644 --- a/components/cli/command/registry/logout.go +++ b/components/cli/command/registry/logout.go @@ -47,7 +47,7 @@ func runLogout(dockerCli *command.DockerCli, serverAddress string) error { ) if !isDefaultRegistry { hostnameAddress = registry.ConvertToHostname(serverAddress) - // the tries below are kept for backward compatibily where a user could have + // the tries below are kept for backward compatibility where a user could have // saved the registry in one of the following format. regsToTry = append(regsToTry, hostnameAddress, "http://"+hostnameAddress, "https://"+hostnameAddress) } diff --git a/components/cli/command/swarm/join_token.go b/components/cli/command/swarm/join_token.go index b411202083..3a17a8020f 100644 --- a/components/cli/command/swarm/join_token.go +++ b/components/cli/command/swarm/join_token.go @@ -46,7 +46,7 @@ func newJoinTokenCommand(dockerCli *command.DockerCli) *cobra.Command { return err } if !quiet { - fmt.Fprintf(dockerCli.Out(), "Succesfully rotated %s join token.\n\n", args[0]) + fmt.Fprintf(dockerCli.Out(), "Successfully rotated %s join token.\n\n", args[0]) } } From 0473c568d7fcae06563273b9ce6717ab7fbd7970 Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Sun, 18 Sep 2016 13:11:02 +0800 Subject: [PATCH 236/978] Modify short and flags for docker inspect Signed-off-by: yuexiao-wang Upstream-commit: 6027424adfec69183e04cb6e6cd33652dc9a8fa5 Component: cli --- components/cli/command/system/inspect.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/components/cli/command/system/inspect.go b/components/cli/command/system/inspect.go index 015c1b5c6d..06f1f1abf5 100644 --- a/components/cli/command/system/inspect.go +++ b/components/cli/command/system/inspect.go @@ -25,9 +25,12 @@ func NewInspectCommand(dockerCli *command.DockerCli) *cobra.Command { var opts inspectOptions cmd := &cobra.Command{ - Use: "inspect [OPTIONS] CONTAINER|IMAGE|TASK [CONTAINER|IMAGE|TASK...]", - Short: "Return low-level information on a container, image or task", - Args: cli.RequiresMinArgs(1), + Use: "inspect [OPTIONS] NAME|ID [NAME|ID...]", + Short: strings.Join([]string{ + "Return low-level information on Docker object(s) (e.g. container, image, volume,", + "\nnetwork, node, service, or task) identified by name or ID", + }, ""), + Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.ids = args return runInspect(dockerCli, opts) From f73b440c5ccccb38661b9998fe7d3095b4198c56 Mon Sep 17 00:00:00 2001 From: Boaz Shuster Date: Mon, 31 Oct 2016 14:12:10 +0200 Subject: [PATCH 237/978] Validate docker-load receives a tar file To load an image from a tar file, you can specify the tar file in the -i/--input option: docker load -i image_1.tar or using stdin: docker load < image_1.tar cat image_1.tat | docker load If the image file isn't given the `docker load` command gets stuck. To avoid that, the load makes sure the CLI input is not a terminal or the `--input` option was set. If not then an error message is shown. Signed-off-by: Boaz Shuster Upstream-commit: fdbf29e1fa69decd3676d225fee2879ea9b5e61a Component: cli --- components/cli/command/image/load.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/components/cli/command/image/load.go b/components/cli/command/image/load.go index 56145a8a34..4f88faf094 100644 --- a/components/cli/command/image/load.go +++ b/components/cli/command/image/load.go @@ -1,6 +1,7 @@ package image import ( + "fmt" "io" "os" @@ -49,6 +50,13 @@ func runLoad(dockerCli *command.DockerCli, opts loadOptions) error { defer file.Close() input = file } + + // To avoid getting stuck, verify that a tar file is given either in + // the input flag or through stdin and if not display an error message and exit. + if opts.input == "" && dockerCli.In().IsTerminal() { + return fmt.Errorf("requested load from stdin, but stdin is empty") + } + if !dockerCli.Out().IsTerminal() { opts.quiet = true } From daa7c9a587536873c2b45a4de967b463c05fe0df Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 4 Oct 2016 11:40:17 -0400 Subject: [PATCH 238/978] Generate VolumeList response from the swagger spec Signed-off-by: Daniel Nephin Upstream-commit: 3e13296c4eca92652c7e29e95a25178a94fe692c Component: cli --- components/cli/interface.go | 3 ++- components/cli/volume_list.go | 6 +++--- components/cli/volume_list_test.go | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/components/cli/interface.go b/components/cli/interface.go index 8abdb0f6fc..613015f865 100644 --- a/components/cli/interface.go +++ b/components/cli/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/components/cli/volume_list.go b/components/cli/volume_list.go index 44f03cfac7..9923ecb82c 100644 --- a/components/cli/volume_list.go +++ b/components/cli/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/components/cli/volume_list_test.go b/components/cli/volume_list_test.go index 0af420eaff..ffdd904b58 100644 --- a/components/cli/volume_list_test.go +++ b/components/cli/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 6fb78dfac2766ccfc259a13a0eed3568a4e144a6 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 6 Oct 2016 12:57:17 -0400 Subject: [PATCH 239/978] Generate VolumesCreateRequest from the swagger spec. Signed-off-by: Daniel Nephin Upstream-commit: 0325c474b881c6f29bd688c2558bf3c0b9495daa Component: cli --- components/cli/interface.go | 2 +- components/cli/volume_create.go | 3 ++- components/cli/volume_create_test.go | 5 +++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/components/cli/interface.go b/components/cli/interface.go index 613015f865..5ec750abe1 100644 --- a/components/cli/interface.go +++ b/components/cli/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/components/cli/volume_create.go b/components/cli/volume_create.go index f3a79f1e11..b18e5fe600 100644 --- a/components/cli/volume_create.go +++ b/components/cli/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/components/cli/volume_create_test.go b/components/cli/volume_create_test.go index 75085296cc..d5d3791685 100644 --- a/components/cli/volume_create_test.go +++ b/components/cli/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 f382f9809209f2a95225415fcfacf6792727fba0 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 6 Oct 2016 12:57:17 -0400 Subject: [PATCH 240/978] Generate VolumesCreateRequest from the swagger spec. Signed-off-by: Daniel Nephin Upstream-commit: 120c5f99644441bfef1c2cf2b4ee39f0fdcc842a Component: cli --- components/cli/command/volume/create.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/command/volume/create.go b/components/cli/command/volume/create.go index fbf62a5ef1..f16e650bbc 100644 --- a/components/cli/command/volume/create.go +++ b/components/cli/command/volume/create.go @@ -5,7 +5,7 @@ import ( "golang.org/x/net/context" - "github.com/docker/docker/api/types" + volumetypes "github.com/docker/docker/api/server/types/volume" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/opts" @@ -55,7 +55,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { func runCreate(dockerCli *command.DockerCli, opts createOptions) error { client := dockerCli.Client() - volReq := types.VolumeCreateRequest{ + volReq := volumetypes.VolumesCreateBody{ Driver: opts.driver, DriverOpts: opts.driverOpts.GetAll(), Name: opts.name, From d817915a4e58579718ca3db1093e185b9ac13341 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 14 Oct 2016 16:20:13 -0400 Subject: [PATCH 241/978] Use a config to generate swagger api types Moves the resposne types to a package under api/types Signed-off-by: Daniel Nephin Upstream-commit: 6dc945ab369fbd67dff60deda19aab21b886a254 Component: cli --- components/cli/interface.go | 2 +- components/cli/volume_create.go | 2 +- components/cli/volume_create_test.go | 2 +- components/cli/volume_list.go | 2 +- components/cli/volume_list_test.go | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/components/cli/interface.go b/components/cli/interface.go index 5ec750abe1..1f20a8be73 100644 --- a/components/cli/interface.go +++ b/components/cli/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/components/cli/volume_create.go b/components/cli/volume_create.go index b18e5fe600..9620c87cbf 100644 --- a/components/cli/volume_create.go +++ b/components/cli/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/components/cli/volume_create_test.go b/components/cli/volume_create_test.go index d5d3791685..9f1b2540b5 100644 --- a/components/cli/volume_create_test.go +++ b/components/cli/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/components/cli/volume_list.go b/components/cli/volume_list.go index 9923ecb82c..32247ce115 100644 --- a/components/cli/volume_list.go +++ b/components/cli/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/components/cli/volume_list_test.go b/components/cli/volume_list_test.go index ffdd904b58..f29639be23 100644 --- a/components/cli/volume_list_test.go +++ b/components/cli/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 1dc49df012e57f4a69e2a46ffcbb4ea53c206ed9 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 14 Oct 2016 16:20:13 -0400 Subject: [PATCH 242/978] Use a config to generate swagger api types Moves the resposne types to a package under api/types Signed-off-by: Daniel Nephin Upstream-commit: 010023c3c6a594aa9ec62985b19fa1c048c49944 Component: cli --- components/cli/command/volume/create.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/volume/create.go b/components/cli/command/volume/create.go index f16e650bbc..7b2a7e3318 100644 --- a/components/cli/command/volume/create.go +++ b/components/cli/command/volume/create.go @@ -5,7 +5,7 @@ import ( "golang.org/x/net/context" - volumetypes "github.com/docker/docker/api/server/types/volume" + volumetypes "github.com/docker/docker/api/types/volume" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/opts" From 9557c883f53590e80e095e73789c5717d3b62fa7 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 14 Oct 2016 16:28:47 -0400 Subject: [PATCH 243/978] Generate container create response from swagger spec. Signed-off-by: Daniel Nephin Upstream-commit: e0f7f8d0dd71fe646faa81bf343a9082918ebd38 Component: cli --- components/cli/container_create.go | 5 ++--- components/cli/container_create_test.go | 3 +-- components/cli/interface.go | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/components/cli/container_create.go b/components/cli/container_create.go index a862172956..c042b17468 100644 --- a/components/cli/container_create.go +++ b/components/cli/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/components/cli/container_create_test.go b/components/cli/container_create_test.go index 5325156beb..89641038f7 100644 --- a/components/cli/container_create_test.go +++ b/components/cli/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/components/cli/interface.go b/components/cli/interface.go index 1f20a8be73..8f8bbaf55f 100644 --- a/components/cli/interface.go +++ b/components/cli/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 71efaf6beb365a9233ce09da8216755297776df9 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 18 Oct 2016 15:56:45 -0700 Subject: [PATCH 244/978] Add an IDResponse type Generated from a swagger spec and use it for container exec response Signed-off-by: Daniel Nephin Upstream-commit: d4d914bd5226b2c9933da7c3881716e1a9e9003a Component: cli --- components/cli/container_exec.go | 4 ++-- components/cli/container_exec_test.go | 2 +- components/cli/interface.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/components/cli/container_exec.go b/components/cli/container_exec.go index 34173d3194..f6df722918 100644 --- a/components/cli/container_exec.go +++ b/components/cli/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/components/cli/container_exec_test.go b/components/cli/container_exec_test.go index 42146ae8a5..0e296a50ad 100644 --- a/components/cli/container_exec_test.go +++ b/components/cli/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/components/cli/interface.go b/components/cli/interface.go index 8f8bbaf55f..0575ce5c3b 100644 --- a/components/cli/interface.go +++ b/components/cli/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 bc55ba3a446f9d12dcfcd084166d16d8265687ab Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 18 Oct 2016 17:27:55 -0700 Subject: [PATCH 245/978] Use IDResponse for container create response. Signed-off-by: Daniel Nephin Upstream-commit: f8cdc5ae711142dfc805a8a3483fb3976f3edaf8 Component: cli --- components/cli/container_commit.go | 8 ++++---- components/cli/container_commit_test.go | 2 +- components/cli/interface.go | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/components/cli/container_commit.go b/components/cli/container_commit.go index 363950cc24..c766d62e40 100644 --- a/components/cli/container_commit.go +++ b/components/cli/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/components/cli/container_commit_test.go b/components/cli/container_commit_test.go index 8f1b58be81..a844675368 100644 --- a/components/cli/container_commit_test.go +++ b/components/cli/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/components/cli/interface.go b/components/cli/interface.go index 0575ce5c3b..2a355fa8ad 100644 --- a/components/cli/interface.go +++ b/components/cli/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 4a62b2cc8edb102c143c15a71c292bcbea458cac Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 18 Oct 2016 17:35:45 -0700 Subject: [PATCH 246/978] Generate container update response from swagger spec. Signed-off-by: Daniel Nephin Upstream-commit: 598e3a4874e28ecdc53b0993011125dde1052ace Component: cli --- components/cli/container_update.go | 5 ++--- components/cli/container_update_test.go | 3 +-- components/cli/interface.go | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/components/cli/container_update.go b/components/cli/container_update.go index 48b75bee30..5082f22dfa 100644 --- a/components/cli/container_update.go +++ b/components/cli/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/components/cli/container_update_test.go b/components/cli/container_update_test.go index e151637a2b..715bb7ca23 100644 --- a/components/cli/container_update_test.go +++ b/components/cli/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/components/cli/interface.go b/components/cli/interface.go index 2a355fa8ad..b303d2fde8 100644 --- a/components/cli/interface.go +++ b/components/cli/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 5175d100a266eef6bfc3d58c6a59f4cee54a78f4 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 14 Oct 2016 16:28:47 -0400 Subject: [PATCH 247/978] Generate container create response from swagger spec. Signed-off-by: Daniel Nephin Upstream-commit: e7e083770259c805cc58b059ac2988e855c6559e Component: cli --- components/cli/command/container/create.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/container/create.go b/components/cli/command/container/create.go index 7bd3856971..7dc644d28c 100644 --- a/components/cli/command/container/create.go +++ b/components/cli/command/container/create.go @@ -148,7 +148,7 @@ func newCIDFile(path string) (*cidFile, error) { return &cidFile{path: path, file: f}, nil } -func createContainer(ctx context.Context, dockerCli *command.DockerCli, config *container.Config, hostConfig *container.HostConfig, networkingConfig *networktypes.NetworkingConfig, cidfile, name string) (*types.ContainerCreateResponse, error) { +func createContainer(ctx context.Context, dockerCli *command.DockerCli, config *container.Config, hostConfig *container.HostConfig, networkingConfig *networktypes.NetworkingConfig, cidfile, name string) (*container.ContainerCreateCreatedBody, error) { stderr := dockerCli.Err() var containerIDFile *cidFile From e10d258b88ff0c33b8470c1cb58570d2ffa6b6ee Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 18 Oct 2016 17:52:46 -0700 Subject: [PATCH 248/978] generate AuthResponse type from swagger spec. Signed-off-by: Daniel Nephin Upstream-commit: ca7404a80acd68cc8aeebdaed24914c5aa481cd0 Component: cli --- components/cli/interface.go | 2 +- components/cli/login.go | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/components/cli/interface.go b/components/cli/interface.go index b303d2fde8..f044c32352 100644 --- a/components/cli/interface.go +++ b/components/cli/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/components/cli/login.go b/components/cli/login.go index d8d277ccba..600dc7196f 100644 --- a/components/cli/login.go +++ b/components/cli/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 fe94d78afa45784695a372f91040ccec3d64ef19 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 20 Oct 2016 15:56:27 -0700 Subject: [PATCH 249/978] Generate ContainerWait response from the swagger spec. Signed-off-by: Daniel Nephin Upstream-commit: 85a0bd062de0d3dc0bcd3b7082feca678e6dd946 Component: cli --- components/cli/container_wait.go | 6 +++--- components/cli/container_wait_test.go | 4 ++-- components/cli/interface.go | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/components/cli/container_wait.go b/components/cli/container_wait.go index 8a858f0ea3..93212c70ee 100644 --- a/components/cli/container_wait.go +++ b/components/cli/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/components/cli/container_wait_test.go b/components/cli/container_wait_test.go index dab5acbdd3..9300bc0a54 100644 --- a/components/cli/container_wait_test.go +++ b/components/cli/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/components/cli/interface.go b/components/cli/interface.go index f044c32352..a78cb759cd 100644 --- a/components/cli/interface.go +++ b/components/cli/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 7d5d39c4f3d6d648ed1c455b82c34fca3f5e93cf Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 31 Oct 2016 12:39:38 -0400 Subject: [PATCH 250/978] Refactor client/request Signed-off-by: Daniel Nephin Upstream-commit: 5f066ed250180f2a66454397671d1ddb8b6f73a6 Component: cli --- components/cli/hijack.go | 7 +++- components/cli/request.go | 78 ++++++++++++++++++++++----------------- 2 files changed, 49 insertions(+), 36 deletions(-) diff --git a/components/cli/hijack.go b/components/cli/hijack.go index dededb7af2..74c53f52b3 100644 --- a/components/cli/hijack.go +++ b/components/cli/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/components/cli/request.go b/components/cli/request.go index 91a05824e1..c73464b54d 100644 --- a/components/cli/request.go +++ b/components/cli/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 1b6c8e9cf6ceb345bb18f398a91fc0ee65e479dd Mon Sep 17 00:00:00 2001 From: yupeng Date: Tue, 1 Nov 2016 11:07:31 +0800 Subject: [PATCH 251/978] Align with other cli descriptions Signed-off-by: yupeng Upstream-commit: 89db77511cce3518ea5f057c4da32c6dd919f816 Component: cli --- components/cli/command/registry/login.go | 2 +- components/cli/command/registry/logout.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/command/registry/login.go b/components/cli/command/registry/login.go index d6f7f8f1d1..7b29cfdb29 100644 --- a/components/cli/command/registry/login.go +++ b/components/cli/command/registry/login.go @@ -23,7 +23,7 @@ func NewLoginCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := &cobra.Command{ Use: "login [OPTIONS] [SERVER]", - Short: "Log in to a Docker registry.", + Short: "Log in to a Docker registry", Long: "Log in to a Docker registry.\nIf no server is specified, the default is defined by the daemon.", Args: cli.RequiresMaxArgs(1), RunE: func(cmd *cobra.Command, args []string) error { diff --git a/components/cli/command/registry/logout.go b/components/cli/command/registry/logout.go index a735818049..8e820dcc8c 100644 --- a/components/cli/command/registry/logout.go +++ b/components/cli/command/registry/logout.go @@ -15,7 +15,7 @@ import ( func NewLogoutCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := &cobra.Command{ Use: "logout [SERVER]", - Short: "Log out from a Docker registry.", + Short: "Log out from a Docker registry", Long: "Log out from a Docker registry.\nIf no server is specified, the default is defined by the daemon.", Args: cli.RequiresMaxArgs(1), RunE: func(cmd *cobra.Command, args []string) error { From a70e23cbcdaf40a9e1e249fadb78e6cbabdbe18a Mon Sep 17 00:00:00 2001 From: yupeng Date: Tue, 1 Nov 2016 11:07:31 +0800 Subject: [PATCH 252/978] Align with other cli descriptions Signed-off-by: yupeng Upstream-commit: f1b1e55f7a111ad7d67dc4c15ffba485a83332ba Component: cli --- components/cli/docker.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/docker.go b/components/cli/docker.go index e01e4ba5c7..53e14f7ec3 100644 --- a/components/cli/docker.go +++ b/components/cli/docker.go @@ -23,7 +23,7 @@ func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := &cobra.Command{ Use: "docker [OPTIONS] COMMAND [arg...]", - Short: "A self-sufficient runtime for containers.", + Short: "A self-sufficient runtime for containers", SilenceUsage: true, SilenceErrors: true, TraverseChildren: true, From c7196b39684d5b8272285be04e1ceb4a8c57d7a9 Mon Sep 17 00:00:00 2001 From: Zhang Wei Date: Tue, 1 Nov 2016 22:01:16 +0800 Subject: [PATCH 253/978] 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 Upstream-commit: d121e14ccded94cf01c21a7a22e7e43f91fd9838 Component: cli --- components/cli/container_list.go | 4 ++-- components/cli/container_list_test.go | 8 ++++---- components/cli/node_list.go | 4 ++-- components/cli/node_list_test.go | 2 +- components/cli/service_list.go | 4 ++-- components/cli/service_list_test.go | 2 +- components/cli/task_list.go | 4 ++-- components/cli/task_list_test.go | 2 +- 8 files changed, 15 insertions(+), 15 deletions(-) diff --git a/components/cli/container_list.go b/components/cli/container_list.go index a8945d84f1..4398912197 100644 --- a/components/cli/container_list.go +++ b/components/cli/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/components/cli/container_list_test.go b/components/cli/container_list_test.go index 5068b7573e..e41c6874b5 100644 --- a/components/cli/container_list_test.go +++ b/components/cli/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/components/cli/node_list.go b/components/cli/node_list.go index 0716875ccc..3e8440f08e 100644 --- a/components/cli/node_list.go +++ b/components/cli/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/components/cli/node_list_test.go b/components/cli/node_list_test.go index 1b3b35f357..0251b5cce4 100644 --- a/components/cli/node_list_test.go +++ b/components/cli/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/components/cli/service_list.go b/components/cli/service_list.go index 4ebc9f3011..c29e6d407d 100644 --- a/components/cli/service_list.go +++ b/components/cli/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/components/cli/service_list_test.go b/components/cli/service_list_test.go index 728187919f..213981ef70 100644 --- a/components/cli/service_list_test.go +++ b/components/cli/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/components/cli/task_list.go b/components/cli/task_list.go index 07c8324c83..66324da959 100644 --- a/components/cli/task_list.go +++ b/components/cli/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/components/cli/task_list_test.go b/components/cli/task_list_test.go index 2d9b812bc2..2a9a4c4346 100644 --- a/components/cli/task_list_test.go +++ b/components/cli/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 ca8b6d0b28a2415b004a2ff7bcf7fd1c1314a2e6 Mon Sep 17 00:00:00 2001 From: Zhang Wei Date: Tue, 1 Nov 2016 22:01:16 +0800 Subject: [PATCH 254/978] 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 Upstream-commit: 9eceaa926f7111a4aaaeb99ca82bf2b2a83beacb Component: cli --- components/cli/command/container/list.go | 8 ++++---- components/cli/command/container/ps_test.go | 4 ++-- components/cli/command/node/list.go | 2 +- components/cli/command/node/ps.go | 2 +- components/cli/command/service/list.go | 4 ++-- components/cli/command/service/ps.go | 2 +- components/cli/command/stack/common.go | 2 +- components/cli/command/stack/list.go | 2 +- components/cli/command/stack/ps.go | 2 +- components/cli/command/stack/services.go | 4 ++-- 10 files changed, 16 insertions(+), 16 deletions(-) diff --git a/components/cli/command/container/list.go b/components/cli/command/container/list.go index 2d46b6604e..80de7c5ff4 100644 --- a/components/cli/command/container/list.go +++ b/components/cli/command/container/list.go @@ -79,10 +79,10 @@ func (p *preProcessor) Networks() bool { func buildContainerListOptions(opts *psOptions) (*types.ContainerListOptions, error) { options := &types.ContainerListOptions{ - All: opts.all, - Limit: opts.last, - Size: opts.size, - Filter: opts.filter.Value(), + All: opts.all, + Limit: opts.last, + Size: opts.size, + Filters: opts.filter.Value(), } if opts.nLatest && opts.last == -1 { diff --git a/components/cli/command/container/ps_test.go b/components/cli/command/container/ps_test.go index dafdcdf905..9df4dfd5fa 100644 --- a/components/cli/command/container/ps_test.go +++ b/components/cli/command/container/ps_test.go @@ -55,10 +55,10 @@ func TestBuildContainerListOptions(t *testing.T) { assert.Equal(t, c.expectedAll, options.All) assert.Equal(t, c.expectedSize, options.Size) assert.Equal(t, c.expectedLimit, options.Limit) - assert.Equal(t, options.Filter.Len(), len(c.expectedFilters)) + assert.Equal(t, options.Filters.Len(), len(c.expectedFilters)) for k, v := range c.expectedFilters { - f := options.Filter + f := options.Filters if !f.ExactMatch(k, v) { t.Fatalf("Expected filter with key %s to be %s but got %s", k, v, f.Get(k)) } diff --git a/components/cli/command/node/list.go b/components/cli/command/node/list.go index d028d19147..9cacdcf441 100644 --- a/components/cli/command/node/list.go +++ b/components/cli/command/node/list.go @@ -50,7 +50,7 @@ func runList(dockerCli *command.DockerCli, opts listOptions) error { nodes, err := client.NodeList( ctx, - types.NodeListOptions{Filter: opts.filter.Value()}) + types.NodeListOptions{Filters: opts.filter.Value()}) if err != nil { return err } diff --git a/components/cli/command/node/ps.go b/components/cli/command/node/ps.go index 607488f35e..a034721d24 100644 --- a/components/cli/command/node/ps.go +++ b/components/cli/command/node/ps.go @@ -72,7 +72,7 @@ func runPs(dockerCli *command.DockerCli, opts psOptions) error { filter := opts.filter.Value() filter.Add("node", node.ID) - nodeTasks, err := client.TaskList(ctx, types.TaskListOptions{Filter: filter}) + nodeTasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: filter}) if err != nil { errs = append(errs, err.Error()) continue diff --git a/components/cli/command/service/list.go b/components/cli/command/service/list.go index 2278643fbc..4db5618798 100644 --- a/components/cli/command/service/list.go +++ b/components/cli/command/service/list.go @@ -51,7 +51,7 @@ func runList(dockerCli *command.DockerCli, opts listOptions) error { client := dockerCli.Client() out := dockerCli.Out() - services, err := client.ServiceList(ctx, types.ServiceListOptions{Filter: opts.filter.Value()}) + services, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: opts.filter.Value()}) if err != nil { return err } @@ -63,7 +63,7 @@ func runList(dockerCli *command.DockerCli, opts listOptions) error { taskFilter.Add("service", service.ID) } - tasks, err := client.TaskList(ctx, types.TaskListOptions{Filter: taskFilter}) + tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: taskFilter}) if err != nil { return err } diff --git a/components/cli/command/service/ps.go b/components/cli/command/service/ps.go index 55f837ba8e..cf94ad7374 100644 --- a/components/cli/command/service/ps.go +++ b/components/cli/command/service/ps.go @@ -64,7 +64,7 @@ func runPS(dockerCli *command.DockerCli, opts psOptions) error { } } - tasks, err := client.TaskList(ctx, types.TaskListOptions{Filter: filter}) + tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: filter}) if err != nil { return err } diff --git a/components/cli/command/stack/common.go b/components/cli/command/stack/common.go index 3e3a35faac..4776ec1b42 100644 --- a/components/cli/command/stack/common.go +++ b/components/cli/command/stack/common.go @@ -34,7 +34,7 @@ func getServices( ) ([]swarm.Service, error) { return apiclient.ServiceList( ctx, - types.ServiceListOptions{Filter: getStackFilter(namespace)}) + types.ServiceListOptions{Filters: getStackFilter(namespace)}) } func getNetworks( diff --git a/components/cli/command/stack/list.go b/components/cli/command/stack/list.go index 5d87cecb5f..f655b929ad 100644 --- a/components/cli/command/stack/list.go +++ b/components/cli/command/stack/list.go @@ -87,7 +87,7 @@ func getStacks( services, err := apiclient.ServiceList( ctx, - types.ServiceListOptions{Filter: filter}) + types.ServiceListOptions{Filters: filter}) if err != nil { return nil, err } diff --git a/components/cli/command/stack/ps.go b/components/cli/command/stack/ps.go index 2fff3de1fa..7a5e069cbe 100644 --- a/components/cli/command/stack/ps.go +++ b/components/cli/command/stack/ps.go @@ -56,7 +56,7 @@ func runPS(dockerCli *command.DockerCli, opts psOptions) error { filter.Add("desired-state", string(swarm.TaskStateAccepted)) } - tasks, err := client.TaskList(ctx, types.TaskListOptions{Filter: filter}) + tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: filter}) if err != nil { return err } diff --git a/components/cli/command/stack/services.go b/components/cli/command/stack/services.go index 50b50179de..1ca1c8c129 100644 --- a/components/cli/command/stack/services.go +++ b/components/cli/command/stack/services.go @@ -46,7 +46,7 @@ func runServices(dockerCli *command.DockerCli, opts servicesOptions) error { filter := opts.filter.Value() filter.Add("label", labelNamespace+"="+opts.namespace) - services, err := client.ServiceList(ctx, types.ServiceListOptions{Filter: filter}) + services, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: filter}) if err != nil { return err } @@ -67,7 +67,7 @@ func runServices(dockerCli *command.DockerCli, opts servicesOptions) error { taskFilter.Add("service", service.ID) } - tasks, err := client.TaskList(ctx, types.TaskListOptions{Filter: taskFilter}) + tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: taskFilter}) if err != nil { return err } From 6edad7997955e15b86e30ef7d21702ed88a6ee14 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 1 Nov 2016 09:12:27 -0700 Subject: [PATCH 255/978] Update deprecation versions for "email" and colon in "security options" These features were originally scheduled for removal in docker 1.13, but we changed our deprecation policy to keep features for three releases instead of two. This updates the deprecation version to match the deprecation policy. Signed-off-by: Sebastiaan van Stijn Upstream-commit: ac7d79389afeaeb9db3992059af62a75764c0815 Component: cli --- components/cli/command/registry/login.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/command/registry/login.go b/components/cli/command/registry/login.go index 7b29cfdb29..93e1b40e36 100644 --- a/components/cli/command/registry/login.go +++ b/components/cli/command/registry/login.go @@ -39,9 +39,9 @@ func NewLoginCommand(dockerCli *command.DockerCli) *cobra.Command { flags.StringVarP(&opts.user, "username", "u", "", "Username") flags.StringVarP(&opts.password, "password", "p", "", "Password") - // Deprecated in 1.11: Should be removed in docker 1.13 + // Deprecated in 1.11: Should be removed in docker 1.14 flags.StringVarP(&opts.email, "email", "e", "", "Email") - flags.MarkDeprecated("email", "will be removed in 1.13.") + flags.MarkDeprecated("email", "will be removed in 1.14.") return cmd } From fad42b5770ce7307e93c6b0a7797c16475956a21 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Thu, 21 Apr 2016 12:08:37 -0400 Subject: [PATCH 256/978] Adds ability to squash image after build Allow built images to be squash to scratch. Squashing does not destroy any images or layers, and preserves the build cache. Introduce a new CLI argument --squash to docker build Introduce a new param to the build API endpoint `squash` Once the build is complete, docker creates a new image loading the diffs from each layer into a single new layer and references all the parent's layers. Signed-off-by: Brian Goff Upstream-commit: b90c048804d3390f746fcceec9d7c43ba6570b74 Component: cli --- components/cli/command/image/build.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go index 7db76a649f..dc18601900 100644 --- a/components/cli/command/image/build.go +++ b/components/cli/command/image/build.go @@ -59,6 +59,7 @@ type buildOptions struct { compress bool securityOpt []string networkMode string + squash bool } // NewBuildCommand creates a new `docker build` command @@ -110,6 +111,10 @@ func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command { command.AddTrustedFlags(flags, true) + if dockerCli.HasExperimental() { + flags.BoolVar(&options.squash, "squash", false, "Squash newly built layers into a single new layer") + } + return cmd } @@ -305,6 +310,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { CacheFrom: options.cacheFrom, SecurityOpt: options.securityOpt, NetworkMode: options.networkMode, + Squash: options.squash, } response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions) From cf21756a8295f52c361e6893475bc2cb88e03cfe Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Wed, 2 Nov 2016 03:11:38 +0800 Subject: [PATCH 257/978] Remove some redundant consts Signed-off-by: yuexiao-wang Upstream-commit: 3acdab83fb9f666b46777783f393657368101c58 Component: cli --- components/cli/command/swarm/init.go | 9 +-------- components/cli/command/swarm/opts.go | 2 +- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/components/cli/command/swarm/init.go b/components/cli/command/swarm/init.go index 60fb8e8fe3..16f372f8d7 100644 --- a/components/cli/command/swarm/init.go +++ b/components/cli/command/swarm/init.go @@ -14,13 +14,6 @@ import ( "github.com/spf13/pflag" ) -const ( - generatedSecretEntropyBytes = 16 - generatedSecretBase = 36 - // floor(log(2^128-1, 36)) + 1 - maxGeneratedSecretLength = 25 -) - type initOptions struct { swarmOptions listenAddr NodeAddrOption @@ -46,7 +39,7 @@ func newInitCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() flags.Var(&opts.listenAddr, flagListenAddr, "Listen address (format: [:port])") flags.StringVar(&opts.advertiseAddr, flagAdvertiseAddr, "", "Advertised address (format: [:port])") - flags.BoolVar(&opts.forceNewCluster, "force-new-cluster", false, "Force create a new cluster from current state.") + flags.BoolVar(&opts.forceNewCluster, "force-new-cluster", false, "Force create a new cluster from current state") addSwarmFlags(flags, &opts.swarmOptions) return cmd } diff --git a/components/cli/command/swarm/opts.go b/components/cli/command/swarm/opts.go index 58330b7f8a..3659b55f81 100644 --- a/components/cli/command/swarm/opts.go +++ b/components/cli/command/swarm/opts.go @@ -33,7 +33,7 @@ type swarmOptions struct { externalCA ExternalCAOption } -// NodeAddrOption is a pflag.Value for listen and remote addresses +// NodeAddrOption is a pflag.Value for listening addresses type NodeAddrOption struct { addr string } From ba2bdb1e37865d95e0bd5bd9687908b2a214aea5 Mon Sep 17 00:00:00 2001 From: allencloud Date: Wed, 2 Nov 2016 15:53:18 +0800 Subject: [PATCH 258/978] add replicated in service scale command description Signed-off-by: allencloud Upstream-commit: 39e34ed1a39d491096b5dc3e29aa760e7f442bcb Component: cli --- components/cli/command/service/scale.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/service/scale.go b/components/cli/command/service/scale.go index 61b73bc354..ea30265bd7 100644 --- a/components/cli/command/service/scale.go +++ b/components/cli/command/service/scale.go @@ -16,7 +16,7 @@ import ( func newScaleCommand(dockerCli *command.DockerCli) *cobra.Command { return &cobra.Command{ Use: "scale SERVICE=REPLICAS [SERVICE=REPLICAS...]", - Short: "Scale one or multiple services", + Short: "Scale one or multiple replicated services", Args: scaleArgs, RunE: func(cmd *cobra.Command, args []string) error { return runScale(dockerCli, args) From 5da8376ad18d0dd4c7ab243d4806e0bbe4e1774c Mon Sep 17 00:00:00 2001 From: allencloud Date: Wed, 2 Nov 2016 17:22:04 +0800 Subject: [PATCH 259/978] node rm can be applied on not only active node Signed-off-by: allencloud Upstream-commit: 503053819eacd0c4ee7642ddb50561f40a8cbb49 Component: cli --- components/cli/command/node/remove.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/node/remove.go b/components/cli/command/node/remove.go index 9ba21b44a2..3b89db8661 100644 --- a/components/cli/command/node/remove.go +++ b/components/cli/command/node/remove.go @@ -29,7 +29,7 @@ func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { }, } flags := cmd.Flags() - flags.BoolVar(&opts.force, "force", false, "Force remove an active node") + flags.BoolVar(&opts.force, "force", false, "Force remove a node from the swarm") return cmd } From e35c329385d384416def4f639347964c5eafc0ca Mon Sep 17 00:00:00 2001 From: Antonio Murdaca Date: Fri, 2 Sep 2016 15:20:54 +0200 Subject: [PATCH 260/978] daemon: add a flag to override the default seccomp profile Signed-off-by: Antonio Murdaca Upstream-commit: 557db1ea688f0f5a0ac0722070f3bc414b856a51 Component: cli --- components/cli/command/system/info.go | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/components/cli/command/system/info.go b/components/cli/command/system/info.go index 0bfd9986d2..dfbc83d90a 100644 --- a/components/cli/command/system/info.go +++ b/components/cli/command/system/info.go @@ -140,9 +140,20 @@ func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error { } if info.OSType == "linux" { - fmt.Fprintf(dockerCli.Out(), "Security Options:") - ioutils.FprintfIfNotEmpty(dockerCli.Out(), " %s", strings.Join(info.SecurityOptions, " ")) - fmt.Fprintf(dockerCli.Out(), "\n") + if len(info.SecurityOptions) != 0 { + fmt.Fprintf(dockerCli.Out(), "Security Options:\n") + for _, o := range info.SecurityOptions { + switch o.Key { + case "Name": + fmt.Fprintf(dockerCli.Out(), " %s\n", o.Value) + case "Profile": + if o.Key != "default" { + fmt.Fprintf(dockerCli.Err(), " WARNING: You're not using the Docker's default seccomp profile\n") + } + fmt.Fprintf(dockerCli.Out(), " %s: %s\n", o.Key, o.Value) + } + } + } } // Isolation only has meaning on a Windows daemon. From f70d6e5d9c94071ca56a867923fac0e780722f2c Mon Sep 17 00:00:00 2001 From: Antonio Murdaca Date: Fri, 2 Sep 2016 15:20:54 +0200 Subject: [PATCH 261/978] daemon: add a flag to override the default seccomp profile Signed-off-by: Antonio Murdaca Upstream-commit: 485bb69238647a23ecf8066e32cfe404440818e3 Component: cli --- components/cli/info_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/components/cli/info_test.go b/components/cli/info_test.go index 79f23c8af2..7af82a8a31 100644 --- a/components/cli/info_test.go +++ b/components/cli/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 b5f18791bfa6e284ddd723fa909ffb92308f3b32 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Thu, 3 Nov 2016 07:20:46 +0100 Subject: [PATCH 262/978] Add support for Names and ID in stats format This adds support to display names or id of container instead of what was provided in the request. This keeps the default behavior (`docker stats byname` will display `byname` in the `CONTAINER` colmun and `docker stats byid` will display the id in the `CONTAINER` column) but adds two new format directive. Signed-off-by: Vincent Demeester Upstream-commit: 07f77b78eaf1474057142f4c9e63de04a57b9ab0 Component: cli --- .../cli/command/container/stats_helpers.go | 10 +++++---- components/cli/command/formatter/stats.go | 21 +++++++++++++++---- .../cli/command/formatter/stats_test.go | 10 ++++----- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/components/cli/command/container/stats_helpers.go b/components/cli/command/container/stats_helpers.go index 32ad84841b..8bc537ad3c 100644 --- a/components/cli/command/container/stats_helpers.go +++ b/components/cli/command/container/stats_helpers.go @@ -29,7 +29,7 @@ var daemonOSType string func (s *stats) add(cs *formatter.ContainerStats) bool { s.mu.Lock() defer s.mu.Unlock() - if _, exists := s.isKnownContainer(cs.Name); !exists { + if _, exists := s.isKnownContainer(cs.Container); !exists { s.cs = append(s.cs, cs) return true } @@ -46,7 +46,7 @@ func (s *stats) remove(id string) { func (s *stats) isKnownContainer(cid string) (int, bool) { for i, c := range s.cs { - if c.Name == cid { + if c.Container == cid { return i, true } } @@ -54,7 +54,7 @@ func (s *stats) isKnownContainer(cid string) (int, bool) { } func collect(s *formatter.ContainerStats, ctx context.Context, cli client.APIClient, streamStats bool, waitFirst *sync.WaitGroup) { - logrus.Debugf("collecting stats for %s", s.Name) + logrus.Debugf("collecting stats for %s", s.Container) var ( getFirst bool previousCPU uint64 @@ -70,7 +70,7 @@ func collect(s *formatter.ContainerStats, ctx context.Context, cli client.APICli } }() - response, err := cli.ContainerStats(ctx, s.Name, streamStats) + response, err := cli.ContainerStats(ctx, s.Container, streamStats) if err != nil { s.SetError(err) return @@ -125,6 +125,8 @@ func collect(s *formatter.ContainerStats, ctx context.Context, cli client.APICli } netRx, netTx := calculateNetwork(v.Networks) s.SetStatistics(formatter.StatsEntry{ + Name: v.Name, + ID: v.ID, CPUPercentage: cpuPercent, Memory: mem, MemoryPercentage: memPerc, diff --git a/components/cli/command/formatter/stats.go b/components/cli/command/formatter/stats.go index b2c972251f..7997f996d8 100644 --- a/components/cli/command/formatter/stats.go +++ b/components/cli/command/formatter/stats.go @@ -24,7 +24,9 @@ const ( // StatsEntry represents represents the statistics data collected from a container type StatsEntry struct { + Container string Name string + ID string CPUPercentage float64 Memory float64 // On Windows this is the private working set MemoryLimit float64 // Not used on Windows @@ -85,7 +87,7 @@ func (cs *ContainerStats) SetError(err error) { func (cs *ContainerStats) SetStatistics(s StatsEntry) { cs.mutex.Lock() defer cs.mutex.Unlock() - s.Name = cs.Name + s.Container = cs.Container s.OSType = cs.OSType cs.StatsEntry = s } @@ -109,9 +111,9 @@ func NewStatsFormat(source, osType string) Format { } // NewContainerStats returns a new ContainerStats entity and sets in it the given name -func NewContainerStats(name, osType string) *ContainerStats { +func NewContainerStats(container, osType string) *ContainerStats { return &ContainerStats{ - StatsEntry: StatsEntry{Name: name, OSType: osType}, + StatsEntry: StatsEntry{Container: container, OSType: osType}, } } @@ -138,7 +140,18 @@ type containerStatsContext struct { func (c *containerStatsContext) Container() string { c.AddHeader(containerHeader) - return c.s.Name + return c.s.Container +} + +func (c *containerStatsContext) Name() string { + c.AddHeader(nameHeader) + name := c.s.Name[1:] + return name +} + +func (c *containerStatsContext) ID() string { + c.AddHeader(containerIDHeader) + return c.s.ID } func (c *containerStatsContext) CPUPerc() string { diff --git a/components/cli/command/formatter/stats_test.go b/components/cli/command/formatter/stats_test.go index f1f449e71a..d5a17cc70e 100644 --- a/components/cli/command/formatter/stats_test.go +++ b/components/cli/command/formatter/stats_test.go @@ -18,7 +18,7 @@ func TestContainerStatsContext(t *testing.T) { expHeader string call func() string }{ - {StatsEntry{Name: containerID}, containerID, containerHeader, ctx.Container}, + {StatsEntry{Container: containerID}, containerID, containerHeader, ctx.Container}, {StatsEntry{CPUPercentage: 5.5}, "5.50%", cpuPercHeader, ctx.CPUPerc}, {StatsEntry{CPUPercentage: 5.5, IsInvalid: true}, "--", cpuPercHeader, ctx.CPUPerc}, {StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3}, "0.31 B / 12.3 B", netIOHeader, ctx.NetIO}, @@ -82,7 +82,7 @@ container2 -- for _, te := range tt { stats := []StatsEntry{ { - Name: "container1", + Container: "container1", CPUPercentage: 20, Memory: 20, MemoryLimit: 20, @@ -96,7 +96,7 @@ container2 -- OSType: "linux", }, { - Name: "container2", + Container: "container2", CPUPercentage: 30, Memory: 30, MemoryLimit: 30, @@ -150,7 +150,7 @@ container2 -- -- for _, te := range tt { stats := []StatsEntry{ { - Name: "container1", + Container: "container1", CPUPercentage: 20, Memory: 20, MemoryLimit: 20, @@ -164,7 +164,7 @@ container2 -- -- OSType: "windows", }, { - Name: "container2", + Container: "container2", CPUPercentage: 30, Memory: 30, MemoryLimit: 30, From 1e7ea7f047f661ed41f89bc42ff08f64d1d03cf1 Mon Sep 17 00:00:00 2001 From: milindchawre Date: Tue, 1 Nov 2016 13:09:18 +0000 Subject: [PATCH 263/978] Fixes #27798 : Update help for --blkio-weight parameter Signed-off-by: milindchawre Upstream-commit: ec7d9291b82d523533ad279fc1bbb4b463ed0e53 Component: cli --- components/cli/command/container/update.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/container/update.go b/components/cli/command/container/update.go index 5bacc9be75..75765856c5 100644 --- a/components/cli/command/container/update.go +++ b/components/cli/command/container/update.go @@ -50,7 +50,7 @@ func NewUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { } flags := cmd.Flags() - flags.Uint16Var(&opts.blkioWeight, "blkio-weight", 0, "Block IO (relative weight), between 10 and 1000") + flags.Uint16Var(&opts.blkioWeight, "blkio-weight", 0, "Block IO (relative weight), between 10 and 1000, or 0 to disable (default 0)") flags.Int64Var(&opts.cpuPeriod, "cpu-period", 0, "Limit CPU CFS (Completely Fair Scheduler) period") flags.Int64Var(&opts.cpuQuota, "cpu-quota", 0, "Limit CPU CFS (Completely Fair Scheduler) quota") flags.Int64Var(&opts.cpuRealtimePeriod, "cpu-rt-period", 0, "Limit the CPU real-time period in microseconds") From 01e1a7306eff0b5af87f561957e872030a6354f4 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Tue, 25 Oct 2016 03:26:54 +0000 Subject: [PATCH 264/978] cli: add `--mount` to `docker run` Signed-off-by: Akihiro Suda Upstream-commit: 51cb4aa7b8fea5216e2dd5b8771a465ede42ac15 Component: cli --- components/cli/command/service/create.go | 2 +- components/cli/command/service/opts.go | 141 +------------------ components/cli/command/service/opts_test.go | 146 -------------------- components/cli/command/service/update.go | 2 +- 4 files changed, 3 insertions(+), 288 deletions(-) diff --git a/components/cli/command/service/create.go b/components/cli/command/service/create.go index 28790ec8e6..92cf969b4b 100644 --- a/components/cli/command/service/create.go +++ b/components/cli/command/service/create.go @@ -35,7 +35,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Var(&opts.containerLabels, flagContainerLabel, "Container labels") flags.VarP(&opts.env, flagEnv, "e", "Set environment variables") flags.Var(&opts.envFile, flagEnvFile, "Read in a file of environment variables") - flags.Var(&opts.mounts, flagMount, "Attach a mount to the service") + flags.Var(&opts.mounts, flagMount, "Attach a filesystem mount to the service") flags.StringSliceVar(&opts.constraints, flagConstraint, []string{}, "Placement constraints") flags.StringSliceVar(&opts.networks, flagNetwork, []string{}, "Network attachments") flags.VarP(&opts.endpoint.ports, flagPublish, "p", "Publish a port as a node port") diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index 43b7b671cb..358185c0b4 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -1,7 +1,6 @@ package service import ( - "encoding/csv" "fmt" "math/big" "strconv" @@ -9,7 +8,6 @@ import ( "time" "github.com/docker/docker/api/types/container" - mounttypes "github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/opts" runconfigopts "github.com/docker/docker/runconfig/opts" @@ -149,143 +147,6 @@ func (i *Uint64Opt) Value() *uint64 { return i.value } -// MountOpt is a Value type for parsing mounts -type MountOpt struct { - values []mounttypes.Mount -} - -// Set a new mount value -func (m *MountOpt) Set(value string) error { - csvReader := csv.NewReader(strings.NewReader(value)) - fields, err := csvReader.Read() - if err != nil { - return err - } - - mount := mounttypes.Mount{} - - volumeOptions := func() *mounttypes.VolumeOptions { - if mount.VolumeOptions == nil { - mount.VolumeOptions = &mounttypes.VolumeOptions{ - Labels: make(map[string]string), - } - } - if mount.VolumeOptions.DriverConfig == nil { - mount.VolumeOptions.DriverConfig = &mounttypes.Driver{} - } - return mount.VolumeOptions - } - - bindOptions := func() *mounttypes.BindOptions { - if mount.BindOptions == nil { - mount.BindOptions = new(mounttypes.BindOptions) - } - return mount.BindOptions - } - - setValueOnMap := func(target map[string]string, value string) { - parts := strings.SplitN(value, "=", 2) - if len(parts) == 1 { - target[value] = "" - } else { - target[parts[0]] = parts[1] - } - } - - mount.Type = mounttypes.TypeVolume // default to volume mounts - // Set writable as the default - for _, field := range fields { - parts := strings.SplitN(field, "=", 2) - key := strings.ToLower(parts[0]) - - if len(parts) == 1 { - switch key { - case "readonly", "ro": - mount.ReadOnly = true - continue - case "volume-nocopy": - volumeOptions().NoCopy = true - continue - } - } - - if len(parts) != 2 { - return fmt.Errorf("invalid field '%s' must be a key=value pair", field) - } - - value := parts[1] - switch key { - case "type": - mount.Type = mounttypes.Type(strings.ToLower(value)) - case "source", "src": - mount.Source = value - case "target", "dst", "destination": - mount.Target = value - case "readonly", "ro": - mount.ReadOnly, err = strconv.ParseBool(value) - if err != nil { - return fmt.Errorf("invalid value for %s: %s", key, value) - } - case "bind-propagation": - bindOptions().Propagation = mounttypes.Propagation(strings.ToLower(value)) - case "volume-nocopy": - volumeOptions().NoCopy, err = strconv.ParseBool(value) - if err != nil { - return fmt.Errorf("invalid value for populate: %s", value) - } - case "volume-label": - setValueOnMap(volumeOptions().Labels, value) - case "volume-driver": - volumeOptions().DriverConfig.Name = value - case "volume-opt": - if volumeOptions().DriverConfig.Options == nil { - volumeOptions().DriverConfig.Options = make(map[string]string) - } - setValueOnMap(volumeOptions().DriverConfig.Options, value) - default: - return fmt.Errorf("unexpected key '%s' in '%s'", key, field) - } - } - - if mount.Type == "" { - return fmt.Errorf("type is required") - } - - if mount.Target == "" { - return fmt.Errorf("target is required") - } - - if mount.Type == mounttypes.TypeBind && mount.VolumeOptions != nil { - return fmt.Errorf("cannot mix 'volume-*' options with mount type '%s'", mounttypes.TypeBind) - } - if mount.Type == mounttypes.TypeVolume && mount.BindOptions != nil { - return fmt.Errorf("cannot mix 'bind-*' options with mount type '%s'", mounttypes.TypeVolume) - } - - m.values = append(m.values, mount) - return nil -} - -// Type returns the type of this option -func (m *MountOpt) Type() string { - return "mount" -} - -// String returns a string repr of this option -func (m *MountOpt) String() string { - mounts := []string{} - for _, mount := range m.values { - repr := fmt.Sprintf("%s %s %s", mount.Type, mount.Source, mount.Target) - mounts = append(mounts, repr) - } - return strings.Join(mounts, ", ") -} - -// Value returns the mounts -func (m *MountOpt) Value() []mounttypes.Mount { - return m.values -} - type updateOptions struct { parallelism uint64 delay time.Duration @@ -460,7 +321,7 @@ type serviceOptions struct { workdir string user string groups []string - mounts MountOpt + mounts opts.MountOpt resources resourceOptions stopGrace DurationOpt diff --git a/components/cli/command/service/opts_test.go b/components/cli/command/service/opts_test.go index 52016cbfc5..26534cf0f5 100644 --- a/components/cli/command/service/opts_test.go +++ b/components/cli/command/service/opts_test.go @@ -6,7 +6,6 @@ import ( "time" "github.com/docker/docker/api/types/container" - mounttypes "github.com/docker/docker/api/types/mount" "github.com/docker/docker/pkg/testutil/assert" ) @@ -68,151 +67,6 @@ func TestUint64OptSetAndValue(t *testing.T) { assert.Equal(t, *opt.Value(), uint64(14445)) } -func TestMountOptString(t *testing.T) { - mount := MountOpt{ - values: []mounttypes.Mount{ - { - Type: mounttypes.TypeBind, - Source: "/home/path", - Target: "/target", - }, - { - Type: mounttypes.TypeVolume, - Source: "foo", - Target: "/target/foo", - }, - }, - } - expected := "bind /home/path /target, volume foo /target/foo" - assert.Equal(t, mount.String(), expected) -} - -func TestMountOptSetBindNoErrorBind(t *testing.T) { - for _, testcase := range []string{ - // tests several aliases that should have same result. - "type=bind,target=/target,source=/source", - "type=bind,src=/source,dst=/target", - "type=bind,source=/source,dst=/target", - "type=bind,src=/source,target=/target", - } { - var mount MountOpt - - assert.NilError(t, mount.Set(testcase)) - - mounts := mount.Value() - assert.Equal(t, len(mounts), 1) - assert.Equal(t, mounts[0], mounttypes.Mount{ - Type: mounttypes.TypeBind, - Source: "/source", - Target: "/target", - }) - } -} - -func TestMountOptSetVolumeNoError(t *testing.T) { - for _, testcase := range []string{ - // tests several aliases that should have same result. - "type=volume,target=/target,source=/source", - "type=volume,src=/source,dst=/target", - "type=volume,source=/source,dst=/target", - "type=volume,src=/source,target=/target", - } { - var mount MountOpt - - assert.NilError(t, mount.Set(testcase)) - - mounts := mount.Value() - assert.Equal(t, len(mounts), 1) - assert.Equal(t, mounts[0], mounttypes.Mount{ - Type: mounttypes.TypeVolume, - Source: "/source", - Target: "/target", - }) - } -} - -// TestMountOptDefaultType ensures that a mount without the type defaults to a -// volume mount. -func TestMountOptDefaultType(t *testing.T) { - var mount MountOpt - assert.NilError(t, mount.Set("target=/target,source=/foo")) - assert.Equal(t, mount.values[0].Type, mounttypes.TypeVolume) -} - -func TestMountOptSetErrorNoTarget(t *testing.T) { - var mount MountOpt - assert.Error(t, mount.Set("type=volume,source=/foo"), "target is required") -} - -func TestMountOptSetErrorInvalidKey(t *testing.T) { - var mount MountOpt - assert.Error(t, mount.Set("type=volume,bogus=foo"), "unexpected key 'bogus'") -} - -func TestMountOptSetErrorInvalidField(t *testing.T) { - var mount MountOpt - assert.Error(t, mount.Set("type=volume,bogus"), "invalid field 'bogus'") -} - -func TestMountOptSetErrorInvalidReadOnly(t *testing.T) { - var mount MountOpt - assert.Error(t, mount.Set("type=volume,readonly=no"), "invalid value for readonly: no") - assert.Error(t, mount.Set("type=volume,readonly=invalid"), "invalid value for readonly: invalid") -} - -func TestMountOptDefaultEnableReadOnly(t *testing.T) { - var m MountOpt - assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo")) - assert.Equal(t, m.values[0].ReadOnly, false) - - m = MountOpt{} - assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly")) - assert.Equal(t, m.values[0].ReadOnly, true) - - m = MountOpt{} - assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=1")) - assert.Equal(t, m.values[0].ReadOnly, true) - - m = MountOpt{} - assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=true")) - assert.Equal(t, m.values[0].ReadOnly, true) - - m = MountOpt{} - assert.NilError(t, m.Set("type=bind,target=/foo,source=/foo,readonly=0")) - assert.Equal(t, m.values[0].ReadOnly, false) -} - -func TestMountOptVolumeNoCopy(t *testing.T) { - var m MountOpt - assert.NilError(t, m.Set("type=volume,target=/foo,volume-nocopy")) - assert.Equal(t, m.values[0].Source, "") - - m = MountOpt{} - assert.NilError(t, m.Set("type=volume,target=/foo,source=foo")) - assert.Equal(t, m.values[0].VolumeOptions == nil, true) - - m = MountOpt{} - assert.NilError(t, m.Set("type=volume,target=/foo,source=foo,volume-nocopy=true")) - assert.Equal(t, m.values[0].VolumeOptions != nil, true) - assert.Equal(t, m.values[0].VolumeOptions.NoCopy, true) - - m = MountOpt{} - assert.NilError(t, m.Set("type=volume,target=/foo,source=foo,volume-nocopy")) - assert.Equal(t, m.values[0].VolumeOptions != nil, true) - assert.Equal(t, m.values[0].VolumeOptions.NoCopy, true) - - m = MountOpt{} - assert.NilError(t, m.Set("type=volume,target=/foo,source=foo,volume-nocopy=1")) - assert.Equal(t, m.values[0].VolumeOptions != nil, true) - assert.Equal(t, m.values[0].VolumeOptions.NoCopy, true) -} - -func TestMountOptTypeConflict(t *testing.T) { - var m MountOpt - assert.Error(t, m.Set("type=bind,target=/foo,source=/foo,volume-nocopy=true"), "cannot mix") - assert.Error(t, m.Set("type=volume,target=/foo,source=/foo,bind-propagation=rprivate"), "cannot mix") -} - func TestHealthCheckOptionsToHealthConfig(t *testing.T) { dur := time.Second opt := healthCheckOptions{ diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index b76a20e97c..a9aa9c9987 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -404,7 +404,7 @@ func removeItems( func updateMounts(flags *pflag.FlagSet, mounts *[]mounttypes.Mount) { if flags.Changed(flagMountAdd) { - values := flags.Lookup(flagMountAdd).Value.(*MountOpt).Value() + values := flags.Lookup(flagMountAdd).Value.(*opts.MountOpt).Value() *mounts = append(*mounts, values...) } toRemove := buildToRemoveSet(flags, flagMountRemove) From 9e5c08ce2b9277bddaca224f76cf0e7692a2f778 Mon Sep 17 00:00:00 2001 From: yupeng Date: Thu, 3 Nov 2016 15:47:58 +0800 Subject: [PATCH 265/978] Add for String Signed-off-by: yupeng Upstream-commit: cd46f069343b4514f98936410a8d95121f395cef Component: cli --- components/cli/flags/common.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/flags/common.go b/components/cli/flags/common.go index f40808ca03..690e8da4b8 100644 --- a/components/cli/flags/common.go +++ b/components/cli/flags/common.go @@ -53,7 +53,7 @@ func (commonOpts *CommonOptions) InstallFlags(flags *pflag.FlagSet) { } flags.BoolVarP(&commonOpts.Debug, "debug", "D", false, "Enable debug mode") - flags.StringVarP(&commonOpts.LogLevel, "log-level", "l", "info", "Set the logging level (debug, info, warn, error, fatal)") + flags.StringVarP(&commonOpts.LogLevel, "log-level", "l", "info", "Set the logging level (\"debug\", \"info\", \"warn\", \"error\", \"fatal\")") flags.BoolVar(&commonOpts.TLS, "tls", false, "Use TLS; implied by --tlsverify") flags.BoolVar(&commonOpts.TLSVerify, FlagTLSVerify, dockerTLSVerify, "Use TLS and verify the remote") From 2cef813aa22154ea512bebc0d786666d74dbdf21 Mon Sep 17 00:00:00 2001 From: Nikolay Milovanov Date: Thu, 27 Oct 2016 12:44:19 +0100 Subject: [PATCH 266/978] Adding the hostname option to docker service command Signed-off-by: Nikolay Milovanov Upstream-commit: 19b7bc17395a954841532206708cf319b5f9fbc0 Component: cli --- components/cli/command/service/create.go | 1 + components/cli/command/service/opts.go | 3 +++ 2 files changed, 4 insertions(+) diff --git a/components/cli/command/service/create.go b/components/cli/command/service/create.go index 28790ec8e6..430d448565 100644 --- a/components/cli/command/service/create.go +++ b/components/cli/command/service/create.go @@ -33,6 +33,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.VarP(&opts.labels, flagLabel, "l", "Service labels") flags.Var(&opts.containerLabels, flagContainerLabel, "Container labels") + flags.StringVar(&opts.hostname, flagHostname, "", "Container hostname") flags.VarP(&opts.env, flagEnv, "e", "Set environment variables") flags.Var(&opts.envFile, flagEnvFile, "Read in a file of environment variables") flags.Var(&opts.mounts, flagMount, "Attach a mount to the service") diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index 43b7b671cb..a134327356 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -455,6 +455,7 @@ type serviceOptions struct { containerLabels opts.ListOpts image string args []string + hostname string env opts.ListOpts envFile opts.ListOpts workdir string @@ -526,6 +527,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { Image: opts.image, Args: opts.args, Env: currentEnv, + Hostname: opts.hostname, Labels: runconfigopts.ConvertKVStringsToMap(opts.containerLabels.GetAll()), Dir: opts.workdir, User: opts.user, @@ -625,6 +627,7 @@ const ( flagContainerLabelRemove = "container-label-rm" flagContainerLabelAdd = "container-label-add" flagEndpointMode = "endpoint-mode" + flagHostname = "hostname" flagEnv = "env" flagEnvFile = "env-file" flagEnvRemove = "env-rm" From f4a9cbc8ae13dc61d3d2b74e6071c8d1390b0f83 Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Tue, 1 Nov 2016 22:58:26 +0800 Subject: [PATCH 267/978] Update for docker volume create Signed-off-by: yuexiao-wang Upstream-commit: 816560ffe907a5af5a91e6e21077b6ef2118cc53 Component: cli --- components/cli/command/volume/cmd.go | 4 ++-- components/cli/command/volume/list.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/cli/command/volume/cmd.go b/components/cli/command/volume/cmd.go index 5f39d3cf33..f35181ffaf 100644 --- a/components/cli/command/volume/cmd.go +++ b/components/cli/command/volume/cmd.go @@ -36,13 +36,13 @@ volume is a specially-designated directory that by-passes storage driver management. Data volumes persist data independent of a container's life cycle. When you -delete a container, the Engine daemon does not delete any data volumes. You can +delete a container, the Docker daemon does not delete any data volumes. You can share volumes across multiple containers. Moreover, you can share data volumes with other computing resources in your system. To see help for a subcommand, use: - docker volume CMD help + docker volume COMMAND --help For full details on using docker volume visit Docker's online documentation. diff --git a/components/cli/command/volume/list.go b/components/cli/command/volume/list.go index 77ce359771..d76006a1b2 100644 --- a/components/cli/command/volume/list.go +++ b/components/cli/command/volume/list.go @@ -76,7 +76,7 @@ func runList(dockerCli *command.DockerCli, opts listOptions) error { var listDescription = ` -Lists all the volumes Docker knows about. You can filter using the **-f** or +Lists all the volumes Docker manages. You can filter using the **-f** or **--filter** flag. The filtering format is a **key=value** pair. To specify more than one filter, pass multiple flags (for example, **--filter "foo=bar" --filter "bif=baz"**) From 4e0dd7ba319cd46cad73c59ef40ab2d5ed6a0754 Mon Sep 17 00:00:00 2001 From: Drew Erny Date: Mon, 24 Oct 2016 16:11:25 -0700 Subject: [PATCH 268/978] added node ip autodetection Manager now auto-detects the address that an agent connects to the cluster from and stores it. This is useful for many kinds of internal cluster management tools. Signed-off-by: Drew Erny Upstream-commit: 2cd40280245b3afe60b472253baeefd2004c2f3e Component: cli --- components/cli/command/node/inspect.go | 1 + 1 file changed, 1 insertion(+) diff --git a/components/cli/command/node/inspect.go b/components/cli/command/node/inspect.go index 0812ec5eab..fde70185f8 100644 --- a/components/cli/command/node/inspect.go +++ b/components/cli/command/node/inspect.go @@ -95,6 +95,7 @@ func printNode(out io.Writer, node swarm.Node) { fmt.Fprintf(out, " State:\t\t\t%s\n", command.PrettyPrint(node.Status.State)) ioutils.FprintfIfNotEmpty(out, " Message:\t\t%s\n", command.PrettyPrint(node.Status.Message)) fmt.Fprintf(out, " Availability:\t\t%s\n", command.PrettyPrint(node.Spec.Availability)) + ioutils.FprintfIfNotEmpty(out, " Address:\t\t%s\n", command.PrettyPrint(node.Status.Addr)) if node.ManagerStatus != nil { fmt.Fprintln(out, "Manager Status:") From a3d34230054ee085830ef890e93727faaf594037 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Thu, 3 Nov 2016 10:33:17 -0700 Subject: [PATCH 269/978] fix double [y/N] in container prune Signed-off-by: Victor Vieux Upstream-commit: 3014d36cd91d85c741b921926de9b8eb27e75d7d Component: cli --- components/cli/command/container/prune.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/container/prune.go b/components/cli/command/container/prune.go index 679471398a..99a97f6cd8 100644 --- a/components/cli/command/container/prune.go +++ b/components/cli/command/container/prune.go @@ -44,7 +44,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { } const warning = `WARNING! This will remove all stopped containers. -Are you sure you want to continue? [y/N] ` +Are you sure you want to continue?` func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed uint64, output string, err error) { if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) { From 40f9852dd3a306430e39cdc6c4634f97067b6f84 Mon Sep 17 00:00:00 2001 From: Vincent Bernat Date: Thu, 3 Nov 2016 20:46:28 +0100 Subject: [PATCH 270/978] cli: shorten description of "inspect" subcommand The short description should be kept short. Spanning on several lines is a bit ugly. A user can still get more information in the manual or we can expand the long description instead if we want (there is currently none). This reverts a bit of #26683. Signed-off-by: Vincent Bernat Upstream-commit: 0e6f4e7cdaa1881faea242a6f85e7bc330c3ac4f Component: cli --- components/cli/command/system/inspect.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/components/cli/command/system/inspect.go b/components/cli/command/system/inspect.go index 8732c467eb..a403685ee7 100644 --- a/components/cli/command/system/inspect.go +++ b/components/cli/command/system/inspect.go @@ -25,12 +25,9 @@ func NewInspectCommand(dockerCli *command.DockerCli) *cobra.Command { var opts inspectOptions cmd := &cobra.Command{ - Use: "inspect [OPTIONS] NAME|ID [NAME|ID...]", - Short: strings.Join([]string{ - "Return low-level information on Docker object(s) (e.g. container, image, volume,", - "\nnetwork, node, service, or task) identified by name or ID", - }, ""), - Args: cli.RequiresMinArgs(1), + Use: "inspect [OPTIONS] NAME|ID [NAME|ID...]", + Short: "Return low-level information on Docker objects", + Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.ids = args return runInspect(dockerCli, opts) From 25608ce8d42ce7878cf6636e1785244f2a1eb934 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Wed, 2 Nov 2016 17:43:32 -0700 Subject: [PATCH 271/978] always add but hide experimental cmds and flags Signed-off-by: Victor Vieux Upstream-commit: eb522dac241ce3b12af9293e7bfbfd10d65f2346 Component: cli --- components/cli/command/cli.go | 2 +- components/cli/command/commands/commands.go | 13 ++++--------- components/cli/command/container/start.go | 9 ++++----- components/cli/command/image/build.go | 5 ++--- 4 files changed, 11 insertions(+), 18 deletions(-) diff --git a/components/cli/command/cli.go b/components/cli/command/cli.go index be82ecf6f3..9b61492442 100644 --- a/components/cli/command/cli.go +++ b/components/cli/command/cli.go @@ -45,7 +45,7 @@ type DockerCli struct { func (cli *DockerCli) HasExperimental() bool { if cli.hasExperimental == nil { if cli.client == nil { - cli.Initialize(cliflags.NewClientOptions()) + return false } enabled := false cli.hasExperimental = &enabled diff --git a/components/cli/command/commands/commands.go b/components/cli/command/commands/commands.go index 425f90ba7d..fad709bca1 100644 --- a/components/cli/command/commands/commands.go +++ b/components/cli/command/commands/commands.go @@ -70,17 +70,12 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) { hide(image.NewSaveCommand(dockerCli)), hide(image.NewTagCommand(dockerCli)), hide(system.NewInspectCommand(dockerCli)), + stack.NewStackCommand(dockerCli), + stack.NewTopLevelDeployCommand(dockerCli), + checkpoint.NewCheckpointCommand(dockerCli), + plugin.NewPluginCommand(dockerCli), ) - if dockerCli.HasExperimental() { - cmd.AddCommand( - stack.NewStackCommand(dockerCli), - stack.NewTopLevelDeployCommand(dockerCli), - checkpoint.NewCheckpointCommand(dockerCli), - plugin.NewPluginCommand(dockerCli), - ) - } - } func hide(cmd *cobra.Command) *cobra.Command { diff --git a/components/cli/command/container/start.go b/components/cli/command/container/start.go index 8e0654da37..e544028932 100644 --- a/components/cli/command/container/start.go +++ b/components/cli/command/container/start.go @@ -45,11 +45,10 @@ func NewStartCommand(dockerCli *command.DockerCli) *cobra.Command { flags.BoolVarP(&opts.openStdin, "interactive", "i", false, "Attach container's STDIN") flags.StringVar(&opts.detachKeys, "detach-keys", "", "Override the key sequence for detaching a container") - if dockerCli.HasExperimental() { - flags.StringVar(&opts.checkpoint, "checkpoint", "", "Restore from this checkpoint") - flags.StringVar(&opts.checkpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory") - } - + flags.StringVar(&opts.checkpoint, "checkpoint", "", "Restore from this checkpoint") + flags.StringVar(&opts.checkpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory") + flags.SetAnnotation("checkpoint", "experimental", nil) + flags.SetAnnotation("checkpoint-dir", "experimental", nil) return cmd } diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go index dc18601900..5cf36cfd53 100644 --- a/components/cli/command/image/build.go +++ b/components/cli/command/image/build.go @@ -111,9 +111,8 @@ func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command { command.AddTrustedFlags(flags, true) - if dockerCli.HasExperimental() { - flags.BoolVar(&options.squash, "squash", false, "Squash newly built layers into a single new layer") - } + flags.BoolVar(&options.squash, "squash", false, "Squash newly built layers into a single new layer") + flags.SetAnnotation("squash", "experimental", nil) return cmd } From 0fef0ef25cfcfe7cca421e5bdfdda09208fa725e Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Wed, 2 Nov 2016 17:43:32 -0700 Subject: [PATCH 272/978] always add but hide experimental cmds and flags Signed-off-by: Victor Vieux Upstream-commit: 73d63ec5a67b12a01549831e45ce8dc98492be48 Component: cli --- components/cli/docker.go | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/components/cli/docker.go b/components/cli/docker.go index 53e14f7ec3..3b6e7f8633 100644 --- a/components/cli/docker.go +++ b/components/cli/docker.go @@ -3,6 +3,7 @@ package main import ( "fmt" "os" + "strings" "github.com/Sirupsen/logrus" "github.com/docker/docker/cli" @@ -33,7 +34,8 @@ func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command { showVersion() return nil } - fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + cmd.SetOutput(dockerCli.Err()) + cmd.HelpFunc()(cmd, args) return nil }, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { @@ -45,6 +47,22 @@ func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command { } cli.SetupRootCommand(cmd) + cmd.SetHelpFunc(func(ccmd *cobra.Command, args []string) { + var err error + if dockerCli.Client() == nil { + // flags must be the top-level command flags, not cmd.Flags() + opts.Common.SetDefaultOptions(flags) + dockerPreRun(opts) + err = dockerCli.Initialize(opts) + } + if err != nil || !dockerCli.HasExperimental() { + hideExperimentalFeatures(ccmd) + } + if err := ccmd.Help(); err != nil { + ccmd.Println(err) + } + }) + flags = cmd.Flags() flags.BoolVarP(&opts.Version, "version", "v", false, "Print version information and quit") flags.StringVar(&opts.ConfigDir, "config", cliconfig.ConfigDir(), "Location of client config files") @@ -105,3 +123,20 @@ func dockerPreRun(opts *cliflags.ClientOptions) { utils.EnableDebug() } } + +func hideExperimentalFeatures(cmd *cobra.Command) { + // hide flags + cmd.Flags().VisitAll(func(f *pflag.Flag) { + if _, ok := f.Annotations["experimental"]; ok { + f.Hidden = true + } + }) + + for _, subcmd := range cmd.Commands() { + // hide subcommands + name := strings.Split(subcmd.Use, " ")[0] + if name == "stack" || name == "deploy" || name == "checkpoint" || name == "plugin" { + subcmd.Hidden = true + } + } +} From 3e2986789f5137122d7930086ae7d40eb590e62d Mon Sep 17 00:00:00 2001 From: Kunal Kushwaha Date: Wed, 24 Aug 2016 17:30:54 +0900 Subject: [PATCH 273/978] correct handling of volumes while service update. Updating a service to replace a volume is handled properly now. Fixes bug#25772 Signed-off-by: Kunal Kushwaha Upstream-commit: b8cf9a880eb1d66bc07ebc03557a02f4c1d32c1b Component: cli --- components/cli/command/service/update.go | 45 ++++++++++++++++--- components/cli/command/service/update_test.go | 28 +++++++++--- 2 files changed, 63 insertions(+), 10 deletions(-) diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index a9aa9c9987..34cc9bc3d8 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -181,7 +181,9 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error { updateEnvironment(flags, &cspec.Env) updateString(flagWorkdir, &cspec.Dir) updateString(flagUser, &cspec.User) - updateMounts(flags, &cspec.Mounts) + if err := updateMounts(flags, &cspec.Mounts); err != nil { + return err + } if flags.Changed(flagLimitCPU) || flags.Changed(flagLimitMemory) { taskResources().Limits = &swarm.Resources{} @@ -402,20 +404,53 @@ func removeItems( return newSeq } -func updateMounts(flags *pflag.FlagSet, mounts *[]mounttypes.Mount) { +type byMountSource []mounttypes.Mount + +func (m byMountSource) Len() int { return len(m) } +func (m byMountSource) Swap(i, j int) { m[i], m[j] = m[j], m[i] } +func (m byMountSource) Less(i, j int) bool { + a, b := m[i], m[j] + + if a.Source == b.Source { + return a.Target < b.Target + } + + return a.Source < b.Source +} + +func updateMounts(flags *pflag.FlagSet, mounts *[]mounttypes.Mount) error { + + mountsByTarget := map[string]mounttypes.Mount{} + if flags.Changed(flagMountAdd) { values := flags.Lookup(flagMountAdd).Value.(*opts.MountOpt).Value() - *mounts = append(*mounts, values...) + for _, mount := range values { + if _, ok := mountsByTarget[mount.Target]; ok { + return fmt.Errorf("duplicate mount target") + } + mountsByTarget[mount.Target] = mount + } + } + + // Add old list of mount points minus updated one. + for _, mount := range *mounts { + if _, ok := mountsByTarget[mount.Target]; !ok { + mountsByTarget[mount.Target] = mount + } } - toRemove := buildToRemoveSet(flags, flagMountRemove) newMounts := []mounttypes.Mount{} - for _, mount := range *mounts { + + toRemove := buildToRemoveSet(flags, flagMountRemove) + + for _, mount := range mountsByTarget { if _, exists := toRemove[mount.Target]; !exists { newMounts = append(newMounts, mount) } } + sort.Sort(byMountSource(newMounts)) *mounts = newMounts + return nil } func updateGroups(flags *pflag.FlagSet, groups *[]string) error { diff --git a/components/cli/command/service/update_test.go b/components/cli/command/service/update_test.go index 731358753e..2123d1b794 100644 --- a/components/cli/command/service/update_test.go +++ b/components/cli/command/service/update_test.go @@ -122,18 +122,36 @@ func TestUpdateGroups(t *testing.T) { func TestUpdateMounts(t *testing.T) { flags := newUpdateCommand(nil).Flags() - flags.Set("mount-add", "type=volume,target=/toadd") + flags.Set("mount-add", "type=volume,source=vol2,target=/toadd") flags.Set("mount-rm", "/toremove") mounts := []mounttypes.Mount{ - {Target: "/toremove", Type: mounttypes.TypeBind}, - {Target: "/tokeep", Type: mounttypes.TypeBind}, + {Target: "/toremove", Source: "vol1", Type: mounttypes.TypeBind}, + {Target: "/tokeep", Source: "vol3", Type: mounttypes.TypeBind}, } updateMounts(flags, &mounts) assert.Equal(t, len(mounts), 2) - assert.Equal(t, mounts[0].Target, "/tokeep") - assert.Equal(t, mounts[1].Target, "/toadd") + assert.Equal(t, mounts[0].Target, "/toadd") + assert.Equal(t, mounts[1].Target, "/tokeep") + +} + +func TestUpdateMountsWithDuplicateMounts(t *testing.T) { + flags := newUpdateCommand(nil).Flags() + flags.Set("mount-add", "type=volume,source=vol4,target=/toadd") + + mounts := []mounttypes.Mount{ + {Target: "/tokeep1", Source: "vol1", Type: mounttypes.TypeBind}, + {Target: "/toadd", Source: "vol2", Type: mounttypes.TypeBind}, + {Target: "/tokeep2", Source: "vol3", Type: mounttypes.TypeBind}, + } + + updateMounts(flags, &mounts) + assert.Equal(t, len(mounts), 3) + assert.Equal(t, mounts[0].Target, "/tokeep1") + assert.Equal(t, mounts[1].Target, "/tokeep2") + assert.Equal(t, mounts[2].Target, "/toadd") } func TestUpdatePorts(t *testing.T) { From 792c7e5a54b6a0372416173268ad1ae91cd15930 Mon Sep 17 00:00:00 2001 From: lixiaobing10051267 Date: Fri, 4 Nov 2016 17:16:11 +0800 Subject: [PATCH 274/978] add error information to distinguish different test scene Signed-off-by: lixiaobing10051267 Upstream-commit: a98c89b310e709f78d64e1321273f5aed17a79f1 Component: cli --- components/cli/container_create_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/container_create_test.go b/components/cli/container_create_test.go index 89641038f7..15dbd5ea01 100644 --- a/components/cli/container_create_test.go +++ b/components/cli/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 9739850eebede51bebaf845bf4be45417584af62 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Tue, 1 Nov 2016 10:12:29 -0700 Subject: [PATCH 275/978] Add `--cpus` flag to control cpu resources This fix tries to address the proposal raised in 27921 and add `--cpus` flag for `docker run/create`. Basically, `--cpus` will allow user to specify a number (possibly partial) about how many CPUs the container will use. For example, on a 2-CPU system `--cpus 1.5` means the container will take 75% (1.5/2) of the CPU share. This fix adds a `NanoCPUs` field to `HostConfig` since swarmkit alreay have a concept of NanoCPUs for tasks. The `--cpus` flag will translate the number into reused `NanoCPUs` to be consistent. This fix adds integration tests to cover the changes. Related docs (`docker run` and Remote APIs) have been updated. This fix fixes 27921. Signed-off-by: Yong Tang Upstream-commit: 1cab3b32a68c0493843a0196fd50029553b160b6 Component: cli --- components/cli/command/service/opts.go | 32 ++------------------- components/cli/command/service/opts_test.go | 5 ++-- 2 files changed, 5 insertions(+), 32 deletions(-) diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index c89d40a767..2199e9f363 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -2,7 +2,6 @@ package service import ( "fmt" - "math/big" "strconv" "strings" "time" @@ -40,33 +39,6 @@ func (m *memBytes) Value() int64 { return int64(*m) } -type nanoCPUs int64 - -func (c *nanoCPUs) String() string { - return big.NewRat(c.Value(), 1e9).FloatString(3) -} - -func (c *nanoCPUs) Set(value string) error { - cpu, ok := new(big.Rat).SetString(value) - if !ok { - return fmt.Errorf("Failed to parse %v as a rational number", value) - } - nano := cpu.Mul(cpu, big.NewRat(1e9, 1)) - if !nano.IsInt() { - return fmt.Errorf("value is too precise") - } - *c = nanoCPUs(nano.Num().Int64()) - return nil -} - -func (c *nanoCPUs) Type() string { - return "NanoCPUs" -} - -func (c *nanoCPUs) Value() int64 { - return int64(*c) -} - // PositiveDurationOpt is an option type for time.Duration that uses a pointer. // It bahave similarly to DurationOpt but only allows positive duration values. type PositiveDurationOpt struct { @@ -156,9 +128,9 @@ type updateOptions struct { } type resourceOptions struct { - limitCPU nanoCPUs + limitCPU opts.NanoCPUs limitMemBytes memBytes - resCPU nanoCPUs + resCPU opts.NanoCPUs resMemBytes memBytes } diff --git a/components/cli/command/service/opts_test.go b/components/cli/command/service/opts_test.go index 26534cf0f5..aa2d999dcf 100644 --- a/components/cli/command/service/opts_test.go +++ b/components/cli/command/service/opts_test.go @@ -6,6 +6,7 @@ import ( "time" "github.com/docker/docker/api/types/container" + "github.com/docker/docker/opts" "github.com/docker/docker/pkg/testutil/assert" ) @@ -21,12 +22,12 @@ func TestMemBytesSetAndValue(t *testing.T) { } func TestNanoCPUsString(t *testing.T) { - var cpus nanoCPUs = 6100000000 + var cpus opts.NanoCPUs = 6100000000 assert.Equal(t, cpus.String(), "6.100") } func TestNanoCPUsSetAndValue(t *testing.T) { - var cpus nanoCPUs + var cpus opts.NanoCPUs assert.NilError(t, cpus.Set("0.35")) assert.Equal(t, cpus.Value(), int64(350000000)) } From a3b971ce9397959c24c200c1271763909b8cb0a2 Mon Sep 17 00:00:00 2001 From: Antonio Murdaca Date: Fri, 4 Nov 2016 17:16:44 +0100 Subject: [PATCH 276/978] cli/info: fix seccomp warning also reword seccomp warning around default seccomp profile Signed-off-by: Antonio Murdaca Upstream-commit: b338ab7c41955c8109711c2e31b9f94f67e9284c Component: cli --- components/cli/command/system/info.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/command/system/info.go b/components/cli/command/system/info.go index dfbc83d90a..7ab658c136 100644 --- a/components/cli/command/system/info.go +++ b/components/cli/command/system/info.go @@ -147,8 +147,8 @@ func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error { case "Name": fmt.Fprintf(dockerCli.Out(), " %s\n", o.Value) case "Profile": - if o.Key != "default" { - fmt.Fprintf(dockerCli.Err(), " WARNING: You're not using the Docker's default seccomp profile\n") + if o.Value != "default" { + fmt.Fprintf(dockerCli.Err(), " WARNING: You're not using the default seccomp profile\n") } fmt.Fprintf(dockerCli.Out(), " %s: %s\n", o.Key, o.Value) } From 75f0b0b043b84c493ad9cb929c5a474c76451194 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Thu, 3 Nov 2016 17:12:15 -0700 Subject: [PATCH 277/978] update cobra and use Tags Signed-off-by: Victor Vieux Upstream-commit: 1e10649f55dbe8a8cd3a91ffae624c9eab067567 Component: cli --- components/cli/command/checkpoint/cmd.go | 1 + components/cli/command/cli.go | 26 +++++++++-------------- components/cli/command/container/start.go | 2 +- components/cli/command/plugin/cmd.go | 1 + components/cli/command/stack/cmd.go | 1 + components/cli/command/stack/deploy.go | 1 + 6 files changed, 15 insertions(+), 17 deletions(-) diff --git a/components/cli/command/checkpoint/cmd.go b/components/cli/command/checkpoint/cmd.go index 84084ab716..7f9e537779 100644 --- a/components/cli/command/checkpoint/cmd.go +++ b/components/cli/command/checkpoint/cmd.go @@ -17,6 +17,7 @@ func NewCheckpointCommand(dockerCli *command.DockerCli) *cobra.Command { Run: func(cmd *cobra.Command, args []string) { fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) }, + Tags: map[string]string{"experimental": ""}, } cmd.AddCommand( newCreateCommand(dockerCli), diff --git a/components/cli/command/cli.go b/components/cli/command/cli.go index 9b61492442..33a26c4c64 100644 --- a/components/cli/command/cli.go +++ b/components/cli/command/cli.go @@ -32,27 +32,21 @@ type Streams interface { // DockerCli represents the docker command line client. // Instances of the client can be returned from NewDockerCli. type DockerCli struct { - configFile *configfile.ConfigFile - in *InStream - out *OutStream - err io.Writer - keyFile string - client client.APIClient - hasExperimental *bool + configFile *configfile.ConfigFile + in *InStream + out *OutStream + err io.Writer + keyFile string + client client.APIClient } // HasExperimental returns true if experimental features are accessible func (cli *DockerCli) HasExperimental() bool { - if cli.hasExperimental == nil { - if cli.client == nil { - return false - } - enabled := false - cli.hasExperimental = &enabled - enabled, _ = cli.client.Ping(context.Background()) + if cli.client == nil { + return false } - - return *cli.hasExperimental + enabled, _ := cli.client.Ping(context.Background()) + return enabled } // Client returns the APIClient diff --git a/components/cli/command/container/start.go b/components/cli/command/container/start.go index e544028932..87e815fed5 100644 --- a/components/cli/command/container/start.go +++ b/components/cli/command/container/start.go @@ -46,8 +46,8 @@ func NewStartCommand(dockerCli *command.DockerCli) *cobra.Command { flags.StringVar(&opts.detachKeys, "detach-keys", "", "Override the key sequence for detaching a container") flags.StringVar(&opts.checkpoint, "checkpoint", "", "Restore from this checkpoint") - flags.StringVar(&opts.checkpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory") flags.SetAnnotation("checkpoint", "experimental", nil) + flags.StringVar(&opts.checkpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory") flags.SetAnnotation("checkpoint-dir", "experimental", nil) return cmd } diff --git a/components/cli/command/plugin/cmd.go b/components/cli/command/plugin/cmd.go index 80fa61cb1c..c78f43a8d4 100644 --- a/components/cli/command/plugin/cmd.go +++ b/components/cli/command/plugin/cmd.go @@ -17,6 +17,7 @@ func NewPluginCommand(dockerCli *command.DockerCli) *cobra.Command { Run: func(cmd *cobra.Command, args []string) { fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) }, + Tags: map[string]string{"experimental": ""}, } cmd.AddCommand( diff --git a/components/cli/command/stack/cmd.go b/components/cli/command/stack/cmd.go index 49fcedf209..70afec9c6d 100644 --- a/components/cli/command/stack/cmd.go +++ b/components/cli/command/stack/cmd.go @@ -17,6 +17,7 @@ func NewStackCommand(dockerCli *command.DockerCli) *cobra.Command { Run: func(cmd *cobra.Command, args []string) { fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) }, + Tags: map[string]string{"experimental": ""}, } cmd.AddCommand( newConfigCommand(dockerCli), diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index fcf55fb7d2..b0f6b455a8 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -36,6 +36,7 @@ func newDeployCommand(dockerCli *command.DockerCli) *cobra.Command { opts.namespace = strings.TrimSuffix(args[0], ".dab") return runDeploy(dockerCli, opts) }, + Tags: map[string]string{"experimental": ""}, } flags := cmd.Flags() From 18b0c2adc0da39305004fa8e29985d6460c45f71 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Thu, 3 Nov 2016 17:12:15 -0700 Subject: [PATCH 278/978] update cobra and use Tags Signed-off-by: Victor Vieux Upstream-commit: 2ee5bbcbfa9c3b1da212f4523c95f2b54e9c1e0c Component: cli --- components/cli/docker.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/components/cli/docker.go b/components/cli/docker.go index 3b6e7f8633..65568e1e61 100644 --- a/components/cli/docker.go +++ b/components/cli/docker.go @@ -3,7 +3,6 @@ package main import ( "fmt" "os" - "strings" "github.com/Sirupsen/logrus" "github.com/docker/docker/cli" @@ -134,8 +133,7 @@ func hideExperimentalFeatures(cmd *cobra.Command) { for _, subcmd := range cmd.Commands() { // hide subcommands - name := strings.Split(subcmd.Use, " ")[0] - if name == "stack" || name == "deploy" || name == "checkpoint" || name == "plugin" { + if _, ok := subcmd.Tags["experimental"]; ok { subcmd.Hidden = true } } From 35da390f938a137a51743cb2565487f02a7e4476 Mon Sep 17 00:00:00 2001 From: yupeng Date: Sat, 5 Nov 2016 10:45:15 +0800 Subject: [PATCH 279/978] Align arg with other cli Signed-off-by: yupeng Upstream-commit: 46418414a20d618ab3b09aec5442e48d7d9b66ad Component: cli --- components/cli/docker.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/docker.go b/components/cli/docker.go index 65568e1e61..56c5a89895 100644 --- a/components/cli/docker.go +++ b/components/cli/docker.go @@ -22,7 +22,7 @@ func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command { var flags *pflag.FlagSet cmd := &cobra.Command{ - Use: "docker [OPTIONS] COMMAND [arg...]", + Use: "docker [OPTIONS] COMMAND [ARG...]", Short: "A self-sufficient runtime for containers", SilenceUsage: true, SilenceErrors: true, From 0ee62b4e2a88489eb0005e0f9daca95f28d68ea1 Mon Sep 17 00:00:00 2001 From: Josh Horwitz Date: Thu, 3 Nov 2016 09:58:45 -0400 Subject: [PATCH 280/978] Add -a option to service/node ps Signed-off-by: Josh Horwitz Upstream-commit: 21096cfc05294f7e34a37c25112b699f564999e5 Component: cli --- components/cli/command/node/ps.go | 7 +++++++ components/cli/command/service/ps.go | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/components/cli/command/node/ps.go b/components/cli/command/node/ps.go index a034721d24..8591f04669 100644 --- a/components/cli/command/node/ps.go +++ b/components/cli/command/node/ps.go @@ -17,6 +17,7 @@ import ( type psOptions struct { nodeIDs []string + all bool noResolve bool noTrunc bool filter opts.FilterOpt @@ -43,6 +44,7 @@ func newPsCommand(dockerCli *command.DockerCli) *cobra.Command { flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output") flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names") flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") + flags.BoolVarP(&opts.all, "all", "a", false, "Show all tasks (default shows tasks that are or will be running)") return cmd } @@ -72,6 +74,11 @@ func runPs(dockerCli *command.DockerCli, opts psOptions) error { filter := opts.filter.Value() filter.Add("node", node.ID) + if !opts.all && !filter.Include("desired-state") { + filter.Add("desired-state", string(swarm.TaskStateRunning)) + filter.Add("desired-state", string(swarm.TaskStateAccepted)) + } + nodeTasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: filter}) if err != nil { errs = append(errs, err.Error()) diff --git a/components/cli/command/service/ps.go b/components/cli/command/service/ps.go index cf94ad7374..0028507c22 100644 --- a/components/cli/command/service/ps.go +++ b/components/cli/command/service/ps.go @@ -2,6 +2,7 @@ package service import ( "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/idresolver" @@ -14,6 +15,7 @@ import ( type psOptions struct { serviceID string + all bool quiet bool noResolve bool noTrunc bool @@ -37,6 +39,7 @@ func newPsCommand(dockerCli *command.DockerCli) *cobra.Command { flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output") flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names") flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") + flags.BoolVarP(&opts.all, "all", "a", false, "Show all tasks (default shows tasks that are or will be running)") return cmd } @@ -64,6 +67,11 @@ func runPS(dockerCli *command.DockerCli, opts psOptions) error { } } + if !opts.all && !filter.Include("desired-state") { + filter.Add("desired-state", string(swarm.TaskStateRunning)) + filter.Add("desired-state", string(swarm.TaskStateAccepted)) + } + tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: filter}) if err != nil { return err From 3c2e5c3d64f21c3f17d18d70b70095ff7b9edf00 Mon Sep 17 00:00:00 2001 From: Alicia Lauerman Date: Thu, 3 Nov 2016 14:20:53 -0400 Subject: [PATCH 281/978] remove COMMAND column from service ls output. closes #27994 Signed-off-by: Alicia Lauerman Upstream-commit: 1491ae50e0656f9354a29e74a125549b1c826276 Component: cli --- components/cli/command/service/list.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/components/cli/command/service/list.go b/components/cli/command/service/list.go index 4db5618798..05b425a45e 100644 --- a/components/cli/command/service/list.go +++ b/components/cli/command/service/list.go @@ -3,7 +3,6 @@ package service import ( "fmt" "io" - "strings" "text/tabwriter" "github.com/docker/docker/api/types" @@ -18,7 +17,7 @@ import ( ) const ( - listItemFmt = "%s\t%s\t%s\t%s\t%s\n" + listItemFmt = "%s\t%s\t%s\t%s\n" ) type listOptions struct { @@ -110,7 +109,7 @@ func printTable(out io.Writer, services []swarm.Service, running map[string]int) // Ignore flushing errors defer writer.Flush() - fmt.Fprintf(writer, listItemFmt, "ID", "NAME", "REPLICAS", "IMAGE", "COMMAND") + fmt.Fprintf(writer, listItemFmt, "ID", "NAME", "REPLICAS", "IMAGE") for _, service := range services { replicas := "" if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil { @@ -124,8 +123,7 @@ func printTable(out io.Writer, services []swarm.Service, running map[string]int) stringid.TruncateID(service.ID), service.Spec.Name, replicas, - service.Spec.TaskTemplate.ContainerSpec.Image, - strings.Join(service.Spec.TaskTemplate.ContainerSpec.Args, " ")) + service.Spec.TaskTemplate.ContainerSpec.Image) } } From 1fb5d053e72bb47f217f3d239543953691eaa67b Mon Sep 17 00:00:00 2001 From: Antonio Murdaca Date: Mon, 7 Nov 2016 10:01:28 +0100 Subject: [PATCH 282/978] client: bump default version to v1.25 Signed-off-by: Antonio Murdaca Upstream-commit: 58c2d938dd653ed6ab2135aee21f2105981deaaa Component: cli --- components/cli/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/client.go b/components/cli/client.go index 9dcb3986cf..3b97720e00 100644 --- a/components/cli/client.go +++ b/components/cli/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 8408e69f91b113813d1f69c789a1e112f7079f23 Mon Sep 17 00:00:00 2001 From: WangPing Date: Mon, 7 Nov 2016 16:17:04 +0800 Subject: [PATCH 283/978] modify to improve code readability Signed-off-by: WangPing align Signed-off-by: WangPing align Signed-off-by: WangPing Upstream-commit: 713c7cd81ed199064ccc1c6226ddabeae8d739cf Component: cli --- components/cli/command/image/build.go | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go index 5cf36cfd53..604888b6fa 100644 --- a/components/cli/command/image/build.go +++ b/components/cli/command/image/build.go @@ -136,20 +136,16 @@ func (out *lastProgressOutput) WriteProgress(prog progress.Progress) error { func runBuild(dockerCli *command.DockerCli, options buildOptions) error { var ( - buildCtx io.ReadCloser - err error - ) - - specifiedContext := options.context - - var ( + buildCtx io.ReadCloser + err error contextDir string tempDir string relDockerfile string progBuff io.Writer buildBuff io.Writer ) - + + specifiedContext := options.context progBuff = dockerCli.Out() buildBuff = dockerCli.Out() if options.quiet { From 1fe9a67a275f5e5df3b5b89f720538e0d73a4e21 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Fri, 4 Nov 2016 11:31:44 -0700 Subject: [PATCH 284/978] Add `--tty` to `docker service create/update` This fix tries to add `--tty` to `docker service create/update`. As was specified in 25644, `TTY` flag has been added to SwarmKit and is already vendored. This fix add `--tty` to `docker service create/update`. Related document has been updated. Additional integration tests has been added. This fix fixes 25644. Signed-off-by: Yong Tang Upstream-commit: 3baa727ed100a46c27f159a4430508b0a4a1b02c Component: cli --- components/cli/command/service/opts.go | 5 +++++ components/cli/command/service/update.go | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index 2199e9f363..989fd18b8f 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -294,6 +294,7 @@ type serviceOptions struct { workdir string user string groups []string + tty bool mounts opts.MountOpt resources resourceOptions @@ -365,6 +366,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { Dir: opts.workdir, User: opts.user, Groups: opts.groups, + TTY: opts.tty, Mounts: opts.mounts.Value(), StopGracePeriod: opts.stopGrace.Value(), }, @@ -450,6 +452,8 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { flags.Var(&opts.healthcheck.timeout, flagHealthTimeout, "Maximum time to allow one check to run") flags.IntVar(&opts.healthcheck.retries, flagHealthRetries, 0, "Consecutive failures needed to report unhealthy") flags.BoolVar(&opts.healthcheck.noHealthcheck, flagNoHealthcheck, false, "Disable any container-specified HEALTHCHECK") + + flags.BoolVarP(&opts.tty, flagTTY, "t", false, "Allocate a pseudo-TTY") } const ( @@ -490,6 +494,7 @@ const ( flagRestartMaxAttempts = "restart-max-attempts" flagRestartWindow = "restart-window" flagStopGracePeriod = "stop-grace-period" + flagTTY = "tty" flagUpdateDelay = "update-delay" flagUpdateFailureAction = "update-failure-action" flagUpdateMaxFailureRatio = "update-max-failure-ratio" diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index 34cc9bc3d8..c278ac1ba4 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -274,6 +274,14 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error { return err } + if flags.Changed(flagTTY) { + tty, err := flags.GetBool(flagTTY) + if err != nil { + return err + } + cspec.TTY = tty + } + return nil } From ffad23ed7359ca713ea1da25d45c2b243d8eaed6 Mon Sep 17 00:00:00 2001 From: John Howard Date: Wed, 2 Nov 2016 10:04:39 -0700 Subject: [PATCH 285/978] Adds minimum API version to version Signed-off-by: John Howard Upstream-commit: 089b33edd8d297b2186f888967d5e6b45354a0a8 Component: cli --- components/cli/command/system/version.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/components/cli/command/system/version.go b/components/cli/command/system/version.go index 0b484cb3b9..6040c79361 100644 --- a/components/cli/command/system/version.go +++ b/components/cli/command/system/version.go @@ -23,13 +23,14 @@ var versionTemplate = `Client: OS/Arch: {{.Client.Os}}/{{.Client.Arch}}{{if .ServerOK}} Server: - Version: {{.Server.Version}} - API version: {{.Server.APIVersion}} - Go version: {{.Server.GoVersion}} - Git commit: {{.Server.GitCommit}} - Built: {{.Server.BuildTime}} - OS/Arch: {{.Server.Os}}/{{.Server.Arch}} - Experimental: {{.Server.Experimental}}{{end}}` + Version: {{.Server.Version}} + API version: {{.Server.APIVersion}} + Minimum API version: {{.Server.MinAPIVersion}} + Go version: {{.Server.GoVersion}} + Git commit: {{.Server.GitCommit}} + Built: {{.Server.BuildTime}} + OS/Arch: {{.Server.Os}}/{{.Server.Arch}} + Experimental: {{.Server.Experimental}}{{end}}` type versionOptions struct { format string From d8d857d3a5d060e2b7ab33c9cd098a62f10b1c7c Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Mon, 7 Nov 2016 17:43:11 -0800 Subject: [PATCH 286/978] support settings in docker plugins install Signed-off-by: Victor Vieux Upstream-commit: 3f7264473d9afc3cb5fdb430c4807e1a0bc71434 Component: cli --- components/cli/plugin_install.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/components/cli/plugin_install.go b/components/cli/plugin_install.go index 636c95364d..d0a3d517fc 100644 --- a/components/cli/plugin_install.go +++ b/components/cli/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 89bd4a499a28e9107b82c45b47f9378e7703d09a Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Mon, 7 Nov 2016 17:43:11 -0800 Subject: [PATCH 287/978] support settings in docker plugins install Signed-off-by: Victor Vieux Upstream-commit: 41513e30517b303e3f0b14ece4ac7ede273d5663 Component: cli --- components/cli/command/plugin/install.go | 9 +++++++-- components/cli/command/plugin/set.go | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/components/cli/command/plugin/install.go b/components/cli/command/plugin/install.go index 3989a35ce6..eae0183671 100644 --- a/components/cli/command/plugin/install.go +++ b/components/cli/command/plugin/install.go @@ -18,16 +18,20 @@ type pluginOptions struct { name string grantPerms bool disable bool + args []string } func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command { var options pluginOptions cmd := &cobra.Command{ - Use: "install [OPTIONS] PLUGIN", + Use: "install [OPTIONS] PLUGIN [KEY=VALUE...]", Short: "Install a plugin", - Args: cli.ExactArgs(1), // TODO: allow for set args + Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { options.name = args[0] + if len(args) > 1 { + options.args = args[1:] + } return runInstall(dockerCli, options) }, } @@ -75,6 +79,7 @@ func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error { AcceptPermissionsFunc: acceptPrivileges(dockerCli, opts.name), // TODO: Rename PrivilegeFunc, it has nothing to do with privileges PrivilegeFunc: registryAuthFunc, + Args: opts.args, } if err := dockerCli.Client().PluginInstall(ctx, ref.String(), options); err != nil { return err diff --git a/components/cli/command/plugin/set.go b/components/cli/command/plugin/set.go index e58ea63bc0..5660523ed9 100644 --- a/components/cli/command/plugin/set.go +++ b/components/cli/command/plugin/set.go @@ -13,7 +13,7 @@ import ( func newSetCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := &cobra.Command{ - Use: "set PLUGIN key1=value1 [key2=value2...]", + Use: "set PLUGIN KEY=VALUE [KEY=VALUE...]", Short: "Change settings for a plugin", Args: cli.RequiresMinArgs(2), RunE: func(cmd *cobra.Command, args []string) error { From 4cd3911d1349f6555d09fc320c79ac95324ab232 Mon Sep 17 00:00:00 2001 From: yupeng Date: Tue, 8 Nov 2016 14:51:17 +0800 Subject: [PATCH 288/978] context.Context should be the first parameter of a function Signed-off-by: yupeng Upstream-commit: 7113bbf2c6ac94b1f51e7f501512e4fb9273dc11 Component: cli --- components/cli/command/container/start.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/command/container/start.go b/components/cli/command/container/start.go index 87e815fed5..77bb9ddb93 100644 --- a/components/cli/command/container/start.go +++ b/components/cli/command/container/start.go @@ -155,13 +155,13 @@ func runStart(dockerCli *command.DockerCli, opts *startOptions) error { } else { // We're not going to attach to anything. // Start as many containers as we want. - return startContainersWithoutAttachments(dockerCli, ctx, opts.containers) + return startContainersWithoutAttachments(ctx, dockerCli, opts.containers) } return nil } -func startContainersWithoutAttachments(dockerCli *command.DockerCli, ctx context.Context, containers []string) error { +func startContainersWithoutAttachments(ctx context.Context, dockerCli *command.DockerCli, containers []string) error { var failedContainers []string for _, container := range containers { if err := dockerCli.Client().ContainerStart(ctx, container, types.ContainerStartOptions{}); err != nil { From 66ece30c85222c41ff6aff610ab359b46e281f48 Mon Sep 17 00:00:00 2001 From: allencloud Date: Tue, 25 Oct 2016 11:39:53 +0800 Subject: [PATCH 289/978] support show numbers of global service in service ls command Signed-off-by: allencloud Upstream-commit: 7891d349b377b5fdc0869bf6f24828e46bb4b9ac Component: cli --- components/cli/command/service/list.go | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/components/cli/command/service/list.go b/components/cli/command/service/list.go index 05b425a45e..f758808d1f 100644 --- a/components/cli/command/service/list.go +++ b/components/cli/command/service/list.go @@ -17,7 +17,7 @@ import ( ) const ( - listItemFmt = "%s\t%s\t%s\t%s\n" + listItemFmt = "%s\t%s\t%s\t%s\t%s\n" ) type listOptions struct { @@ -74,7 +74,7 @@ func runList(dockerCli *command.DockerCli, opts listOptions) error { PrintNotQuiet(out, services, nodes, tasks) } else if !opts.quiet { - // no services and not quiet, print only one line with columns ID, NAME, REPLICAS... + // no services and not quiet, print only one line with columns ID, NAME, MODE, REPLICAS... PrintNotQuiet(out, services, []swarm.Node{}, []swarm.Task{}) } else { PrintQuiet(out, services) @@ -94,34 +94,45 @@ func PrintNotQuiet(out io.Writer, services []swarm.Service, nodes []swarm.Node, } running := map[string]int{} + tasksNoShutdown := map[string]int{} + for _, task := range tasks { - if _, nodeActive := activeNodes[task.NodeID]; nodeActive && task.Status.State == "running" { + if task.DesiredState != swarm.TaskStateShutdown { + tasksNoShutdown[task.ServiceID]++ + } + + if _, nodeActive := activeNodes[task.NodeID]; nodeActive && task.Status.State == swarm.TaskStateRunning { running[task.ServiceID]++ } } - printTable(out, services, running) + printTable(out, services, running, tasksNoShutdown) } -func printTable(out io.Writer, services []swarm.Service, running map[string]int) { +func printTable(out io.Writer, services []swarm.Service, running, tasksNoShutdown map[string]int) { writer := tabwriter.NewWriter(out, 0, 4, 2, ' ', 0) // Ignore flushing errors defer writer.Flush() - fmt.Fprintf(writer, listItemFmt, "ID", "NAME", "REPLICAS", "IMAGE") + fmt.Fprintf(writer, listItemFmt, "ID", "NAME", "MODE", "REPLICAS", "IMAGE") + for _, service := range services { + mode := "" replicas := "" if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil { + mode = "replicated" replicas = fmt.Sprintf("%d/%d", running[service.ID], *service.Spec.Mode.Replicated.Replicas) } else if service.Spec.Mode.Global != nil { - replicas = "global" + mode = "global" + replicas = fmt.Sprintf("%d/%d", running[service.ID], tasksNoShutdown[service.ID]) } fmt.Fprintf( writer, listItemFmt, stringid.TruncateID(service.ID), service.Spec.Name, + mode, replicas, service.Spec.TaskTemplate.ContainerSpec.Image) } From 9670705c94294d6a34dcf0a66fca27dbad00011c Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Tue, 8 Nov 2016 16:15:09 +0800 Subject: [PATCH 290/978] Update for docker checkpoint Signed-off-by: yuexiao-wang Upstream-commit: cd2269a456bbdbf9276493738b104f1c5d9075e3 Component: cli --- components/cli/command/checkpoint/create.go | 6 +++--- components/cli/command/checkpoint/list.go | 4 ++-- components/cli/command/checkpoint/remove.go | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/components/cli/command/checkpoint/create.go b/components/cli/command/checkpoint/create.go index 646901ccd6..2377b5e2e3 100644 --- a/components/cli/command/checkpoint/create.go +++ b/components/cli/command/checkpoint/create.go @@ -20,7 +20,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { var opts createOptions cmd := &cobra.Command{ - Use: "create CONTAINER CHECKPOINT", + Use: "create [OPTIONS] CONTAINER CHECKPOINT", Short: "Create a checkpoint from a running container", Args: cli.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { @@ -31,8 +31,8 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { } flags := cmd.Flags() - flags.BoolVar(&opts.leaveRunning, "leave-running", false, "leave the container running after checkpoint") - flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "use a custom checkpoint storage directory") + flags.BoolVar(&opts.leaveRunning, "leave-running", false, "Leave the container running after checkpoint") + flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory") return cmd } diff --git a/components/cli/command/checkpoint/list.go b/components/cli/command/checkpoint/list.go index fef91a4ccd..daf8349993 100644 --- a/components/cli/command/checkpoint/list.go +++ b/components/cli/command/checkpoint/list.go @@ -20,7 +20,7 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command { var opts listOptions cmd := &cobra.Command{ - Use: "ls CONTAINER", + Use: "ls [OPTIONS] CONTAINER", Aliases: []string{"list"}, Short: "List checkpoints for a container", Args: cli.ExactArgs(1), @@ -30,7 +30,7 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command { } flags := cmd.Flags() - flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "use a custom checkpoint storage directory") + flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory") return cmd diff --git a/components/cli/command/checkpoint/remove.go b/components/cli/command/checkpoint/remove.go index c6ec56df84..ec39fa7b55 100644 --- a/components/cli/command/checkpoint/remove.go +++ b/components/cli/command/checkpoint/remove.go @@ -17,7 +17,7 @@ func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { var opts removeOptions cmd := &cobra.Command{ - Use: "rm CONTAINER CHECKPOINT", + Use: "rm [OPTIONS] CONTAINER CHECKPOINT", Aliases: []string{"remove"}, Short: "Remove a checkpoint", Args: cli.ExactArgs(2), @@ -27,7 +27,7 @@ func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { } flags := cmd.Flags() - flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "use a custom checkpoint storage directory") + flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory") return cmd } From 307edac9632c8caf199ceac8eb7afe25f83461c4 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Wed, 2 Nov 2016 17:43:32 -0700 Subject: [PATCH 291/978] 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 Upstream-commit: 4ae7176ffb8d37c60d2152fb155678e659af5b99 Component: cli --- components/cli/command/checkpoint/cmd.go | 7 ++-- components/cli/command/cli.go | 44 ++++++++++++++++------- components/cli/command/container/cmd.go | 5 ++- components/cli/command/container/exec.go | 1 + components/cli/command/container/prune.go | 1 + components/cli/command/image/build.go | 3 +- components/cli/command/image/cmd.go | 6 ++-- components/cli/command/image/prune.go | 1 + components/cli/command/network/cmd.go | 5 ++- components/cli/command/network/prune.go | 1 + components/cli/command/node/cmd.go | 5 ++- components/cli/command/plugin/cmd.go | 5 ++- components/cli/command/service/cmd.go | 5 ++- components/cli/command/stack/cmd.go | 7 ++-- components/cli/command/stack/deploy.go | 2 +- components/cli/command/swarm/cmd.go | 5 ++- components/cli/command/system/cmd.go | 6 ++-- components/cli/command/system/df.go | 1 + components/cli/command/system/prune.go | 1 + components/cli/command/system/version.go | 8 ++++- components/cli/command/volume/cmd.go | 5 ++- components/cli/command/volume/prune.go | 1 + components/cli/command/volume/remove.go | 2 +- 23 files changed, 75 insertions(+), 52 deletions(-) diff --git a/components/cli/command/checkpoint/cmd.go b/components/cli/command/checkpoint/cmd.go index 7f9e537779..f186232a4d 100644 --- a/components/cli/command/checkpoint/cmd.go +++ b/components/cli/command/checkpoint/cmd.go @@ -1,8 +1,6 @@ package checkpoint import ( - "fmt" - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/spf13/cobra" @@ -15,9 +13,10 @@ func NewCheckpointCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Manage checkpoints", Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { - fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + cmd.SetOutput(dockerCli.Err()) + cmd.HelpFunc()(cmd, args) }, - Tags: map[string]string{"experimental": ""}, + Tags: map[string]string{"experimental": "", "version": "1.25"}, } cmd.AddCommand( newCreateCommand(dockerCli), diff --git a/components/cli/command/cli.go b/components/cli/command/cli.go index 33a26c4c64..ef9de2edf1 100644 --- a/components/cli/command/cli.go +++ b/components/cli/command/cli.go @@ -10,6 +10,7 @@ import ( "runtime" "github.com/docker/docker/api" + "github.com/docker/docker/api/types/versions" cliflags "github.com/docker/docker/cli/flags" "github.com/docker/docker/cliconfig" "github.com/docker/docker/cliconfig/configfile" @@ -32,21 +33,24 @@ type Streams interface { // DockerCli represents the docker command line client. // Instances of the client can be returned from NewDockerCli. type DockerCli struct { - configFile *configfile.ConfigFile - in *InStream - out *OutStream - err io.Writer - keyFile string - client client.APIClient + configFile *configfile.ConfigFile + in *InStream + out *OutStream + err io.Writer + keyFile string + client client.APIClient + hasExperimental bool + defaultVersion string } -// HasExperimental returns true if experimental features are accessible +// HasExperimental returns true if experimental features are accessible. func (cli *DockerCli) HasExperimental() bool { - if cli.client == nil { - return false - } - enabled, _ := cli.client.Ping(context.Background()) - return enabled + return cli.hasExperimental +} + +// DefaultVersion returns api.defaultVersion of DOCKER_API_VERSION if specified. +func (cli *DockerCli) DefaultVersion() string { + return cli.defaultVersion } // Client returns the APIClient @@ -93,12 +97,28 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error { if err != nil { return err } + + cli.defaultVersion = cli.client.ClientVersion() + if opts.Common.TrustKey == "" { cli.keyFile = filepath.Join(cliconfig.ConfigDir(), cliflags.DefaultTrustKeyFile) } else { cli.keyFile = opts.Common.TrustKey } + if ping, err := cli.client.Ping(context.Background()); err == nil { + cli.hasExperimental = ping.Experimental + + // since the new header was added in 1.25, assume server is 1.24 if header is not present. + if ping.APIVersion == "" { + ping.APIVersion = "1.24" + } + + // if server version is lower than the current cli, downgrade + if versions.LessThan(ping.APIVersion, cli.client.ClientVersion()) { + cli.client.UpdateClientVersion(ping.APIVersion) + } + } return nil } diff --git a/components/cli/command/container/cmd.go b/components/cli/command/container/cmd.go index f06b863b58..075f936bd9 100644 --- a/components/cli/command/container/cmd.go +++ b/components/cli/command/container/cmd.go @@ -1,8 +1,6 @@ package container import ( - "fmt" - "github.com/spf13/cobra" "github.com/docker/docker/cli" @@ -16,7 +14,8 @@ func NewContainerCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Manage containers", Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { - fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + cmd.SetOutput(dockerCli.Err()) + cmd.HelpFunc()(cmd, args) }, } cmd.AddCommand( diff --git a/components/cli/command/container/exec.go b/components/cli/command/container/exec.go index 48964693b2..84eba113cf 100644 --- a/components/cli/command/container/exec.go +++ b/components/cli/command/container/exec.go @@ -59,6 +59,7 @@ func NewExecCommand(dockerCli *command.DockerCli) *cobra.Command { flags.StringVarP(&opts.user, "user", "u", "", "Username or UID (format: [:])") flags.BoolVarP(&opts.privileged, "privileged", "", false, "Give extended privileges to the command") flags.VarP(opts.env, "env", "e", "Set environment variables") + flags.SetAnnotation("env", "version", []string{"1.25"}) return cmd } diff --git a/components/cli/command/container/prune.go b/components/cli/command/container/prune.go index 99a97f6cd8..ec6b0e3147 100644 --- a/components/cli/command/container/prune.go +++ b/components/cli/command/container/prune.go @@ -35,6 +35,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) return nil }, + Tags: map[string]string{"version": "1.25"}, } flags := cmd.Flags() diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go index 604888b6fa..ebec87d641 100644 --- a/components/cli/command/image/build.go +++ b/components/cli/command/image/build.go @@ -113,6 +113,7 @@ func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command { flags.BoolVar(&options.squash, "squash", false, "Squash newly built layers into a single new layer") flags.SetAnnotation("squash", "experimental", nil) + flags.SetAnnotation("squash", "version", []string{"1.25"}) return cmd } @@ -144,7 +145,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { progBuff io.Writer buildBuff io.Writer ) - + specifiedContext := options.context progBuff = dockerCli.Out() buildBuff = dockerCli.Out() diff --git a/components/cli/command/image/cmd.go b/components/cli/command/image/cmd.go index 6f8e7b7d4b..dc98257438 100644 --- a/components/cli/command/image/cmd.go +++ b/components/cli/command/image/cmd.go @@ -1,8 +1,6 @@ package image import ( - "fmt" - "github.com/spf13/cobra" "github.com/docker/docker/cli" @@ -16,7 +14,8 @@ func NewImageCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Manage images", Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { - fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + cmd.SetOutput(dockerCli.Err()) + cmd.HelpFunc()(cmd, args) }, } cmd.AddCommand( @@ -33,6 +32,5 @@ func NewImageCommand(dockerCli *command.DockerCli) *cobra.Command { newInspectCommand(dockerCli), NewPruneCommand(dockerCli), ) - return cmd } diff --git a/components/cli/command/image/prune.go b/components/cli/command/image/prune.go index 46bd56cb10..ea84cda877 100644 --- a/components/cli/command/image/prune.go +++ b/components/cli/command/image/prune.go @@ -36,6 +36,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) return nil }, + Tags: map[string]string{"version": "1.25"}, } flags := cmd.Flags() diff --git a/components/cli/command/network/cmd.go b/components/cli/command/network/cmd.go index 77c8e4908e..c2a7e83dd8 100644 --- a/components/cli/command/network/cmd.go +++ b/components/cli/command/network/cmd.go @@ -1,8 +1,6 @@ package network import ( - "fmt" - "github.com/spf13/cobra" "github.com/docker/docker/cli" @@ -16,7 +14,8 @@ func NewNetworkCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Manage networks", Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { - fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + cmd.SetOutput(dockerCli.Err()) + cmd.HelpFunc()(cmd, args) }, } cmd.AddCommand( diff --git a/components/cli/command/network/prune.go b/components/cli/command/network/prune.go index 00e05d3bdf..f2f8cc20c4 100644 --- a/components/cli/command/network/prune.go +++ b/components/cli/command/network/prune.go @@ -33,6 +33,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { } return nil }, + Tags: map[string]string{"version": "1.25"}, } flags := cmd.Flags() diff --git a/components/cli/command/node/cmd.go b/components/cli/command/node/cmd.go index c7d0cf8181..d70ee81789 100644 --- a/components/cli/command/node/cmd.go +++ b/components/cli/command/node/cmd.go @@ -1,8 +1,6 @@ package node import ( - "fmt" - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" apiclient "github.com/docker/docker/client" @@ -17,7 +15,8 @@ func NewNodeCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Manage Swarm nodes", Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { - fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + cmd.SetOutput(dockerCli.Err()) + cmd.HelpFunc()(cmd, args) }, } cmd.AddCommand( diff --git a/components/cli/command/plugin/cmd.go b/components/cli/command/plugin/cmd.go index c78f43a8d4..03d01c8882 100644 --- a/components/cli/command/plugin/cmd.go +++ b/components/cli/command/plugin/cmd.go @@ -1,8 +1,6 @@ package plugin import ( - "fmt" - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/spf13/cobra" @@ -15,7 +13,8 @@ func NewPluginCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Manage plugins", Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { - fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + cmd.SetOutput(dockerCli.Err()) + cmd.HelpFunc()(cmd, args) }, Tags: map[string]string{"experimental": ""}, } diff --git a/components/cli/command/service/cmd.go b/components/cli/command/service/cmd.go index 9f342e1342..f4f7d00f91 100644 --- a/components/cli/command/service/cmd.go +++ b/components/cli/command/service/cmd.go @@ -1,8 +1,6 @@ package service import ( - "fmt" - "github.com/spf13/cobra" "github.com/docker/docker/cli" @@ -16,7 +14,8 @@ func NewServiceCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Manage services", Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { - fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + cmd.SetOutput(dockerCli.Err()) + cmd.HelpFunc()(cmd, args) }, } cmd.AddCommand( diff --git a/components/cli/command/stack/cmd.go b/components/cli/command/stack/cmd.go index 70afec9c6d..4189504403 100644 --- a/components/cli/command/stack/cmd.go +++ b/components/cli/command/stack/cmd.go @@ -1,8 +1,6 @@ package stack import ( - "fmt" - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/spf13/cobra" @@ -15,9 +13,10 @@ func NewStackCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Manage Docker stacks", Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { - fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + cmd.SetOutput(dockerCli.Err()) + cmd.HelpFunc()(cmd, args) }, - Tags: map[string]string{"experimental": ""}, + Tags: map[string]string{"experimental": "", "version": "1.25"}, } cmd.AddCommand( newConfigCommand(dockerCli), diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index b0f6b455a8..435a9193b4 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -36,7 +36,7 @@ func newDeployCommand(dockerCli *command.DockerCli) *cobra.Command { opts.namespace = strings.TrimSuffix(args[0], ".dab") return runDeploy(dockerCli, opts) }, - Tags: map[string]string{"experimental": ""}, + Tags: map[string]string{"experimental": "", "version": "1.25"}, } flags := cmd.Flags() diff --git a/components/cli/command/swarm/cmd.go b/components/cli/command/swarm/cmd.go index 9f9df53950..f0a6bcdeb8 100644 --- a/components/cli/command/swarm/cmd.go +++ b/components/cli/command/swarm/cmd.go @@ -1,8 +1,6 @@ package swarm import ( - "fmt" - "github.com/spf13/cobra" "github.com/docker/docker/cli" @@ -16,7 +14,8 @@ func NewSwarmCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Manage Swarm", Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { - fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + cmd.SetOutput(dockerCli.Err()) + cmd.HelpFunc()(cmd, args) }, } cmd.AddCommand( diff --git a/components/cli/command/system/cmd.go b/components/cli/command/system/cmd.go index 46caa2491c..9cd74b5d4b 100644 --- a/components/cli/command/system/cmd.go +++ b/components/cli/command/system/cmd.go @@ -1,8 +1,6 @@ package system import ( - "fmt" - "github.com/spf13/cobra" "github.com/docker/docker/cli" @@ -16,7 +14,8 @@ func NewSystemCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Manage Docker", Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { - fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + cmd.SetOutput(dockerCli.Err()) + cmd.HelpFunc()(cmd, args) }, } cmd.AddCommand( @@ -25,5 +24,6 @@ func NewSystemCommand(dockerCli *command.DockerCli) *cobra.Command { NewDiskUsageCommand(dockerCli), NewPruneCommand(dockerCli), ) + return cmd } diff --git a/components/cli/command/system/df.go b/components/cli/command/system/df.go index 293946c188..9f712484aa 100644 --- a/components/cli/command/system/df.go +++ b/components/cli/command/system/df.go @@ -23,6 +23,7 @@ func NewDiskUsageCommand(dockerCli *command.DockerCli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { return runDiskUsage(dockerCli, opts) }, + Tags: map[string]string{"version": "1.25"}, } flags := cmd.Flags() diff --git a/components/cli/command/system/prune.go b/components/cli/command/system/prune.go index c79bc6910e..92dddbdca6 100644 --- a/components/cli/command/system/prune.go +++ b/components/cli/command/system/prune.go @@ -26,6 +26,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { return runPrune(dockerCli, opts) }, + Tags: map[string]string{"version": "1.25"}, } flags := cmd.Flags() diff --git a/components/cli/command/system/version.go b/components/cli/command/system/version.go index 6040c79361..00a84a3cbc 100644 --- a/components/cli/command/system/version.go +++ b/components/cli/command/system/version.go @@ -1,6 +1,7 @@ package system import ( + "fmt" "runtime" "time" @@ -70,10 +71,15 @@ func runVersion(dockerCli *command.DockerCli, opts *versionOptions) error { Status: "Template parsing error: " + err.Error()} } + APIVersion := dockerCli.Client().ClientVersion() + if defaultAPIVersion := dockerCli.DefaultVersion(); APIVersion != defaultAPIVersion { + APIVersion = fmt.Sprintf("%s (downgraded from %s)", APIVersion, defaultAPIVersion) + } + vd := types.VersionResponse{ Client: &types.Version{ Version: dockerversion.Version, - APIVersion: dockerCli.Client().ClientVersion(), + APIVersion: APIVersion, GoVersion: runtime.Version(), GitCommit: dockerversion.GitCommit, BuildTime: dockerversion.BuildTime, diff --git a/components/cli/command/volume/cmd.go b/components/cli/command/volume/cmd.go index f35181ffaf..39e4b7f46e 100644 --- a/components/cli/command/volume/cmd.go +++ b/components/cli/command/volume/cmd.go @@ -1,8 +1,6 @@ package volume import ( - "fmt" - "github.com/spf13/cobra" "github.com/docker/docker/cli" @@ -17,7 +15,8 @@ func NewVolumeCommand(dockerCli *command.DockerCli) *cobra.Command { Long: volumeDescription, Args: cli.NoArgs, Run: func(cmd *cobra.Command, args []string) { - fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + cmd.SetOutput(dockerCli.Err()) + cmd.HelpFunc()(cmd, args) }, } cmd.AddCommand( diff --git a/components/cli/command/volume/prune.go b/components/cli/command/volume/prune.go index a4bb0092d6..ac9c94451a 100644 --- a/components/cli/command/volume/prune.go +++ b/components/cli/command/volume/prune.go @@ -35,6 +35,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) return nil }, + Tags: map[string]string{"version": "1.25"}, } flags := cmd.Flags() diff --git a/components/cli/command/volume/remove.go b/components/cli/command/volume/remove.go index 213ad26ab5..f464bb3e1a 100644 --- a/components/cli/command/volume/remove.go +++ b/components/cli/command/volume/remove.go @@ -34,7 +34,7 @@ func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() flags.BoolVarP(&opts.force, "force", "f", false, "Force the removal of one or more volumes") - + flags.SetAnnotation("force", "version", []string{"1.25"}) return cmd } From 10e4d1c58c3b6762200ba038b67ae02b2941f7bc Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Wed, 2 Nov 2016 17:43:32 -0700 Subject: [PATCH 292/978] 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 Upstream-commit: 4f63bfb619a88fa3df8b57bf1b11e89f90250061 Component: cli --- components/cli/client.go | 17 ++++++++++++++--- components/cli/container_create.go | 5 +++++ components/cli/container_exec.go | 5 +++++ components/cli/container_prune.go | 4 ++++ components/cli/errors.go | 11 +++++++++++ components/cli/image_build.go | 7 +++++-- components/cli/image_prune.go | 4 ++++ components/cli/interface.go | 2 +- components/cli/ping.go | 29 ++++++++++++++++++++--------- components/cli/request.go | 3 +++ components/cli/volume_prune.go | 4 ++++ components/cli/volume_remove.go | 7 +++++-- 12 files changed, 81 insertions(+), 17 deletions(-) diff --git a/components/cli/client.go b/components/cli/client.go index 3b97720e00..76a1ac07c0 100644 --- a/components/cli/client.go +++ b/components/cli/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/components/cli/container_create.go b/components/cli/container_create.go index c042b17468..9f627aafa6 100644 --- a/components/cli/container_create.go +++ b/components/cli/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/components/cli/container_exec.go b/components/cli/container_exec.go index f6df722918..0665c54fbd 100644 --- a/components/cli/container_exec.go +++ b/components/cli/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/components/cli/container_prune.go b/components/cli/container_prune.go index 0d8bd3292c..3eabe71a7f 100644 --- a/components/cli/container_prune.go +++ b/components/cli/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/components/cli/errors.go b/components/cli/errors.go index ad1dadabb6..53e2065332 100644 --- a/components/cli/errors.go +++ b/components/cli/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/components/cli/image_build.go b/components/cli/image_build.go index 4d611d5430..0049e4e290 100644 --- a/components/cli/image_build.go +++ b/components/cli/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/components/cli/image_prune.go b/components/cli/image_prune.go index f6752e5043..d5e69d5b19 100644 --- a/components/cli/image_prune.go +++ b/components/cli/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/components/cli/interface.go b/components/cli/interface.go index a78cb759cd..99b06709b5 100644 --- a/components/cli/interface.go +++ b/components/cli/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/components/cli/ping.go b/components/cli/ping.go index 5e99e1bba1..22dcda24fd 100644 --- a/components/cli/ping.go +++ b/components/cli/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/components/cli/request.go b/components/cli/request.go index c73464b54d..ac05363655 100644 --- a/components/cli/request.go +++ b/components/cli/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/components/cli/volume_prune.go b/components/cli/volume_prune.go index e7ea7b591d..ea4e234a30 100644 --- a/components/cli/volume_prune.go +++ b/components/cli/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/components/cli/volume_remove.go b/components/cli/volume_remove.go index 3d5aeff252..6c26575b49 100644 --- a/components/cli/volume_remove.go +++ b/components/cli/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 b322424e887c20c94ddab025c70e1d4276a36b8a Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Wed, 2 Nov 2016 17:43:32 -0700 Subject: [PATCH 293/978] 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 Upstream-commit: e1beebbeefaf85107e93ada7b04434bc56deaa3e Component: cli --- components/cli/docker.go | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/components/cli/docker.go b/components/cli/docker.go index 56c5a89895..18cd0e833c 100644 --- a/components/cli/docker.go +++ b/components/cli/docker.go @@ -5,6 +5,7 @@ import ( "os" "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/types/versions" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/commands" @@ -47,16 +48,15 @@ func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command { cli.SetupRootCommand(cmd) cmd.SetHelpFunc(func(ccmd *cobra.Command, args []string) { - var err error if dockerCli.Client() == nil { // flags must be the top-level command flags, not cmd.Flags() opts.Common.SetDefaultOptions(flags) dockerPreRun(opts) - err = dockerCli.Initialize(opts) - } - if err != nil || !dockerCli.HasExperimental() { - hideExperimentalFeatures(ccmd) + dockerCli.Initialize(opts) } + + hideUnsupportedFeatures(ccmd, dockerCli.Client().ClientVersion(), dockerCli.HasExperimental()) + if err := ccmd.Help(); err != nil { ccmd.Println(err) } @@ -123,18 +123,29 @@ func dockerPreRun(opts *cliflags.ClientOptions) { } } -func hideExperimentalFeatures(cmd *cobra.Command) { - // hide flags +func hideUnsupportedFeatures(cmd *cobra.Command, clientVersion string, hasExperimental bool) { cmd.Flags().VisitAll(func(f *pflag.Flag) { + // hide experimental flags if _, ok := f.Annotations["experimental"]; ok { f.Hidden = true } + + // hide flags not supported by the server + if flagVersion, ok := f.Annotations["version"]; ok && len(flagVersion) == 1 && versions.LessThan(clientVersion, flagVersion[0]) { + f.Hidden = true + } + }) for _, subcmd := range cmd.Commands() { - // hide subcommands + // hide experimental subcommands if _, ok := subcmd.Tags["experimental"]; ok { subcmd.Hidden = true } + + // hide subcommands not supported by the server + if subcmdVersion, ok := subcmd.Tags["version"]; ok && versions.LessThan(clientVersion, subcmdVersion) { + subcmd.Hidden = true + } } } From 36d1732f95acf5c584bebd24ad4961e53a5d348b Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Mon, 7 Nov 2016 18:40:47 -0800 Subject: [PATCH 294/978] Change to plural forms for help output of `docker service update` This fix is based on the comment in https://github.com/docker/docker/pull/27567#discussion_r86910604 Basically, in the help output of `docker service update`, the `--xxx-add` flags typically have plural forms while `--xxx-rm` flags have singular forms. This fix updates the help output for consistency. This fix also updates the related docs in `service_update.md`. The help output in `service_update.md` has been quite out-of-sync with the actual output so this fix replaces the output with the most up-to-date output. This fix is related to #27567. Signed-off-by: Yong Tang Upstream-commit: af8ebf69db3e5f80dbf8c17a79ee9503589365d9 Component: cli --- components/cli/command/service/update.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index c278ac1ba4..32a23f23ff 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -42,19 +42,19 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { addServiceFlags(cmd, opts) flags.Var(newListOptsVar(), flagEnvRemove, "Remove an environment variable") - flags.Var(newListOptsVar(), flagGroupRemove, "Remove previously added supplementary user groups from the container") + flags.Var(newListOptsVar(), flagGroupRemove, "Remove a previously added supplementary user group from the container") flags.Var(newListOptsVar(), flagLabelRemove, "Remove a label by its key") flags.Var(newListOptsVar(), flagContainerLabelRemove, "Remove a container label by its key") flags.Var(newListOptsVar(), flagMountRemove, "Remove a mount by its target path") flags.Var(newListOptsVar(), flagPublishRemove, "Remove a published port by its target port") flags.Var(newListOptsVar(), flagConstraintRemove, "Remove a constraint") - flags.Var(&opts.labels, flagLabelAdd, "Add or update service labels") - flags.Var(&opts.containerLabels, flagContainerLabelAdd, "Add or update container labels") - flags.Var(&opts.env, flagEnvAdd, "Add or update environment variables") + flags.Var(&opts.labels, flagLabelAdd, "Add or update a service label") + flags.Var(&opts.containerLabels, flagContainerLabelAdd, "Add or update a container label") + flags.Var(&opts.env, flagEnvAdd, "Add or update an environment variable") flags.Var(&opts.mounts, flagMountAdd, "Add or update a mount on a service") - flags.StringSliceVar(&opts.constraints, flagConstraintAdd, []string{}, "Add or update placement constraints") + flags.StringSliceVar(&opts.constraints, flagConstraintAdd, []string{}, "Add or update a placement constraint") flags.Var(&opts.endpoint.ports, flagPublishAdd, "Add or update a published port") - flags.StringSliceVar(&opts.groups, flagGroupAdd, []string{}, "Add additional supplementary user groups to the container") + flags.StringSliceVar(&opts.groups, flagGroupAdd, []string{}, "Add an additional supplementary user group to the container") return cmd } From 57632533c460702f831f0c6786f76a137c7a9cda Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Wed, 2 Nov 2016 12:29:51 -0700 Subject: [PATCH 295/978] cli: Add options for Raft snapshotting Add the following options to "swarm init" and "swarm update": - --max-snapshots: Retain this many old Raft snapshots in addition to the latest one - --snapshot-interval: Number of log entries between Raft snapshots These options already existed in SwarmKit and the Docker API but were never exposed in the CLI. I'm adding them here to fix this oversight. --max-snapshots may be useful for debugging purposes and more conservative users who want to store rolling backups of old versions of the Raft state. --snapshot-interval is most useful for performance tuning. The default value of 10000 may not be ideal for some setups. There is also a LogEntriesForSlowFollowers option that is not exposed. I decided not to expose it along with these others because I don't think it's generally useful (and I'm not sure what I would call the CLI flag). But if people want, I can expose it for the sake of completeness. Signed-off-by: Aaron Lehmann Upstream-commit: 2af34ea285138573d793e3a18fe9dad43b31f69f Component: cli --- components/cli/command/swarm/opts.go | 21 +++++++++++++--- components/cli/command/swarm/update.go | 33 +------------------------- components/cli/command/system/info.go | 3 +++ 3 files changed, 22 insertions(+), 35 deletions(-) diff --git a/components/cli/command/swarm/opts.go b/components/cli/command/swarm/opts.go index 3659b55f81..af36a71673 100644 --- a/components/cli/command/swarm/opts.go +++ b/components/cli/command/swarm/opts.go @@ -24,6 +24,8 @@ const ( flagToken = "token" flagTaskHistoryLimit = "task-history-limit" flagExternalCA = "external-ca" + flagMaxSnapshots = "max-snapshots" + flagSnapshotInterval = "snapshot-interval" ) type swarmOptions struct { @@ -31,6 +33,8 @@ type swarmOptions struct { dispatcherHeartbeat time.Duration nodeCertExpiry time.Duration externalCA ExternalCAOption + maxSnapshots uint64 + snapshotInterval uint64 } // NodeAddrOption is a pflag.Value for listening addresses @@ -167,11 +171,11 @@ func addSwarmFlags(flags *pflag.FlagSet, opts *swarmOptions) { flags.DurationVar(&opts.dispatcherHeartbeat, flagDispatcherHeartbeat, time.Duration(5*time.Second), "Dispatcher heartbeat period") flags.DurationVar(&opts.nodeCertExpiry, flagCertExpiry, time.Duration(90*24*time.Hour), "Validity period for node certificates") flags.Var(&opts.externalCA, flagExternalCA, "Specifications of one or more certificate signing endpoints") + flags.Uint64Var(&opts.maxSnapshots, flagMaxSnapshots, 0, "Number of additional Raft snapshots to retain") + flags.Uint64Var(&opts.snapshotInterval, flagSnapshotInterval, 10000, "Number of log entries between Raft snapshots") } -func (opts *swarmOptions) ToSpec(flags *pflag.FlagSet) swarm.Spec { - spec := swarm.Spec{} - +func (opts *swarmOptions) mergeSwarmSpec(spec *swarm.Spec, flags *pflag.FlagSet) { if flags.Changed(flagTaskHistoryLimit) { spec.Orchestration.TaskHistoryRetentionLimit = &opts.taskHistoryLimit } @@ -184,5 +188,16 @@ func (opts *swarmOptions) ToSpec(flags *pflag.FlagSet) swarm.Spec { if flags.Changed(flagExternalCA) { spec.CAConfig.ExternalCAs = opts.externalCA.Value() } + if flags.Changed(flagMaxSnapshots) { + spec.Raft.KeepOldSnapshots = &opts.maxSnapshots + } + if flags.Changed(flagSnapshotInterval) { + spec.Raft.SnapshotInterval = opts.snapshotInterval + } +} + +func (opts *swarmOptions) ToSpec(flags *pflag.FlagSet) swarm.Spec { + var spec swarm.Spec + opts.mergeSwarmSpec(&spec, flags) return spec } diff --git a/components/cli/command/swarm/update.go b/components/cli/command/swarm/update.go index 71451e450c..a39f34c881 100644 --- a/components/cli/command/swarm/update.go +++ b/components/cli/command/swarm/update.go @@ -39,10 +39,7 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts swarmOpt return err } - err = mergeSwarm(&swarm, flags) - if err != nil { - return err - } + opts.mergeSwarmSpec(&swarm.Spec, flags) err = client.SwarmUpdate(ctx, swarm.Version, swarm.Spec, updateFlags) if err != nil { @@ -53,31 +50,3 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts swarmOpt return nil } - -func mergeSwarm(swarm *swarm.Swarm, flags *pflag.FlagSet) error { - spec := &swarm.Spec - - if flags.Changed(flagTaskHistoryLimit) { - taskHistoryRetentionLimit, _ := flags.GetInt64(flagTaskHistoryLimit) - spec.Orchestration.TaskHistoryRetentionLimit = &taskHistoryRetentionLimit - } - - if flags.Changed(flagDispatcherHeartbeat) { - if v, err := flags.GetDuration(flagDispatcherHeartbeat); err == nil { - spec.Dispatcher.HeartbeatPeriod = v - } - } - - if flags.Changed(flagCertExpiry) { - if v, err := flags.GetDuration(flagCertExpiry); err == nil { - spec.CAConfig.NodeCertExpiry = v - } - } - - if flags.Changed(flagExternalCA) { - value := flags.Lookup(flagExternalCA).Value.(*ExternalCAOption) - spec.CAConfig.ExternalCAs = value.Value() - } - - return nil -} diff --git a/components/cli/command/system/info.go b/components/cli/command/system/info.go index 7ab658c136..5ea23ed430 100644 --- a/components/cli/command/system/info.go +++ b/components/cli/command/system/info.go @@ -114,6 +114,9 @@ func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error { fmt.Fprintf(dockerCli.Out(), " Task History Retention Limit: %d\n", taskHistoryRetentionLimit) fmt.Fprintf(dockerCli.Out(), " Raft:\n") fmt.Fprintf(dockerCli.Out(), " Snapshot Interval: %d\n", info.Swarm.Cluster.Spec.Raft.SnapshotInterval) + if info.Swarm.Cluster.Spec.Raft.KeepOldSnapshots != nil { + fmt.Fprintf(dockerCli.Out(), " Number of Old Snapshots to Retain: %d\n", *info.Swarm.Cluster.Spec.Raft.KeepOldSnapshots) + } fmt.Fprintf(dockerCli.Out(), " Heartbeat Tick: %d\n", info.Swarm.Cluster.Spec.Raft.HeartbeatTick) fmt.Fprintf(dockerCli.Out(), " Election Tick: %d\n", info.Swarm.Cluster.Spec.Raft.ElectionTick) fmt.Fprintf(dockerCli.Out(), " Dispatcher:\n") From e9737d72023d1cbe6fa562441a76f01c0c274707 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 19 Oct 2016 17:07:44 -0700 Subject: [PATCH 296/978] Add custom DNS settings to service definition This fix tries to fix the issue raised in 24391 about allowing custom DNS settings to service definition. This fix adds `DNSConfig` (`Nameservers`, `Options`, `Search`) to service definition, as well as `--dns`, `--dns-opt`, and `dns-search` to `service create`. An integration test has been added to cover the changes in this fix. This fix fixes 24391. A PR in swarmkit will be created separately. Signed-off-by: Yong Tang Upstream-commit: f40b12d0f73b7ecc670a3266344086dcda71742d Component: cli --- components/cli/command/service/create.go | 3 ++ components/cli/command/service/opts.go | 36 ++++++++++++++++-------- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/components/cli/command/service/create.go b/components/cli/command/service/create.go index e2c4c4d116..6aca4635ae 100644 --- a/components/cli/command/service/create.go +++ b/components/cli/command/service/create.go @@ -41,6 +41,9 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.StringSliceVar(&opts.networks, flagNetwork, []string{}, "Network attachments") flags.VarP(&opts.endpoint.ports, flagPublish, "p", "Publish a port as a node port") flags.StringSliceVar(&opts.groups, flagGroup, []string{}, "Set one or more supplementary user groups for the container") + flags.Var(&opts.dns, flagDNS, "Set custom DNS servers") + flags.Var(&opts.dnsOptions, flagDNSOptions, "Set DNS options") + flags.Var(&opts.dnsSearch, flagDNSSearch, "Set custom DNS search domains") flags.SetInterspersed(false) return cmd diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index 989fd18b8f..5f1ca86344 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -296,6 +296,9 @@ type serviceOptions struct { groups []string tty bool mounts opts.MountOpt + dns opts.ListOpts + dnsSearch opts.ListOpts + dnsOptions opts.ListOpts resources resourceOptions stopGrace DurationOpt @@ -325,7 +328,10 @@ func newServiceOptions() *serviceOptions { endpoint: endpointOptions{ ports: opts.NewListOpts(ValidatePort), }, - logDriver: newLogDriverOptions(), + logDriver: newLogDriverOptions(), + dns: opts.NewListOpts(opts.ValidateIPAddress), + dnsOptions: opts.NewListOpts(nil), + dnsSearch: opts.NewListOpts(opts.ValidateDNSSearch), } } @@ -358,16 +364,21 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { }, TaskTemplate: swarm.TaskSpec{ ContainerSpec: swarm.ContainerSpec{ - Image: opts.image, - Args: opts.args, - Env: currentEnv, - Hostname: opts.hostname, - Labels: runconfigopts.ConvertKVStringsToMap(opts.containerLabels.GetAll()), - Dir: opts.workdir, - User: opts.user, - Groups: opts.groups, - TTY: opts.tty, - Mounts: opts.mounts.Value(), + Image: opts.image, + Args: opts.args, + Env: currentEnv, + Hostname: opts.hostname, + Labels: runconfigopts.ConvertKVStringsToMap(opts.containerLabels.GetAll()), + Dir: opts.workdir, + User: opts.user, + Groups: opts.groups, + TTY: opts.tty, + Mounts: opts.mounts.Value(), + DNSConfig: &swarm.DNSConfig{ + Nameservers: opts.dns.GetAll(), + Search: opts.dnsSearch.GetAll(), + Options: opts.dnsOptions.GetAll(), + }, StopGracePeriod: opts.stopGrace.Value(), }, Networks: convertNetworks(opts.networks), @@ -463,6 +474,9 @@ const ( flagContainerLabel = "container-label" flagContainerLabelRemove = "container-label-rm" flagContainerLabelAdd = "container-label-add" + flagDNS = "dns" + flagDNSOptions = "dns-opt" + flagDNSSearch = "dns-search" flagEndpointMode = "endpoint-mode" flagHostname = "hostname" flagEnv = "env" From 1c91091ad7c736cfa7501832b8483a2cee5c9bd2 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 26 Oct 2016 20:05:39 -0700 Subject: [PATCH 297/978] Add custom DNS settings to service update This fix adds `--dns-add`, `--dns-rm`, `--dns-opt-add`, `--dns-opt-rm`, `--dns-search-add` and `--dns-search-rm` to `service update`. An integration test and a unit test have been added to cover the changes in this fix. Signed-off-by: Yong Tang Upstream-commit: 49e528e18ac518c32c25dd67aa6c6f65f8e9a971 Component: cli --- components/cli/command/service/opts.go | 8 +- components/cli/command/service/update.go | 81 +++++++++++++++++++ components/cli/command/service/update_test.go | 46 +++++++++++ 3 files changed, 134 insertions(+), 1 deletion(-) diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index 5f1ca86344..7a5db67b79 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -475,8 +475,14 @@ const ( flagContainerLabelRemove = "container-label-rm" flagContainerLabelAdd = "container-label-add" flagDNS = "dns" - flagDNSOptions = "dns-opt" + flagDNSRemove = "dns-rm" + flagDNSAdd = "dns-add" + flagDNSOptions = "dns-options" + flagDNSOptionsRemove = "dns-options-rm" + flagDNSOptionsAdd = "dns-options-add" flagDNSSearch = "dns-search" + flagDNSSearchRemove = "dns-search-rm" + flagDNSSearchAdd = "dns-search-add" flagEndpointMode = "endpoint-mode" flagHostname = "hostname" flagEnv = "env" diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index 32a23f23ff..d3088720a0 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -48,6 +48,9 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Var(newListOptsVar(), flagMountRemove, "Remove a mount by its target path") flags.Var(newListOptsVar(), flagPublishRemove, "Remove a published port by its target port") flags.Var(newListOptsVar(), flagConstraintRemove, "Remove a constraint") + flags.Var(newListOptsVar(), flagDNSRemove, "Remove custom DNS servers") + flags.Var(newListOptsVar(), flagDNSOptionsRemove, "Remove DNS options") + flags.Var(newListOptsVar(), flagDNSSearchRemove, "Remove DNS search domains") flags.Var(&opts.labels, flagLabelAdd, "Add or update a service label") flags.Var(&opts.containerLabels, flagContainerLabelAdd, "Add or update a container label") flags.Var(&opts.env, flagEnvAdd, "Add or update an environment variable") @@ -55,6 +58,10 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.StringSliceVar(&opts.constraints, flagConstraintAdd, []string{}, "Add or update a placement constraint") flags.Var(&opts.endpoint.ports, flagPublishAdd, "Add or update a published port") flags.StringSliceVar(&opts.groups, flagGroupAdd, []string{}, "Add an additional supplementary user group to the container") + flags.Var(&opts.dns, flagDNSAdd, "Add or update custom DNS servers") + flags.Var(&opts.dnsOptions, flagDNSOptionsAdd, "Add or update DNS options") + flags.Var(&opts.dnsSearch, flagDNSSearchAdd, "Add or update custom DNS search domains") + return cmd } @@ -257,6 +264,15 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error { } } + if anyChanged(flags, flagDNSAdd, flagDNSRemove, flagDNSOptionsAdd, flagDNSOptionsRemove, flagDNSSearchAdd, flagDNSSearchRemove) { + if cspec.DNSConfig == nil { + cspec.DNSConfig = &swarm.DNSConfig{} + } + if err := updateDNSConfig(flags, &cspec.DNSConfig); err != nil { + return err + } + } + if err := updateLogDriver(flags, &spec.TaskTemplate); err != nil { return err } @@ -484,6 +500,71 @@ func updateGroups(flags *pflag.FlagSet, groups *[]string) error { return nil } +func removeDuplicates(entries []string) []string { + hit := map[string]bool{} + newEntries := []string{} + for _, v := range entries { + if !hit[v] { + newEntries = append(newEntries, v) + hit[v] = true + } + } + return newEntries +} + +func updateDNSConfig(flags *pflag.FlagSet, config **swarm.DNSConfig) error { + newConfig := &swarm.DNSConfig{} + + nameservers := (*config).Nameservers + if flags.Changed(flagDNSAdd) { + values := flags.Lookup(flagDNSAdd).Value.(*opts.ListOpts).GetAll() + nameservers = append(nameservers, values...) + } + nameservers = removeDuplicates(nameservers) + toRemove := buildToRemoveSet(flags, flagDNSRemove) + for _, nameserver := range nameservers { + if _, exists := toRemove[nameserver]; !exists { + newConfig.Nameservers = append(newConfig.Nameservers, nameserver) + + } + } + // Sort so that result is predictable. + sort.Strings(newConfig.Nameservers) + + search := (*config).Search + if flags.Changed(flagDNSSearchAdd) { + values := flags.Lookup(flagDNSSearchAdd).Value.(*opts.ListOpts).GetAll() + search = append(search, values...) + } + search = removeDuplicates(search) + toRemove = buildToRemoveSet(flags, flagDNSSearchRemove) + for _, entry := range search { + if _, exists := toRemove[entry]; !exists { + newConfig.Search = append(newConfig.Search, entry) + } + } + // Sort so that result is predictable. + sort.Strings(newConfig.Search) + + options := (*config).Options + if flags.Changed(flagDNSOptionsAdd) { + values := flags.Lookup(flagDNSOptionsAdd).Value.(*opts.ListOpts).GetAll() + options = append(options, values...) + } + options = removeDuplicates(options) + toRemove = buildToRemoveSet(flags, flagDNSOptionsRemove) + for _, option := range options { + if _, exists := toRemove[option]; !exists { + newConfig.Options = append(newConfig.Options, option) + } + } + // Sort so that result is predictable. + sort.Strings(newConfig.Options) + + *config = newConfig + return nil +} + type byPortConfig []swarm.PortConfig func (r byPortConfig) Len() int { return len(r) } diff --git a/components/cli/command/service/update_test.go b/components/cli/command/service/update_test.go index 2123d1b794..91829b8615 100644 --- a/components/cli/command/service/update_test.go +++ b/components/cli/command/service/update_test.go @@ -120,6 +120,52 @@ func TestUpdateGroups(t *testing.T) { assert.Equal(t, groups[2], "wheel") } +func TestUpdateDNSConfig(t *testing.T) { + flags := newUpdateCommand(nil).Flags() + + // IPv4, with duplicates + flags.Set("dns-add", "1.1.1.1") + flags.Set("dns-add", "1.1.1.1") + flags.Set("dns-add", "2.2.2.2") + flags.Set("dns-rm", "3.3.3.3") + flags.Set("dns-rm", "2.2.2.2") + // IPv6 + flags.Set("dns-add", "2001:db8:abc8::1") + // Invalid dns record + assert.Error(t, flags.Set("dns-add", "x.y.z.w"), "x.y.z.w is not an ip address") + + // domains with duplicates + flags.Set("dns-search-add", "example.com") + flags.Set("dns-search-add", "example.com") + flags.Set("dns-search-add", "example.org") + flags.Set("dns-search-rm", "example.org") + // Invalid dns search domain + assert.Error(t, flags.Set("dns-search-add", "example$com"), "example$com is not a valid domain") + + flags.Set("dns-options-add", "ndots:9") + flags.Set("dns-options-rm", "timeout:3") + + config := &swarm.DNSConfig{ + Nameservers: []string{"3.3.3.3", "5.5.5.5"}, + Search: []string{"localdomain"}, + Options: []string{"timeout:3"}, + } + + updateDNSConfig(flags, &config) + + assert.Equal(t, len(config.Nameservers), 3) + assert.Equal(t, config.Nameservers[0], "1.1.1.1") + assert.Equal(t, config.Nameservers[1], "2001:db8:abc8::1") + assert.Equal(t, config.Nameservers[2], "5.5.5.5") + + assert.Equal(t, len(config.Search), 2) + assert.Equal(t, config.Search[0], "example.com") + assert.Equal(t, config.Search[1], "localdomain") + + assert.Equal(t, len(config.Options), 1) + assert.Equal(t, config.Options[0], "ndots:9") +} + func TestUpdateMounts(t *testing.T) { flags := newUpdateCommand(nil).Flags() flags.Set("mount-add", "type=volume,source=vol2,target=/toadd") From a0d17c1ef54b53e02d4a9467b88a92cb72c5349b Mon Sep 17 00:00:00 2001 From: Andrea Luzzardi Date: Fri, 4 Nov 2016 19:23:07 -0700 Subject: [PATCH 298/978] service ps: Truncate Task IDs - Refactored to move resolution code into the idresolver - Made `ps` output more bearable by shortening service IDs in task names Signed-off-by: Andrea Luzzardi Upstream-commit: 5834d378e020b37c605b9da2014d1f22e3101fca Component: cli --- .../cli/command/idresolver/idresolver.go | 22 ++++++++++++++++- components/cli/command/task/print.go | 24 ++++--------------- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/components/cli/command/idresolver/idresolver.go b/components/cli/command/idresolver/idresolver.go index ad0d96735d..511b1a8f54 100644 --- a/components/cli/command/idresolver/idresolver.go +++ b/components/cli/command/idresolver/idresolver.go @@ -7,6 +7,7 @@ import ( "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/client" + "github.com/docker/docker/pkg/stringid" ) // IDResolver provides ID to Name resolution. @@ -26,7 +27,7 @@ func New(client client.APIClient, noResolve bool) *IDResolver { } func (r *IDResolver) get(ctx context.Context, t interface{}, id string) (string, error) { - switch t.(type) { + switch t := t.(type) { case swarm.Node: node, _, err := r.client.NodeInspectWithRaw(ctx, id) if err != nil { @@ -45,6 +46,25 @@ func (r *IDResolver) get(ctx context.Context, t interface{}, id string) (string, return id, nil } return service.Spec.Annotations.Name, nil + case swarm.Task: + // If the caller passes the full task there's no need to do a lookup. + if t.ID == "" { + var err error + + t, _, err = r.client.TaskInspectWithRaw(ctx, id) + if err != nil { + return id, nil + } + } + taskID := stringid.TruncateID(t.ID) + if t.ServiceID == "" { + return taskID, nil + } + service, err := r.Resolve(ctx, swarm.Service{}, t.ServiceID) + if err != nil { + return "", err + } + return fmt.Sprintf("%s.%d.%s", service, t.Slot, taskID), nil default: return "", fmt.Errorf("unsupported type") } diff --git a/components/cli/command/task/print.go b/components/cli/command/task/print.go index b3cdcbe533..45af178a42 100644 --- a/components/cli/command/task/print.go +++ b/components/cli/command/task/print.go @@ -74,38 +74,24 @@ func PrintQuiet(dockerCli *command.DockerCli, tasks []swarm.Task) error { } func print(out io.Writer, ctx context.Context, tasks []swarm.Task, resolver *idresolver.IDResolver, noTrunc bool) error { - prevServiceName := "" + prevService := "" prevSlot := 0 for _, task := range tasks { - serviceName, err := resolver.Resolve(ctx, swarm.Service{}, task.ServiceID) - if err != nil { - return err - } + name, err := resolver.Resolve(ctx, task, task.ID) + nodeValue, err := resolver.Resolve(ctx, swarm.Node{}, task.NodeID) if err != nil { return err } - name := task.Annotations.Name - // TODO: This is the fallback .. in case task name is not present in - // Annotations (upgraded from 1.12). - // We may be able to remove the following in the future. - if name == "" { - if task.Slot != 0 { - name = fmt.Sprintf("%v.%v.%v", serviceName, task.Slot, task.ID) - } else { - name = fmt.Sprintf("%v.%v.%v", serviceName, task.NodeID, task.ID) - } - } - // Indent the name if necessary indentedName := name // Since the new format of the task name is .., we should only compare // and here. - if prevServiceName == serviceName && prevSlot == task.Slot { + if prevService == task.ServiceID && prevSlot == task.Slot { indentedName = fmt.Sprintf(" \\_ %s", indentedName) } - prevServiceName = serviceName + prevService = task.ServiceID prevSlot = task.Slot // Trim and quote the error message. From 24e894456a029695be58203a10cf3027e65294a0 Mon Sep 17 00:00:00 2001 From: allencloud Date: Wed, 9 Nov 2016 14:22:06 +0800 Subject: [PATCH 299/978] add short flag for force Signed-off-by: allencloud Upstream-commit: 31c5b957e274222c037ccbf42c80e49c624104dc Component: cli --- components/cli/command/node/remove.go | 2 +- components/cli/command/swarm/leave.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/command/node/remove.go b/components/cli/command/node/remove.go index 3b89db8661..19b4a96631 100644 --- a/components/cli/command/node/remove.go +++ b/components/cli/command/node/remove.go @@ -29,7 +29,7 @@ func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { }, } flags := cmd.Flags() - flags.BoolVar(&opts.force, "force", false, "Force remove a node from the swarm") + flags.BoolVarP(&opts.force, "force", "f", false, "Force remove a node from the swarm") return cmd } diff --git a/components/cli/command/swarm/leave.go b/components/cli/command/swarm/leave.go index ae13884154..1ffaa3fcc9 100644 --- a/components/cli/command/swarm/leave.go +++ b/components/cli/command/swarm/leave.go @@ -27,7 +27,7 @@ func newLeaveCommand(dockerCli *command.DockerCli) *cobra.Command { } flags := cmd.Flags() - flags.BoolVar(&opts.force, "force", false, "Force this node to leave the swarm, ignoring warnings") + flags.BoolVarP(&opts.force, "force", "f", false, "Force this node to leave the swarm, ignoring warnings") return cmd } From cc55a23f4c3be880d29d6a50b5d74124881d9a62 Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Wed, 9 Nov 2016 17:43:10 +0800 Subject: [PATCH 300/978] Update function name for TestCalculBlockIO Signed-off-by: yuexiao-wang Upstream-commit: 18caa28b669871adb11dff0f6914d3094d2d2c4e Component: cli --- components/cli/command/container/stats_unit_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/container/stats_unit_test.go b/components/cli/command/container/stats_unit_test.go index fc6563c4d9..828d634c8a 100644 --- a/components/cli/command/container/stats_unit_test.go +++ b/components/cli/command/container/stats_unit_test.go @@ -6,7 +6,7 @@ import ( "github.com/docker/docker/api/types" ) -func TestCalculBlockIO(t *testing.T) { +func TestCalculateBlockIO(t *testing.T) { blkio := types.BlkioStats{ IoServiceBytesRecursive: []types.BlkioStatEntry{{8, 0, "read", 1234}, {8, 1, "read", 4567}, {8, 0, "write", 123}, {8, 1, "write", 456}}, } From 19b738db2f95af748106be28eeb096362c68b4de Mon Sep 17 00:00:00 2001 From: milindchawre Date: Tue, 25 Oct 2016 12:22:07 +0000 Subject: [PATCH 301/978] Fixes #24083 : Improving cli help for flags with duration option Signed-off-by: milindchawre Upstream-commit: e87262cc2dff9affb58ab984dc1ab7af7d615072 Component: cli --- components/cli/command/service/opts.go | 4 ++-- components/cli/command/swarm/opts.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index 7a5db67b79..8de8b173ef 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -446,8 +446,8 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { flags.Var(&opts.restartPolicy.window, flagRestartWindow, "Window used to evaluate the restart policy") flags.Uint64Var(&opts.update.parallelism, flagUpdateParallelism, 1, "Maximum number of tasks updated simultaneously (0 to update all at once)") - flags.DurationVar(&opts.update.delay, flagUpdateDelay, time.Duration(0), "Delay between updates") - flags.DurationVar(&opts.update.monitor, flagUpdateMonitor, time.Duration(0), "Duration after each task update to monitor for failure") + flags.DurationVar(&opts.update.delay, flagUpdateDelay, time.Duration(0), "Delay between updates (ns|us|ms|s|m|h) (default 0s)") + flags.DurationVar(&opts.update.monitor, flagUpdateMonitor, time.Duration(0), "Duration after each task update to monitor for failure (ns|us|ms|s|m|h) (default 0s)") flags.StringVar(&opts.update.onFailure, flagUpdateFailureAction, "pause", "Action on update failure (pause|continue)") flags.Float32Var(&opts.update.maxFailureRatio, flagUpdateMaxFailureRatio, 0, "Failure rate to tolerate during an update") diff --git a/components/cli/command/swarm/opts.go b/components/cli/command/swarm/opts.go index af36a71673..ce5a9b1de0 100644 --- a/components/cli/command/swarm/opts.go +++ b/components/cli/command/swarm/opts.go @@ -168,8 +168,8 @@ func parseExternalCA(caSpec string) (*swarm.ExternalCA, error) { func addSwarmFlags(flags *pflag.FlagSet, opts *swarmOptions) { flags.Int64Var(&opts.taskHistoryLimit, flagTaskHistoryLimit, 5, "Task history retention limit") - flags.DurationVar(&opts.dispatcherHeartbeat, flagDispatcherHeartbeat, time.Duration(5*time.Second), "Dispatcher heartbeat period") - flags.DurationVar(&opts.nodeCertExpiry, flagCertExpiry, time.Duration(90*24*time.Hour), "Validity period for node certificates") + flags.DurationVar(&opts.dispatcherHeartbeat, flagDispatcherHeartbeat, time.Duration(5*time.Second), "Dispatcher heartbeat period (ns|us|ms|s|m|h) (default 5s)") + flags.DurationVar(&opts.nodeCertExpiry, flagCertExpiry, time.Duration(90*24*time.Hour), "Validity period for node certificates (ns|us|ms|s|m|h) (default 2160h0m0s)") flags.Var(&opts.externalCA, flagExternalCA, "Specifications of one or more certificate signing endpoints") flags.Uint64Var(&opts.maxSnapshots, flagMaxSnapshots, 0, "Number of additional Raft snapshots to retain") flags.Uint64Var(&opts.snapshotInterval, flagSnapshotInterval, 10000, "Number of log entries between Raft snapshots") From 180746c100c07d9fc925edb3c89b0ea66b889781 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Tue, 8 Nov 2016 07:06:07 -0800 Subject: [PATCH 302/978] Remove `-ptr` from the help output of `service create` This fix is based on the comment: https://github.com/docker/docker/pull/28147#discussion_r86996347 Previously the output string of the `DurationOpt` is `duration-ptr` and `Uint64Opt` is `uint64-ptr`. While it is clear to developers, for a normal user `-ptr` might not be very informative. On the other hand, the default value of `DurationOpt` and `Uint64Opt` has already been quite informative: `none`. That means if no flag provided, the value will be treated as none. (like a ptr with nil as the default) For that reason this fix removes the `-ptr`. Also, the output in the docs of `service create` has been quite out-of-sync with the true output. So this fix updates the docs to have the most up-to-date help output of `service create --help`. This fix is related to #28147. Signed-off-by: Yong Tang Upstream-commit: 071c746e5ef5e407d6769a0fd436e8cf50b371a9 Component: cli --- components/cli/command/service/create.go | 6 +-- components/cli/command/service/opts.go | 53 +++++++++++++++++------- components/cli/command/service/update.go | 34 +++++++++------ 3 files changed, 62 insertions(+), 31 deletions(-) diff --git a/components/cli/command/service/create.go b/components/cli/command/service/create.go index 6aca4635ae..d6c3ebdb9c 100644 --- a/components/cli/command/service/create.go +++ b/components/cli/command/service/create.go @@ -37,10 +37,10 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.VarP(&opts.env, flagEnv, "e", "Set environment variables") flags.Var(&opts.envFile, flagEnvFile, "Read in a file of environment variables") flags.Var(&opts.mounts, flagMount, "Attach a filesystem mount to the service") - flags.StringSliceVar(&opts.constraints, flagConstraint, []string{}, "Placement constraints") - flags.StringSliceVar(&opts.networks, flagNetwork, []string{}, "Network attachments") + flags.Var(&opts.constraints, flagConstraint, "Placement constraints") + flags.Var(&opts.networks, flagNetwork, "Network attachments") flags.VarP(&opts.endpoint.ports, flagPublish, "p", "Publish a port as a node port") - flags.StringSliceVar(&opts.groups, flagGroup, []string{}, "Set one or more supplementary user groups for the container") + flags.Var(&opts.groups, flagGroup, "Set one or more supplementary user groups for the container") flags.Var(&opts.dns, flagDNS, "Set custom DNS servers") flags.Var(&opts.dnsOptions, flagDNSOptions, "Set DNS options") flags.Var(&opts.dnsSearch, flagDNSSearch, "Set custom DNS search domains") diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index 8de8b173ef..827c4e5cdc 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -32,7 +32,7 @@ func (m *memBytes) Set(value string) error { } func (m *memBytes) Type() string { - return "MemoryBytes" + return "bytes" } func (m *memBytes) Value() int64 { @@ -71,9 +71,9 @@ func (d *DurationOpt) Set(s string) error { return err } -// Type returns the type of this option +// Type returns the type of this option, which will be displayed in `--help` output func (d *DurationOpt) Type() string { - return "duration-ptr" + return "duration" } // String returns a string repr of this option @@ -101,9 +101,9 @@ func (i *Uint64Opt) Set(s string) error { return err } -// Type returns the type of this option +// Type returns the type of this option, which will be displayed in `--help` output func (i *Uint64Opt) Type() string { - return "uint64-ptr" + return "uint" } // String returns a string repr of this option @@ -119,12 +119,32 @@ func (i *Uint64Opt) Value() *uint64 { return i.value } +type floatValue float32 + +func (f *floatValue) Set(s string) error { + v, err := strconv.ParseFloat(s, 32) + *f = floatValue(v) + return err +} + +func (f *floatValue) Type() string { + return "float" +} + +func (f *floatValue) String() string { + return strconv.FormatFloat(float64(*f), 'g', -1, 32) +} + +func (f *floatValue) Value() float32 { + return float32(*f) +} + type updateOptions struct { parallelism uint64 delay time.Duration monitor time.Duration onFailure string - maxFailureRatio float32 + maxFailureRatio floatValue } type resourceOptions struct { @@ -293,7 +313,7 @@ type serviceOptions struct { envFile opts.ListOpts workdir string user string - groups []string + groups opts.ListOpts tty bool mounts opts.MountOpt dns opts.ListOpts @@ -307,9 +327,9 @@ type serviceOptions struct { mode string restartPolicy restartPolicyOptions - constraints []string + constraints opts.ListOpts update updateOptions - networks []string + networks opts.ListOpts endpoint endpointOptions registryAuth bool @@ -322,16 +342,19 @@ type serviceOptions struct { func newServiceOptions() *serviceOptions { return &serviceOptions{ labels: opts.NewListOpts(runconfigopts.ValidateEnv), + constraints: opts.NewListOpts(nil), containerLabels: opts.NewListOpts(runconfigopts.ValidateEnv), env: opts.NewListOpts(runconfigopts.ValidateEnv), envFile: opts.NewListOpts(nil), endpoint: endpointOptions{ ports: opts.NewListOpts(ValidatePort), }, + groups: opts.NewListOpts(nil), logDriver: newLogDriverOptions(), dns: opts.NewListOpts(opts.ValidateIPAddress), dnsOptions: opts.NewListOpts(nil), dnsSearch: opts.NewListOpts(opts.ValidateDNSSearch), + networks: opts.NewListOpts(nil), } } @@ -371,7 +394,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { Labels: runconfigopts.ConvertKVStringsToMap(opts.containerLabels.GetAll()), Dir: opts.workdir, User: opts.user, - Groups: opts.groups, + Groups: opts.groups.GetAll(), TTY: opts.tty, Mounts: opts.mounts.Value(), DNSConfig: &swarm.DNSConfig{ @@ -381,22 +404,22 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { }, StopGracePeriod: opts.stopGrace.Value(), }, - Networks: convertNetworks(opts.networks), + Networks: convertNetworks(opts.networks.GetAll()), Resources: opts.resources.ToResourceRequirements(), RestartPolicy: opts.restartPolicy.ToRestartPolicy(), Placement: &swarm.Placement{ - Constraints: opts.constraints, + Constraints: opts.constraints.GetAll(), }, LogDriver: opts.logDriver.toLogDriver(), }, - Networks: convertNetworks(opts.networks), + Networks: convertNetworks(opts.networks.GetAll()), Mode: swarm.ServiceMode{}, UpdateConfig: &swarm.UpdateConfig{ Parallelism: opts.update.parallelism, Delay: opts.update.delay, Monitor: opts.update.monitor, FailureAction: opts.update.onFailure, - MaxFailureRatio: opts.update.maxFailureRatio, + MaxFailureRatio: opts.update.maxFailureRatio.Value(), }, EndpointSpec: opts.endpoint.ToEndpointSpec(), } @@ -449,7 +472,7 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { flags.DurationVar(&opts.update.delay, flagUpdateDelay, time.Duration(0), "Delay between updates (ns|us|ms|s|m|h) (default 0s)") flags.DurationVar(&opts.update.monitor, flagUpdateMonitor, time.Duration(0), "Duration after each task update to monitor for failure (ns|us|ms|s|m|h) (default 0s)") flags.StringVar(&opts.update.onFailure, flagUpdateFailureAction, "pause", "Action on update failure (pause|continue)") - flags.Float32Var(&opts.update.maxFailureRatio, flagUpdateMaxFailureRatio, 0, "Failure rate to tolerate during an update") + flags.Var(&opts.update.maxFailureRatio, flagUpdateMaxFailureRatio, "Failure rate to tolerate during an update") flags.StringVar(&opts.endpoint.mode, flagEndpointMode, "", "Endpoint mode (vip or dnsrr)") diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index d3088720a0..4a77229497 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -55,9 +55,9 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Var(&opts.containerLabels, flagContainerLabelAdd, "Add or update a container label") flags.Var(&opts.env, flagEnvAdd, "Add or update an environment variable") flags.Var(&opts.mounts, flagMountAdd, "Add or update a mount on a service") - flags.StringSliceVar(&opts.constraints, flagConstraintAdd, []string{}, "Add or update a placement constraint") + flags.Var(&opts.constraints, flagConstraintAdd, "Add or update a placement constraint") flags.Var(&opts.endpoint.ports, flagPublishAdd, "Add or update a published port") - flags.StringSliceVar(&opts.groups, flagGroupAdd, []string{}, "Add an additional supplementary user group to the container") + flags.Var(&opts.groups, flagGroupAdd, "Add an additional supplementary user group to the container") flags.Var(&opts.dns, flagDNSAdd, "Add or update custom DNS servers") flags.Var(&opts.dnsOptions, flagDNSOptionsAdd, "Add or update DNS options") flags.Var(&opts.dnsSearch, flagDNSSearchAdd, "Add or update custom DNS search domains") @@ -139,9 +139,9 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error { } } - updateFloat32 := func(flag string, field *float32) { + updateFloatValue := func(flag string, field *float32) { if flags.Changed(flag) { - *field, _ = flags.GetFloat32(flag) + *field = flags.Lookup(flag).Value.(*floatValue).Value() } } @@ -238,7 +238,7 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error { updateDuration(flagUpdateDelay, &spec.UpdateConfig.Delay) updateDuration(flagUpdateMonitor, &spec.UpdateConfig.Monitor) updateString(flagUpdateFailureAction, &spec.UpdateConfig.FailureAction) - updateFloat32(flagUpdateMaxFailureRatio, &spec.UpdateConfig.MaxFailureRatio) + updateFloatValue(flagUpdateMaxFailureRatio, &spec.UpdateConfig.MaxFailureRatio) } if flags.Changed(flagEndpointMode) { @@ -322,11 +322,22 @@ func anyChanged(flags *pflag.FlagSet, fields ...string) bool { } func updatePlacement(flags *pflag.FlagSet, placement *swarm.Placement) { - field, _ := flags.GetStringSlice(flagConstraintAdd) - placement.Constraints = append(placement.Constraints, field...) - + if flags.Changed(flagConstraintAdd) { + values := flags.Lookup(flagConstraintAdd).Value.(*opts.ListOpts).GetAll() + placement.Constraints = append(placement.Constraints, values...) + } toRemove := buildToRemoveSet(flags, flagConstraintRemove) - placement.Constraints = removeItems(placement.Constraints, toRemove, itemKey) + + newConstraints := []string{} + for _, constraint := range placement.Constraints { + if _, exists := toRemove[constraint]; !exists { + newConstraints = append(newConstraints, constraint) + } + } + // Sort so that result is predictable. + sort.Strings(newConstraints) + + placement.Constraints = newConstraints } func updateContainerLabels(flags *pflag.FlagSet, field *map[string]string) { @@ -479,10 +490,7 @@ func updateMounts(flags *pflag.FlagSet, mounts *[]mounttypes.Mount) error { func updateGroups(flags *pflag.FlagSet, groups *[]string) error { if flags.Changed(flagGroupAdd) { - values, err := flags.GetStringSlice(flagGroupAdd) - if err != nil { - return err - } + values := flags.Lookup(flagGroupAdd).Value.(*opts.ListOpts).GetAll() *groups = append(*groups, values...) } toRemove := buildToRemoveSet(flags, flagGroupRemove) From f1194715ab7d80c937d0b58d9f1e9f457cd3b7ae Mon Sep 17 00:00:00 2001 From: Kenfe-Mickael Laventure Date: Mon, 24 Oct 2016 15:18:58 -0700 Subject: [PATCH 303/978] Add expected 3rd party binaries commit ids to info Signed-off-by: Kenfe-Mickael Laventure Upstream-commit: 801167fcecd2d8cdeb94ee2ab7444c4bf71268eb Component: cli --- components/cli/command/system/info.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/components/cli/command/system/info.go b/components/cli/command/system/info.go index 5ea23ed430..fceef59237 100644 --- a/components/cli/command/system/info.go +++ b/components/cli/command/system/info.go @@ -143,6 +143,22 @@ func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error { } if info.OSType == "linux" { + fmt.Fprintf(dockerCli.Out(), "Init Binary: %v\n", info.InitBinary) + + for _, ci := range []struct { + Name string + Commit types.Commit + }{ + {"containerd", info.ContainerdCommit}, + {"runc", info.RuncCommit}, + {"init", info.InitCommit}, + } { + fmt.Fprintf(dockerCli.Out(), "%s version: %s", ci.Name, ci.Commit.ID) + if ci.Commit.ID != ci.Commit.Expected { + fmt.Fprintf(dockerCli.Out(), " (expected: %s)", ci.Commit.Expected) + } + fmt.Fprintf(dockerCli.Out(), "\n") + } if len(info.SecurityOptions) != 0 { fmt.Fprintf(dockerCli.Out(), "Security Options:\n") for _, o := range info.SecurityOptions { From a0307eb205ef5330ab67a1fc26d4436d83779c32 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Wed, 19 Oct 2016 12:22:02 -0400 Subject: [PATCH 304/978] 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 Upstream-commit: 1be644fbcf68872433e56914e6c4357920d084ca Component: cli --- components/cli/command/commands/commands.go | 2 + components/cli/command/secret/cmd.go | 29 +++++++ components/cli/command/secret/create.go | 57 +++++++++++++ components/cli/command/secret/inspect.go | 42 ++++++++++ components/cli/command/secret/ls.go | 62 ++++++++++++++ components/cli/command/secret/remove.go | 43 ++++++++++ components/cli/command/service/create.go | 7 ++ components/cli/command/service/opts.go | 17 ++++ components/cli/command/service/parse.go | 92 +++++++++++++++++++++ 9 files changed, 351 insertions(+) create mode 100644 components/cli/command/secret/cmd.go create mode 100644 components/cli/command/secret/create.go create mode 100644 components/cli/command/secret/inspect.go create mode 100644 components/cli/command/secret/ls.go create mode 100644 components/cli/command/secret/remove.go create mode 100644 components/cli/command/service/parse.go diff --git a/components/cli/command/commands/commands.go b/components/cli/command/commands/commands.go index fad709bca1..d64d5680cc 100644 --- a/components/cli/command/commands/commands.go +++ b/components/cli/command/commands/commands.go @@ -11,6 +11,7 @@ import ( "github.com/docker/docker/cli/command/node" "github.com/docker/docker/cli/command/plugin" "github.com/docker/docker/cli/command/registry" + "github.com/docker/docker/cli/command/secret" "github.com/docker/docker/cli/command/service" "github.com/docker/docker/cli/command/stack" "github.com/docker/docker/cli/command/swarm" @@ -25,6 +26,7 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) { node.NewNodeCommand(dockerCli), service.NewServiceCommand(dockerCli), swarm.NewSwarmCommand(dockerCli), + secret.NewSecretCommand(dockerCli), container.NewContainerCommand(dockerCli), image.NewImageCommand(dockerCli), system.NewSystemCommand(dockerCli), diff --git a/components/cli/command/secret/cmd.go b/components/cli/command/secret/cmd.go new file mode 100644 index 0000000000..995300ad77 --- /dev/null +++ b/components/cli/command/secret/cmd.go @@ -0,0 +1,29 @@ +package secret + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" +) + +// NewSecretCommand returns a cobra command for `secret` subcommands +func NewSecretCommand(dockerCli *command.DockerCli) *cobra.Command { + cmd := &cobra.Command{ + Use: "secret", + Short: "Manage Docker secrets", + Args: cli.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) + }, + } + cmd.AddCommand( + newSecretListCommand(dockerCli), + newSecretCreateCommand(dockerCli), + newSecretInspectCommand(dockerCli), + newSecretRemoveCommand(dockerCli), + ) + return cmd +} diff --git a/components/cli/command/secret/create.go b/components/cli/command/secret/create.go new file mode 100644 index 0000000000..1c0e933f57 --- /dev/null +++ b/components/cli/command/secret/create.go @@ -0,0 +1,57 @@ +package secret + +import ( + "context" + "fmt" + "io/ioutil" + "os" + + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +type createOptions struct { + name string +} + +func newSecretCreateCommand(dockerCli *command.DockerCli) *cobra.Command { + return &cobra.Command{ + Use: "create [name]", + Short: "Create a secret using stdin as content", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts := createOptions{ + name: args[0], + } + + return runSecretCreate(dockerCli, opts) + }, + } +} + +func runSecretCreate(dockerCli *command.DockerCli, opts createOptions) error { + client := dockerCli.Client() + ctx := context.Background() + + secretData, err := ioutil.ReadAll(os.Stdin) + if err != nil { + return fmt.Errorf("Error reading content from STDIN: %v", err) + } + + spec := swarm.SecretSpec{ + Annotations: swarm.Annotations{ + Name: opts.name, + }, + Data: secretData, + } + + r, err := client.SecretCreate(ctx, spec) + if err != nil { + return err + } + + fmt.Fprintln(dockerCli.Out(), r.ID) + return nil +} diff --git a/components/cli/command/secret/inspect.go b/components/cli/command/secret/inspect.go new file mode 100644 index 0000000000..c8d5cd8f79 --- /dev/null +++ b/components/cli/command/secret/inspect.go @@ -0,0 +1,42 @@ +package secret + +import ( + "context" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/inspect" + "github.com/spf13/cobra" +) + +type inspectOptions struct { + name string + format string +} + +func newSecretInspectCommand(dockerCli *command.DockerCli) *cobra.Command { + opts := inspectOptions{} + cmd := &cobra.Command{ + Use: "inspect [name]", + Short: "Inspect a secret", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.name = args[0] + return runSecretInspect(dockerCli, opts) + }, + } + + cmd.Flags().StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template") + return cmd +} + +func runSecretInspect(dockerCli *command.DockerCli, opts inspectOptions) error { + client := dockerCli.Client() + ctx := context.Background() + + getRef := func(name string) (interface{}, []byte, error) { + return client.SecretInspectWithRaw(ctx, name) + } + + return inspect.Inspect(dockerCli.Out(), []string{opts.name}, opts.format, getRef) +} diff --git a/components/cli/command/secret/ls.go b/components/cli/command/secret/ls.go new file mode 100644 index 0000000000..1befdad9d0 --- /dev/null +++ b/components/cli/command/secret/ls.go @@ -0,0 +1,62 @@ +package secret + +import ( + "context" + "fmt" + "text/tabwriter" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +type listOptions struct { + quiet bool +} + +func newSecretListCommand(dockerCli *command.DockerCli) *cobra.Command { + opts := listOptions{} + + cmd := &cobra.Command{ + Use: "ls", + Short: "List secrets", + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runSecretList(dockerCli, opts) + }, + } + + flags := cmd.Flags() + flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs") + + return cmd +} + +func runSecretList(dockerCli *command.DockerCli, opts listOptions) error { + client := dockerCli.Client() + ctx := context.Background() + + secrets, err := client.SecretList(ctx, types.SecretListOptions{}) + if err != nil { + return err + } + + w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0) + if opts.quiet { + for _, s := range secrets { + fmt.Fprintf(w, "%s\n", s.ID) + } + } else { + fmt.Fprintf(w, "ID\tNAME\tCREATED\tUPDATED\tSIZE") + fmt.Fprintf(w, "\n") + + for _, s := range secrets { + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d\n", s.ID, s.Spec.Annotations.Name, s.Meta.CreatedAt, s.Meta.UpdatedAt, s.SecretSize) + } + } + + w.Flush() + + return nil +} diff --git a/components/cli/command/secret/remove.go b/components/cli/command/secret/remove.go new file mode 100644 index 0000000000..f336c6161a --- /dev/null +++ b/components/cli/command/secret/remove.go @@ -0,0 +1,43 @@ +package secret + +import ( + "context" + "fmt" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" +) + +type removeOptions struct { + ids []string +} + +func newSecretRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { + return &cobra.Command{ + Use: "rm [id]", + Short: "Remove a secret", + Args: cli.RequiresMinArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts := removeOptions{ + ids: args, + } + return runSecretRemove(dockerCli, opts) + }, + } +} + +func runSecretRemove(dockerCli *command.DockerCli, opts removeOptions) error { + client := dockerCli.Client() + ctx := context.Background() + + for _, id := range opts.ids { + if err := client.SecretRemove(ctx, id); err != nil { + return err + } + + fmt.Fprintln(dockerCli.Out(), id) + } + + return nil +} diff --git a/components/cli/command/service/create.go b/components/cli/command/service/create.go index d6c3ebdb9c..8fb9070e67 100644 --- a/components/cli/command/service/create.go +++ b/components/cli/command/service/create.go @@ -58,6 +58,13 @@ func runCreate(dockerCli *command.DockerCli, opts *serviceOptions) error { return err } + // parse and validate secrets + secrets, err := parseSecrets(apiClient, opts.secrets) + if err != nil { + return err + } + service.TaskTemplate.ContainerSpec.Secrets = secrets + ctx := context.Background() // only send auth if flag was set diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index 827c4e5cdc..a4fd08881c 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -191,6 +191,19 @@ func convertNetworks(networks []string) []swarm.NetworkAttachmentConfig { return nets } +func convertSecrets(secrets []string) []*swarm.SecretReference { + sec := []*swarm.SecretReference{} + for _, s := range secrets { + sec = append(sec, &swarm.SecretReference{ + SecretID: s, + Mode: swarm.SecretReferenceFile, + Target: "", + }) + } + + return sec +} + type endpointOptions struct { mode string ports opts.ListOpts @@ -337,6 +350,7 @@ type serviceOptions struct { logDriver logDriverOptions healthcheck healthCheckOptions + secrets []string } func newServiceOptions() *serviceOptions { @@ -403,6 +417,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { Options: opts.dnsOptions.GetAll(), }, StopGracePeriod: opts.stopGrace.Value(), + Secrets: convertSecrets(opts.secrets), }, Networks: convertNetworks(opts.networks.GetAll()), Resources: opts.resources.ToResourceRequirements(), @@ -488,6 +503,7 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { flags.BoolVar(&opts.healthcheck.noHealthcheck, flagNoHealthcheck, false, "Disable any container-specified HEALTHCHECK") flags.BoolVarP(&opts.tty, flagTTY, "t", false, "Allocate a pseudo-TTY") + flags.StringSliceVar(&opts.secrets, flagSecret, []string{}, "Specify secrets to expose to the service") } const ( @@ -553,4 +569,5 @@ const ( flagHealthRetries = "health-retries" flagHealthTimeout = "health-timeout" flagNoHealthcheck = "no-healthcheck" + flagSecret = "secret" ) diff --git a/components/cli/command/service/parse.go b/components/cli/command/service/parse.go new file mode 100644 index 0000000000..41883fb445 --- /dev/null +++ b/components/cli/command/service/parse.go @@ -0,0 +1,92 @@ +package service + +import ( + "context" + "fmt" + "strings" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + swarmtypes "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/client" +) + +// parseSecretString parses the requested secret and returns the secret name +// and target. Expects format SECRET_NAME:TARGET +func parseSecretString(secretString string) (string, string, error) { + tokens := strings.Split(secretString, ":") + + secretName := strings.TrimSpace(tokens[0]) + targetName := "" + + if secretName == "" { + return "", "", fmt.Errorf("invalid secret name provided") + } + + if len(tokens) > 1 { + targetName = strings.TrimSpace(tokens[1]) + if targetName == "" { + return "", "", fmt.Errorf("invalid presentation name provided") + } + } else { + targetName = secretName + } + return secretName, targetName, nil +} + +// parseSecrets retrieves the secrets from the requested names and converts +// them to secret references to use with the spec +func parseSecrets(client client.APIClient, requestedSecrets []string) ([]*swarmtypes.SecretReference, error) { + lookupSecretNames := []string{} + needSecrets := make(map[string]*swarmtypes.SecretReference) + ctx := context.Background() + + for _, secret := range requestedSecrets { + n, t, err := parseSecretString(secret) + if err != nil { + return nil, err + } + + secretRef := &swarmtypes.SecretReference{ + SecretName: n, + Mode: swarmtypes.SecretReferenceFile, + Target: t, + } + + lookupSecretNames = append(lookupSecretNames, n) + needSecrets[n] = secretRef + } + + args := filters.NewArgs() + for _, s := range lookupSecretNames { + args.Add("names", s) + } + + secrets, err := client.SecretList(ctx, types.SecretListOptions{ + Filter: args, + }) + if err != nil { + return nil, err + } + + foundSecrets := make(map[string]*swarmtypes.Secret) + for _, secret := range secrets { + foundSecrets[secret.Spec.Annotations.Name] = &secret + } + + addedSecrets := []*swarmtypes.SecretReference{} + + for secretName, secretRef := range needSecrets { + s, ok := foundSecrets[secretName] + if !ok { + return nil, fmt.Errorf("secret not found: %s", secretName) + } + + // set the id for the ref to properly assign in swarm + // since swarm needs the ID instead of the name + secretRef.SecretID = s.ID + addedSecrets = append(addedSecrets, secretRef) + } + + return addedSecrets, nil +} From 71582157067db5d49840f19fe0946d0d81ff4d61 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Wed, 26 Oct 2016 13:30:53 -0700 Subject: [PATCH 305/978] review changes - fix lint issues - use errors pkg for wrapping errors - cleanup on error when setting up secrets mount - fix erroneous import - remove unneeded switch for secret reference mode - return single mount for secrets instead of slice Signed-off-by: Evan Hazlett Upstream-commit: 3f9494f1d64c9ffe04c17ec976f5a2620c4e6ba9 Component: cli --- components/cli/command/service/parse.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/components/cli/command/service/parse.go b/components/cli/command/service/parse.go index 41883fb445..596d8e50d8 100644 --- a/components/cli/command/service/parse.go +++ b/components/cli/command/service/parse.go @@ -3,6 +3,7 @@ package service import ( "context" "fmt" + "path/filepath" "strings" "github.com/docker/docker/api/types" @@ -31,6 +32,13 @@ func parseSecretString(secretString string) (string, string, error) { } else { targetName = secretName } + + // ensure target is a filename only; no paths allowed + tDir, _ := filepath.Split(targetName) + if tDir != "" { + return "", "", fmt.Errorf("target must not have a path") + } + return secretName, targetName, nil } From d1a4dc8e909a99b795e1a617e79c7e13fbd45b4e Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Wed, 19 Oct 2016 12:22:02 -0400 Subject: [PATCH 306/978] 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 Upstream-commit: 72ff77999cbd4c943ad9e86f30c55a05992f41c4 Component: cli --- components/cli/errors.go | 22 +++++++ components/cli/interface.go | 9 +++ components/cli/secret_create.go | 24 +++++++ components/cli/secret_create_test.go | 57 ++++++++++++++++ components/cli/secret_inspect.go | 34 ++++++++++ components/cli/secret_inspect_test.go | 65 ++++++++++++++++++ components/cli/secret_list.go | 35 ++++++++++ components/cli/secret_list_test.go | 94 +++++++++++++++++++++++++++ components/cli/secret_remove.go | 10 +++ components/cli/secret_remove_test.go | 47 ++++++++++++++ 10 files changed, 397 insertions(+) create mode 100644 components/cli/secret_create.go create mode 100644 components/cli/secret_create_test.go create mode 100644 components/cli/secret_inspect.go create mode 100644 components/cli/secret_inspect_test.go create mode 100644 components/cli/secret_list.go create mode 100644 components/cli/secret_list_test.go create mode 100644 components/cli/secret_remove.go create mode 100644 components/cli/secret_remove_test.go diff --git a/components/cli/errors.go b/components/cli/errors.go index 53e2065332..db7294daa8 100644 --- a/components/cli/errors.go +++ b/components/cli/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/components/cli/interface.go b/components/cli/interface.go index 99b06709b5..49b66b1d17 100644 --- a/components/cli/interface.go +++ b/components/cli/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/components/cli/secret_create.go b/components/cli/secret_create.go new file mode 100644 index 0000000000..de8b041567 --- /dev/null +++ b/components/cli/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/components/cli/secret_create_test.go b/components/cli/secret_create_test.go new file mode 100644 index 0000000000..d264eb6692 --- /dev/null +++ b/components/cli/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/components/cli/secret_inspect.go b/components/cli/secret_inspect.go new file mode 100644 index 0000000000..f774576118 --- /dev/null +++ b/components/cli/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/components/cli/secret_inspect_test.go b/components/cli/secret_inspect_test.go new file mode 100644 index 0000000000..423d986968 --- /dev/null +++ b/components/cli/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/components/cli/secret_list.go b/components/cli/secret_list.go new file mode 100644 index 0000000000..5e9d2b5098 --- /dev/null +++ b/components/cli/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/components/cli/secret_list_test.go b/components/cli/secret_list_test.go new file mode 100644 index 0000000000..174963c7ee --- /dev/null +++ b/components/cli/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/components/cli/secret_remove.go b/components/cli/secret_remove.go new file mode 100644 index 0000000000..1955b988a9 --- /dev/null +++ b/components/cli/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/components/cli/secret_remove_test.go b/components/cli/secret_remove_test.go new file mode 100644 index 0000000000..f269f787d2 --- /dev/null +++ b/components/cli/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 89698b2350c44a24692fab3f335a172a28baadfb Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 27 Oct 2016 00:41:32 -0700 Subject: [PATCH 307/978] 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 Upstream-commit: 4e8f1a7dd9926802a6bd9c292ea4bc3d2339a9eb Component: cli --- components/cli/command/service/opts.go | 15 +-------------- components/cli/command/service/parse.go | 12 +++++------- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index a4fd08881c..3ef24ee33c 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -191,19 +191,6 @@ func convertNetworks(networks []string) []swarm.NetworkAttachmentConfig { return nets } -func convertSecrets(secrets []string) []*swarm.SecretReference { - sec := []*swarm.SecretReference{} - for _, s := range secrets { - sec = append(sec, &swarm.SecretReference{ - SecretID: s, - Mode: swarm.SecretReferenceFile, - Target: "", - }) - } - - return sec -} - type endpointOptions struct { mode string ports opts.ListOpts @@ -417,7 +404,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { Options: opts.dnsOptions.GetAll(), }, StopGracePeriod: opts.stopGrace.Value(), - Secrets: convertSecrets(opts.secrets), + Secrets: nil, }, Networks: convertNetworks(opts.networks.GetAll()), Resources: opts.resources.ToResourceRequirements(), diff --git a/components/cli/command/service/parse.go b/components/cli/command/service/parse.go index 596d8e50d8..f3061660a2 100644 --- a/components/cli/command/service/parse.go +++ b/components/cli/command/service/parse.go @@ -18,7 +18,7 @@ func parseSecretString(secretString string) (string, string, error) { tokens := strings.Split(secretString, ":") secretName := strings.TrimSpace(tokens[0]) - targetName := "" + targetName := secretName if secretName == "" { return "", "", fmt.Errorf("invalid secret name provided") @@ -29,8 +29,6 @@ func parseSecretString(secretString string) (string, string, error) { if targetName == "" { return "", "", fmt.Errorf("invalid presentation name provided") } - } else { - targetName = secretName } // ensure target is a filename only; no paths allowed @@ -77,22 +75,22 @@ func parseSecrets(client client.APIClient, requestedSecrets []string) ([]*swarmt return nil, err } - foundSecrets := make(map[string]*swarmtypes.Secret) + foundSecrets := make(map[string]string) for _, secret := range secrets { - foundSecrets[secret.Spec.Annotations.Name] = &secret + foundSecrets[secret.Spec.Annotations.Name] = secret.ID } addedSecrets := []*swarmtypes.SecretReference{} for secretName, secretRef := range needSecrets { - s, ok := foundSecrets[secretName] + id, ok := foundSecrets[secretName] if !ok { return nil, fmt.Errorf("secret not found: %s", secretName) } // set the id for the ref to properly assign in swarm // since swarm needs the ID instead of the name - secretRef.SecretID = s.ID + secretRef.SecretID = id addedSecrets = append(addedSecrets, secretRef) } From 719ce57b9efa089c41667c37d310fb609e1363b1 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 27 Oct 2016 00:41:32 -0700 Subject: [PATCH 308/978] 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 Upstream-commit: 548728bb843ec0ca4f5f8a36edf7e92556eb2f77 Component: cli --- components/cli/errors.go | 2 +- components/cli/secret_create.go | 2 +- components/cli/secret_create_test.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/cli/errors.go b/components/cli/errors.go index db7294daa8..94c22a728a 100644 --- a/components/cli/errors.go +++ b/components/cli/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/components/cli/secret_create.go b/components/cli/secret_create.go index de8b041567..f92a3d1510 100644 --- a/components/cli/secret_create.go +++ b/components/cli/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/components/cli/secret_create_test.go b/components/cli/secret_create_test.go index d264eb6692..b7def89d0e 100644 --- a/components/cli/secret_create_test.go +++ b/components/cli/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 41cabcef6d54e58c93085574792c1dca5e36da32 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 27 Oct 2016 15:51:02 -0700 Subject: [PATCH 309/978] add secret support for service update - add nosuid and noexec to tmpfs Signed-off-by: Evan Hazlett Upstream-commit: 8554b64b99bdc58c693d2ffe79dc2bfeb910bbd8 Component: cli --- components/cli/command/service/opts.go | 2 ++ components/cli/command/service/update.go | 34 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index 3ef24ee33c..37da5d1145 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -557,4 +557,6 @@ const ( flagHealthTimeout = "health-timeout" flagNoHealthcheck = "no-healthcheck" flagSecret = "secret" + flagSecretAdd = "secret-add" + flagSecretRemove = "secret-rm" ) diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index 4a77229497..a9f5ac9be6 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -14,6 +14,7 @@ import ( "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/client" "github.com/docker/docker/opts" runconfigopts "github.com/docker/docker/runconfig/opts" "github.com/docker/go-connections/nat" @@ -54,6 +55,8 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Var(&opts.labels, flagLabelAdd, "Add or update a service label") flags.Var(&opts.containerLabels, flagContainerLabelAdd, "Add or update a container label") flags.Var(&opts.env, flagEnvAdd, "Add or update an environment variable") + flags.Var(newListOptsVar(), flagSecretRemove, "Remove a secret") + flags.StringSliceVar(&opts.secrets, flagSecretAdd, []string{}, "Add a secret") flags.Var(&opts.mounts, flagMountAdd, "Add or update a mount on a service") flags.Var(&opts.constraints, flagConstraintAdd, "Add or update a placement constraint") flags.Var(&opts.endpoint.ports, flagPublishAdd, "Add or update a published port") @@ -97,6 +100,13 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID str return err } + updatedSecrets, err := getUpdatedSecrets(apiClient, flags, spec.TaskTemplate.ContainerSpec.Secrets) + if err != nil { + return err + } + + spec.TaskTemplate.ContainerSpec.Secrets = updatedSecrets + // only send auth if flag was set sendAuth, err := flags.GetBool(flagRegistryAuth) if err != nil { @@ -401,6 +411,30 @@ func updateEnvironment(flags *pflag.FlagSet, field *[]string) { *field = removeItems(*field, toRemove, envKey) } +func getUpdatedSecrets(apiClient client.APIClient, flags *pflag.FlagSet, secrets []*swarm.SecretReference) ([]*swarm.SecretReference, error) { + if flags.Changed(flagSecretAdd) { + values, err := flags.GetStringSlice(flagSecretAdd) + if err != nil { + return nil, err + } + + addSecrets, err := parseSecrets(apiClient, values) + if err != nil { + return nil, err + } + secrets = append(secrets, addSecrets...) + } + toRemove := buildToRemoveSet(flags, flagSecretRemove) + newSecrets := []*swarm.SecretReference{} + for _, secret := range secrets { + if _, exists := toRemove[secret.SecretName]; !exists { + newSecrets = append(newSecrets, secret) + } + } + + return newSecrets, nil +} + func envKey(value string) string { kv := strings.SplitN(value, "=", 2) return kv[0] From 9c66350efecc8c17ab60e154465c4629af06d894 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 27 Oct 2016 17:18:12 -0700 Subject: [PATCH 310/978] support the same secret with different targets on service create Signed-off-by: Evan Hazlett Upstream-commit: ab5f829742a58c6d9f3b168e6724f351f788d6f3 Component: cli --- components/cli/command/service/parse.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/components/cli/command/service/parse.go b/components/cli/command/service/parse.go index f3061660a2..1a8e56b8c4 100644 --- a/components/cli/command/service/parse.go +++ b/components/cli/command/service/parse.go @@ -44,9 +44,10 @@ func parseSecretString(secretString string) (string, string, error) { // them to secret references to use with the spec func parseSecrets(client client.APIClient, requestedSecrets []string) ([]*swarmtypes.SecretReference, error) { lookupSecretNames := []string{} - needSecrets := make(map[string]*swarmtypes.SecretReference) + neededSecrets := make(map[string]*swarmtypes.SecretReference) ctx := context.Background() + neededLookup := map[string]string{} for _, secret := range requestedSecrets { n, t, err := parseSecretString(secret) if err != nil { @@ -60,7 +61,8 @@ func parseSecrets(client client.APIClient, requestedSecrets []string) ([]*swarmt } lookupSecretNames = append(lookupSecretNames, n) - needSecrets[n] = secretRef + neededLookup[t] = n + neededSecrets[t] = secretRef } args := filters.NewArgs() @@ -82,12 +84,17 @@ func parseSecrets(client client.APIClient, requestedSecrets []string) ([]*swarmt addedSecrets := []*swarmtypes.SecretReference{} - for secretName, secretRef := range needSecrets { + for target, secretName := range neededLookup { id, ok := foundSecrets[secretName] if !ok { return nil, fmt.Errorf("secret not found: %s", secretName) } + secretRef, ok := neededSecrets[target] + if !ok { + return nil, fmt.Errorf("secret reference not found: %s", secretName) + } + // set the id for the ref to properly assign in swarm // since swarm needs the ID instead of the name secretRef.SecretID = id From 2983ce883c0d372bd8bb978e8a69d61d014ca7fb Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 27 Oct 2016 17:57:38 -0700 Subject: [PATCH 311/978] simplify secret lookup on service create Signed-off-by: Evan Hazlett Upstream-commit: 6bbc35a7434744c70646ba5bc1f686f4faf9476c Component: cli --- components/cli/command/service/parse.go | 30 ++++++++++--------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/components/cli/command/service/parse.go b/components/cli/command/service/parse.go index 1a8e56b8c4..71d6fb1958 100644 --- a/components/cli/command/service/parse.go +++ b/components/cli/command/service/parse.go @@ -43,11 +43,9 @@ func parseSecretString(secretString string) (string, string, error) { // parseSecrets retrieves the secrets from the requested names and converts // them to secret references to use with the spec func parseSecrets(client client.APIClient, requestedSecrets []string) ([]*swarmtypes.SecretReference, error) { - lookupSecretNames := []string{} - neededSecrets := make(map[string]*swarmtypes.SecretReference) + secretRefs := make(map[string]*swarmtypes.SecretReference) ctx := context.Background() - neededLookup := map[string]string{} for _, secret := range requestedSecrets { n, t, err := parseSecretString(secret) if err != nil { @@ -60,14 +58,15 @@ func parseSecrets(client client.APIClient, requestedSecrets []string) ([]*swarmt Target: t, } - lookupSecretNames = append(lookupSecretNames, n) - neededLookup[t] = n - neededSecrets[t] = secretRef + if _, exists := secretRefs[t]; exists { + return nil, fmt.Errorf("duplicate secret target for %s not allowed", n) + } + secretRefs[t] = secretRef } args := filters.NewArgs() - for _, s := range lookupSecretNames { - args.Add("names", s) + for _, s := range secretRefs { + args.Add("names", s.SecretName) } secrets, err := client.SecretList(ctx, types.SecretListOptions{ @@ -84,21 +83,16 @@ func parseSecrets(client client.APIClient, requestedSecrets []string) ([]*swarmt addedSecrets := []*swarmtypes.SecretReference{} - for target, secretName := range neededLookup { - id, ok := foundSecrets[secretName] + for _, ref := range secretRefs { + id, ok := foundSecrets[ref.SecretName] if !ok { - return nil, fmt.Errorf("secret not found: %s", secretName) - } - - secretRef, ok := neededSecrets[target] - if !ok { - return nil, fmt.Errorf("secret reference not found: %s", secretName) + return nil, fmt.Errorf("secret not found: %s", ref.SecretName) } // set the id for the ref to properly assign in swarm // since swarm needs the ID instead of the name - secretRef.SecretID = id - addedSecrets = append(addedSecrets, secretRef) + ref.SecretID = id + addedSecrets = append(addedSecrets, ref) } return addedSecrets, nil From 852ef986760199ec7b8593da77afa5db809b5d70 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Tue, 1 Nov 2016 18:11:43 -0400 Subject: [PATCH 312/978] update to support new target in swarmkit Signed-off-by: Evan Hazlett Upstream-commit: 2b0fa52c0905f231edf71f3e097c1d8395ba0f60 Component: cli --- components/cli/command/service/parse.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/components/cli/command/service/parse.go b/components/cli/command/service/parse.go index 71d6fb1958..5a22ed352c 100644 --- a/components/cli/command/service/parse.go +++ b/components/cli/command/service/parse.go @@ -54,8 +54,13 @@ func parseSecrets(client client.APIClient, requestedSecrets []string) ([]*swarmt secretRef := &swarmtypes.SecretReference{ SecretName: n, - Mode: swarmtypes.SecretReferenceFile, - Target: t, + // TODO (ehazlett): parse these from cli request + Target: swarmtypes.SecretReferenceFileTarget{ + Name: t, + UID: "0", + GID: "0", + Mode: 0444, + }, } if _, exists := secretRefs[t]; exists { From dfef330ccc3efd0203c88d3fbd1d8c71fa39473f Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Tue, 1 Nov 2016 22:28:32 -0400 Subject: [PATCH 313/978] secrets: use explicit format when using secrets Signed-off-by: Evan Hazlett Upstream-commit: 15b97a39d7668569a3bb5c018f593ad1bcf42b77 Component: cli --- components/cli/command/service/create.go | 3 +- components/cli/command/service/opts.go | 97 ++++++++++++++++++++- components/cli/command/service/opts_test.go | 45 ++++++++++ components/cli/command/service/parse.go | 54 ++---------- components/cli/command/service/update.go | 7 +- 5 files changed, 154 insertions(+), 52 deletions(-) diff --git a/components/cli/command/service/create.go b/components/cli/command/service/create.go index 8fb9070e67..e5b728d3e8 100644 --- a/components/cli/command/service/create.go +++ b/components/cli/command/service/create.go @@ -39,6 +39,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Var(&opts.mounts, flagMount, "Attach a filesystem mount to the service") flags.Var(&opts.constraints, flagConstraint, "Placement constraints") flags.Var(&opts.networks, flagNetwork, "Network attachments") + flags.Var(&opts.secrets, flagSecret, "Specify secrets to expose to the service") flags.VarP(&opts.endpoint.ports, flagPublish, "p", "Publish a port as a node port") flags.Var(&opts.groups, flagGroup, "Set one or more supplementary user groups for the container") flags.Var(&opts.dns, flagDNS, "Set custom DNS servers") @@ -59,7 +60,7 @@ func runCreate(dockerCli *command.DockerCli, opts *serviceOptions) error { } // parse and validate secrets - secrets, err := parseSecrets(apiClient, opts.secrets) + secrets, err := parseSecrets(apiClient, opts.secrets.Value()) if err != nil { return err } diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index 37da5d1145..00cdecb67d 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -1,7 +1,10 @@ package service import ( + "encoding/csv" "fmt" + "os" + "path/filepath" "strconv" "strings" "time" @@ -139,6 +142,98 @@ func (f *floatValue) Value() float32 { return float32(*f) } +// SecretRequestSpec is a type for requesting secrets +type SecretRequestSpec struct { + source string + target string + uid string + gid string + mode os.FileMode +} + +// SecretOpt is a Value type for parsing secrets +type SecretOpt struct { + values []*SecretRequestSpec +} + +// Set a new secret value +func (o *SecretOpt) Set(value string) error { + csvReader := csv.NewReader(strings.NewReader(value)) + fields, err := csvReader.Read() + if err != nil { + return err + } + + spec := &SecretRequestSpec{ + source: "", + target: "", + uid: "0", + gid: "0", + mode: 0444, + } + + for _, field := range fields { + parts := strings.SplitN(field, "=", 2) + key := strings.ToLower(parts[0]) + + if len(parts) != 2 { + return fmt.Errorf("invalid field '%s' must be a key=value pair", field) + } + + value := parts[1] + switch key { + case "source": + spec.source = value + case "target": + tDir, _ := filepath.Split(value) + if tDir != "" { + return fmt.Errorf("target must not have a path") + } + spec.target = value + case "uid": + spec.uid = value + case "gid": + spec.gid = value + case "mode": + m, err := strconv.ParseUint(value, 0, 32) + if err != nil { + return fmt.Errorf("invalid mode specified: %v", err) + } + + spec.mode = os.FileMode(m) + default: + return fmt.Errorf("invalid field in secret request: %s", key) + } + } + + if spec.source == "" { + return fmt.Errorf("source is required") + } + + o.values = append(o.values, spec) + return nil +} + +// Type returns the type of this option +func (o *SecretOpt) Type() string { + return "secret" +} + +// String returns a string repr of this option +func (o *SecretOpt) String() string { + secrets := []string{} + for _, secret := range o.values { + repr := fmt.Sprintf("%s -> %s", secret.source, secret.target) + secrets = append(secrets, repr) + } + return strings.Join(secrets, ", ") +} + +// Value returns the secret requests +func (o *SecretOpt) Value() []*SecretRequestSpec { + return o.values +} + type updateOptions struct { parallelism uint64 delay time.Duration @@ -337,7 +432,7 @@ type serviceOptions struct { logDriver logDriverOptions healthcheck healthCheckOptions - secrets []string + secrets SecretOpt } func newServiceOptions() *serviceOptions { diff --git a/components/cli/command/service/opts_test.go b/components/cli/command/service/opts_test.go index aa2d999dcf..551dfc239c 100644 --- a/components/cli/command/service/opts_test.go +++ b/components/cli/command/service/opts_test.go @@ -1,6 +1,7 @@ package service import ( + "os" "reflect" "testing" "time" @@ -105,3 +106,47 @@ func TestHealthCheckOptionsToHealthConfigConflict(t *testing.T) { _, err := opt.toHealthConfig() assert.Error(t, err, "--no-healthcheck conflicts with --health-* options") } + +func TestSecretOptionsSimple(t *testing.T) { + var opt SecretOpt + + testCase := "source=/foo,target=testing" + assert.NilError(t, opt.Set(testCase)) + + reqs := opt.Value() + assert.Equal(t, len(reqs), 1) + req := reqs[0] + assert.Equal(t, req.source, "/foo") + assert.Equal(t, req.target, "testing") +} + +func TestSecretOptionsCustomUidGid(t *testing.T) { + var opt SecretOpt + + testCase := "source=/foo,target=testing,uid=1000,gid=1001" + assert.NilError(t, opt.Set(testCase)) + + reqs := opt.Value() + assert.Equal(t, len(reqs), 1) + req := reqs[0] + assert.Equal(t, req.source, "/foo") + assert.Equal(t, req.target, "testing") + assert.Equal(t, req.uid, "1000") + assert.Equal(t, req.gid, "1001") +} + +func TestSecretOptionsCustomMode(t *testing.T) { + var opt SecretOpt + + testCase := "source=/foo,target=testing,uid=1000,gid=1001,mode=0444" + assert.NilError(t, opt.Set(testCase)) + + reqs := opt.Value() + assert.Equal(t, len(reqs), 1) + req := reqs[0] + assert.Equal(t, req.source, "/foo") + assert.Equal(t, req.target, "testing") + assert.Equal(t, req.uid, "1000") + assert.Equal(t, req.gid, "1001") + assert.Equal(t, req.mode, os.FileMode(0444)) +} diff --git a/components/cli/command/service/parse.go b/components/cli/command/service/parse.go index 5a22ed352c..73fa8a0cb9 100644 --- a/components/cli/command/service/parse.go +++ b/components/cli/command/service/parse.go @@ -3,8 +3,6 @@ package service import ( "context" "fmt" - "path/filepath" - "strings" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" @@ -12,61 +10,27 @@ import ( "github.com/docker/docker/client" ) -// parseSecretString parses the requested secret and returns the secret name -// and target. Expects format SECRET_NAME:TARGET -func parseSecretString(secretString string) (string, string, error) { - tokens := strings.Split(secretString, ":") - - secretName := strings.TrimSpace(tokens[0]) - targetName := secretName - - if secretName == "" { - return "", "", fmt.Errorf("invalid secret name provided") - } - - if len(tokens) > 1 { - targetName = strings.TrimSpace(tokens[1]) - if targetName == "" { - return "", "", fmt.Errorf("invalid presentation name provided") - } - } - - // ensure target is a filename only; no paths allowed - tDir, _ := filepath.Split(targetName) - if tDir != "" { - return "", "", fmt.Errorf("target must not have a path") - } - - return secretName, targetName, nil -} - // parseSecrets retrieves the secrets from the requested names and converts // them to secret references to use with the spec -func parseSecrets(client client.APIClient, requestedSecrets []string) ([]*swarmtypes.SecretReference, error) { +func parseSecrets(client client.APIClient, requestedSecrets []*SecretRequestSpec) ([]*swarmtypes.SecretReference, error) { secretRefs := make(map[string]*swarmtypes.SecretReference) ctx := context.Background() for _, secret := range requestedSecrets { - n, t, err := parseSecretString(secret) - if err != nil { - return nil, err - } - secretRef := &swarmtypes.SecretReference{ - SecretName: n, - // TODO (ehazlett): parse these from cli request + SecretName: secret.source, Target: swarmtypes.SecretReferenceFileTarget{ - Name: t, - UID: "0", - GID: "0", - Mode: 0444, + Name: secret.target, + UID: secret.uid, + GID: secret.gid, + Mode: secret.mode, }, } - if _, exists := secretRefs[t]; exists { - return nil, fmt.Errorf("duplicate secret target for %s not allowed", n) + if _, exists := secretRefs[secret.target]; exists { + return nil, fmt.Errorf("duplicate secret target for %s not allowed", secret.source) } - secretRefs[t] = secretRef + secretRefs[secret.target] = secretRef } args := filters.NewArgs() diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index a9f5ac9be6..37f709e230 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -56,7 +56,7 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Var(&opts.containerLabels, flagContainerLabelAdd, "Add or update a container label") flags.Var(&opts.env, flagEnvAdd, "Add or update an environment variable") flags.Var(newListOptsVar(), flagSecretRemove, "Remove a secret") - flags.StringSliceVar(&opts.secrets, flagSecretAdd, []string{}, "Add a secret") + flags.Var(&opts.secrets, flagSecretAdd, "Add or update a secret on a service") flags.Var(&opts.mounts, flagMountAdd, "Add or update a mount on a service") flags.Var(&opts.constraints, flagConstraintAdd, "Add or update a placement constraint") flags.Var(&opts.endpoint.ports, flagPublishAdd, "Add or update a published port") @@ -413,10 +413,7 @@ func updateEnvironment(flags *pflag.FlagSet, field *[]string) { func getUpdatedSecrets(apiClient client.APIClient, flags *pflag.FlagSet, secrets []*swarm.SecretReference) ([]*swarm.SecretReference, error) { if flags.Changed(flagSecretAdd) { - values, err := flags.GetStringSlice(flagSecretAdd) - if err != nil { - return nil, err - } + values := flags.Lookup(flagSecretAdd).Value.(*SecretOpt).Value() addSecrets, err := parseSecrets(apiClient, values) if err != nil { From 4cba6b316c155ab0ae58da086d5dbb6010f7f160 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Tue, 1 Nov 2016 23:32:21 -0400 Subject: [PATCH 314/978] secrets: enable secret inspect and rm by secret name Signed-off-by: Evan Hazlett Upstream-commit: d22e1a91f628d5acea0e3db54d23ed99832fffbf Component: cli --- components/cli/command/secret/inspect.go | 20 ++++++++++++++++--- components/cli/command/secret/remove.go | 25 +++++++++++++++++++++++- components/cli/command/secret/utils.go | 21 ++++++++++++++++++++ 3 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 components/cli/command/secret/utils.go diff --git a/components/cli/command/secret/inspect.go b/components/cli/command/secret/inspect.go index c8d5cd8f79..c5b0aa6a3d 100644 --- a/components/cli/command/secret/inspect.go +++ b/components/cli/command/secret/inspect.go @@ -34,9 +34,23 @@ func runSecretInspect(dockerCli *command.DockerCli, opts inspectOptions) error { client := dockerCli.Client() ctx := context.Background() - getRef := func(name string) (interface{}, []byte, error) { - return client.SecretInspectWithRaw(ctx, name) + // attempt to lookup secret by name + secrets, err := getSecrets(client, ctx, []string{opts.name}) + if err != nil { + return err } - return inspect.Inspect(dockerCli.Out(), []string{opts.name}, opts.format, getRef) + id := opts.name + for _, s := range secrets { + if s.Spec.Annotations.Name == opts.name { + id = s.ID + break + } + } + + getRef := func(name string) (interface{}, []byte, error) { + return client.SecretInspectWithRaw(ctx, id) + } + + return inspect.Inspect(dockerCli.Out(), []string{id}, opts.format, getRef) } diff --git a/components/cli/command/secret/remove.go b/components/cli/command/secret/remove.go index f336c6161a..9396b9b179 100644 --- a/components/cli/command/secret/remove.go +++ b/components/cli/command/secret/remove.go @@ -31,7 +31,30 @@ func runSecretRemove(dockerCli *command.DockerCli, opts removeOptions) error { client := dockerCli.Client() ctx := context.Background() - for _, id := range opts.ids { + // attempt to lookup secret by name + secrets, err := getSecrets(client, ctx, opts.ids) + if err != nil { + return err + } + + ids := opts.ids + + names := make(map[string]int) + for _, id := range ids { + names[id] = 1 + } + + if len(secrets) > 0 { + ids = []string{} + + for _, s := range secrets { + if _, ok := names[s.Spec.Annotations.Name]; ok { + ids = append(ids, s.ID) + } + } + } + + for _, id := range ids { if err := client.SecretRemove(ctx, id); err != nil { return err } diff --git a/components/cli/command/secret/utils.go b/components/cli/command/secret/utils.go new file mode 100644 index 0000000000..40aa4a6d77 --- /dev/null +++ b/components/cli/command/secret/utils.go @@ -0,0 +1,21 @@ +package secret + +import ( + "context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/client" +) + +func getSecrets(client client.APIClient, ctx context.Context, names []string) ([]swarm.Secret, error) { + args := filters.NewArgs() + for _, n := range names { + args.Add("names", n) + } + + return client.SecretList(ctx, types.SecretListOptions{ + Filter: args, + }) +} From 7fac7dc9cb24691164b21e215ee5db35d0f8a634 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 3 Nov 2016 11:08:22 -0400 Subject: [PATCH 315/978] move secretopt to opts pkg Signed-off-by: Evan Hazlett Upstream-commit: 91c08eab93ac0158842313a1b3a5ec9ca58d493c Component: cli --- components/cli/command/service/opts.go | 2 +- components/cli/command/service/parse.go | 18 +++++++++--------- components/cli/command/service/update.go | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index 00cdecb67d..45adb37672 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -432,7 +432,7 @@ type serviceOptions struct { logDriver logDriverOptions healthcheck healthCheckOptions - secrets SecretOpt + secrets opts.SecretOpt } func newServiceOptions() *serviceOptions { diff --git a/components/cli/command/service/parse.go b/components/cli/command/service/parse.go index 73fa8a0cb9..cbf2745dce 100644 --- a/components/cli/command/service/parse.go +++ b/components/cli/command/service/parse.go @@ -12,25 +12,25 @@ import ( // parseSecrets retrieves the secrets from the requested names and converts // them to secret references to use with the spec -func parseSecrets(client client.APIClient, requestedSecrets []*SecretRequestSpec) ([]*swarmtypes.SecretReference, error) { +func parseSecrets(client client.APIClient, requestedSecrets []*types.SecretRequestOptions) ([]*swarmtypes.SecretReference, error) { secretRefs := make(map[string]*swarmtypes.SecretReference) ctx := context.Background() for _, secret := range requestedSecrets { secretRef := &swarmtypes.SecretReference{ - SecretName: secret.source, + SecretName: secret.Source, Target: swarmtypes.SecretReferenceFileTarget{ - Name: secret.target, - UID: secret.uid, - GID: secret.gid, - Mode: secret.mode, + Name: secret.Target, + UID: secret.UID, + GID: secret.GID, + Mode: secret.Mode, }, } - if _, exists := secretRefs[secret.target]; exists { - return nil, fmt.Errorf("duplicate secret target for %s not allowed", secret.source) + if _, exists := secretRefs[secret.Target]; exists { + return nil, fmt.Errorf("duplicate secret target for %s not allowed", secret.Source) } - secretRefs[secret.target] = secretRef + secretRefs[secret.Target] = secretRef } args := filters.NewArgs() diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index 37f709e230..1bc72a8f19 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -413,7 +413,7 @@ func updateEnvironment(flags *pflag.FlagSet, field *[]string) { func getUpdatedSecrets(apiClient client.APIClient, flags *pflag.FlagSet, secrets []*swarm.SecretReference) ([]*swarm.SecretReference, error) { if flags.Changed(flagSecretAdd) { - values := flags.Lookup(flagSecretAdd).Value.(*SecretOpt).Value() + values := flags.Lookup(flagSecretAdd).Value.(*opts.SecretOpt).Value() addSecrets, err := parseSecrets(apiClient, values) if err != nil { From b5d5eec7812ff6758f5191224aa4d901a44acedf Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 3 Nov 2016 14:09:13 -0400 Subject: [PATCH 316/978] 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 Upstream-commit: 90743339570aea9001339aea628001387d8c1afe Component: cli --- components/cli/command/secret/inspect.go | 2 +- components/cli/command/secret/remove.go | 2 +- components/cli/command/secret/utils.go | 4 +-- components/cli/command/service/opts_test.go | 34 ++++++++++----------- components/cli/command/service/parse.go | 4 +-- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/components/cli/command/secret/inspect.go b/components/cli/command/secret/inspect.go index c5b0aa6a3d..25da79f16d 100644 --- a/components/cli/command/secret/inspect.go +++ b/components/cli/command/secret/inspect.go @@ -35,7 +35,7 @@ func runSecretInspect(dockerCli *command.DockerCli, opts inspectOptions) error { ctx := context.Background() // attempt to lookup secret by name - secrets, err := getSecrets(client, ctx, []string{opts.name}) + secrets, err := getSecretsByName(client, ctx, []string{opts.name}) if err != nil { return err } diff --git a/components/cli/command/secret/remove.go b/components/cli/command/secret/remove.go index 9396b9b179..d277eceba2 100644 --- a/components/cli/command/secret/remove.go +++ b/components/cli/command/secret/remove.go @@ -32,7 +32,7 @@ func runSecretRemove(dockerCli *command.DockerCli, opts removeOptions) error { ctx := context.Background() // attempt to lookup secret by name - secrets, err := getSecrets(client, ctx, opts.ids) + secrets, err := getSecretsByName(client, ctx, opts.ids) if err != nil { return err } diff --git a/components/cli/command/secret/utils.go b/components/cli/command/secret/utils.go index 40aa4a6d77..d1a7d97c44 100644 --- a/components/cli/command/secret/utils.go +++ b/components/cli/command/secret/utils.go @@ -9,13 +9,13 @@ import ( "github.com/docker/docker/client" ) -func getSecrets(client client.APIClient, ctx context.Context, names []string) ([]swarm.Secret, error) { +func getSecretsByName(client client.APIClient, ctx context.Context, names []string) ([]swarm.Secret, error) { args := filters.NewArgs() for _, n := range names { args.Add("names", n) } return client.SecretList(ctx, types.SecretListOptions{ - Filter: args, + Filters: args, }) } diff --git a/components/cli/command/service/opts_test.go b/components/cli/command/service/opts_test.go index 551dfc239c..3df3a4fd5d 100644 --- a/components/cli/command/service/opts_test.go +++ b/components/cli/command/service/opts_test.go @@ -108,45 +108,45 @@ func TestHealthCheckOptionsToHealthConfigConflict(t *testing.T) { } func TestSecretOptionsSimple(t *testing.T) { - var opt SecretOpt + var opt opts.SecretOpt - testCase := "source=/foo,target=testing" + testCase := "source=foo,target=testing" assert.NilError(t, opt.Set(testCase)) reqs := opt.Value() assert.Equal(t, len(reqs), 1) req := reqs[0] - assert.Equal(t, req.source, "/foo") - assert.Equal(t, req.target, "testing") + assert.Equal(t, req.Source, "foo") + assert.Equal(t, req.Target, "testing") } func TestSecretOptionsCustomUidGid(t *testing.T) { - var opt SecretOpt + var opt opts.SecretOpt - testCase := "source=/foo,target=testing,uid=1000,gid=1001" + testCase := "source=foo,target=testing,uid=1000,gid=1001" assert.NilError(t, opt.Set(testCase)) reqs := opt.Value() assert.Equal(t, len(reqs), 1) req := reqs[0] - assert.Equal(t, req.source, "/foo") - assert.Equal(t, req.target, "testing") - assert.Equal(t, req.uid, "1000") - assert.Equal(t, req.gid, "1001") + assert.Equal(t, req.Source, "foo") + assert.Equal(t, req.Target, "testing") + assert.Equal(t, req.UID, "1000") + assert.Equal(t, req.GID, "1001") } func TestSecretOptionsCustomMode(t *testing.T) { - var opt SecretOpt + var opt opts.SecretOpt - testCase := "source=/foo,target=testing,uid=1000,gid=1001,mode=0444" + testCase := "source=foo,target=testing,uid=1000,gid=1001,mode=0444" assert.NilError(t, opt.Set(testCase)) reqs := opt.Value() assert.Equal(t, len(reqs), 1) req := reqs[0] - assert.Equal(t, req.source, "/foo") - assert.Equal(t, req.target, "testing") - assert.Equal(t, req.uid, "1000") - assert.Equal(t, req.gid, "1001") - assert.Equal(t, req.mode, os.FileMode(0444)) + assert.Equal(t, req.Source, "foo") + assert.Equal(t, req.Target, "testing") + assert.Equal(t, req.UID, "1000") + assert.Equal(t, req.GID, "1001") + assert.Equal(t, req.Mode, os.FileMode(0444)) } diff --git a/components/cli/command/service/parse.go b/components/cli/command/service/parse.go index cbf2745dce..4728c773c4 100644 --- a/components/cli/command/service/parse.go +++ b/components/cli/command/service/parse.go @@ -1,13 +1,13 @@ package service import ( - "context" "fmt" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" swarmtypes "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/client" + "golang.org/x/net/context" ) // parseSecrets retrieves the secrets from the requested names and converts @@ -39,7 +39,7 @@ func parseSecrets(client client.APIClient, requestedSecrets []*types.SecretReque } secrets, err := client.SecretList(ctx, types.SecretListOptions{ - Filter: args, + Filters: args, }) if err != nil { return nil, err From 8fd729c7c97373b6ae7926a867ac6e0d3a910cbc Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 3 Nov 2016 15:56:05 -0400 Subject: [PATCH 317/978] secrets: support simple syntax --secret foo Signed-off-by: Evan Hazlett Upstream-commit: b3bbcc1ba62af66ec33932f4d94fead0f04a5dd4 Component: cli --- components/cli/command/service/opts_test.go | 46 --------------------- 1 file changed, 46 deletions(-) diff --git a/components/cli/command/service/opts_test.go b/components/cli/command/service/opts_test.go index 3df3a4fd5d..85de3ae88a 100644 --- a/components/cli/command/service/opts_test.go +++ b/components/cli/command/service/opts_test.go @@ -1,13 +1,11 @@ package service import ( - "os" "reflect" "testing" "time" "github.com/docker/docker/api/types/container" - "github.com/docker/docker/opts" "github.com/docker/docker/pkg/testutil/assert" ) @@ -106,47 +104,3 @@ func TestHealthCheckOptionsToHealthConfigConflict(t *testing.T) { _, err := opt.toHealthConfig() assert.Error(t, err, "--no-healthcheck conflicts with --health-* options") } - -func TestSecretOptionsSimple(t *testing.T) { - var opt opts.SecretOpt - - testCase := "source=foo,target=testing" - assert.NilError(t, opt.Set(testCase)) - - reqs := opt.Value() - assert.Equal(t, len(reqs), 1) - req := reqs[0] - assert.Equal(t, req.Source, "foo") - assert.Equal(t, req.Target, "testing") -} - -func TestSecretOptionsCustomUidGid(t *testing.T) { - var opt opts.SecretOpt - - testCase := "source=foo,target=testing,uid=1000,gid=1001" - assert.NilError(t, opt.Set(testCase)) - - reqs := opt.Value() - assert.Equal(t, len(reqs), 1) - req := reqs[0] - assert.Equal(t, req.Source, "foo") - assert.Equal(t, req.Target, "testing") - assert.Equal(t, req.UID, "1000") - assert.Equal(t, req.GID, "1001") -} - -func TestSecretOptionsCustomMode(t *testing.T) { - var opt opts.SecretOpt - - testCase := "source=foo,target=testing,uid=1000,gid=1001,mode=0444" - assert.NilError(t, opt.Set(testCase)) - - reqs := opt.Value() - assert.Equal(t, len(reqs), 1) - req := reqs[0] - assert.Equal(t, req.Source, "foo") - assert.Equal(t, req.Target, "testing") - assert.Equal(t, req.UID, "1000") - assert.Equal(t, req.GID, "1001") - assert.Equal(t, req.Mode, os.FileMode(0444)) -} From 46910777dc1903f6d39e8ab513d7d16895c95f95 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 3 Nov 2016 17:01:54 -0400 Subject: [PATCH 318/978] support labels for secrets upon creation; review updates Signed-off-by: Evan Hazlett Upstream-commit: 0bda23ec2b53d4226c6fb5aab4c793f7bd6dfea7 Component: cli --- components/cli/command/secret/create.go | 29 ++++++++++++++++--------- components/cli/command/service/parse.go | 2 +- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/components/cli/command/secret/create.go b/components/cli/command/secret/create.go index 1c0e933f57..9800048341 100644 --- a/components/cli/command/secret/create.go +++ b/components/cli/command/secret/create.go @@ -9,29 +9,37 @@ import ( "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/opts" + runconfigopts "github.com/docker/docker/runconfig/opts" "github.com/spf13/cobra" ) type createOptions struct { - name string + name string + labels opts.ListOpts } func newSecretCreateCommand(dockerCli *command.DockerCli) *cobra.Command { - return &cobra.Command{ + createOpts := createOptions{ + labels: opts.NewListOpts(runconfigopts.ValidateEnv), + } + + cmd := &cobra.Command{ Use: "create [name]", Short: "Create a secret using stdin as content", - Args: cli.ExactArgs(1), + Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - opts := createOptions{ - name: args[0], - } - - return runSecretCreate(dockerCli, opts) + createOpts.name = args[0] + return runSecretCreate(dockerCli, createOpts) }, } + flags := cmd.Flags() + flags.VarP(&createOpts.labels, "label", "l", "Secret labels") + + return cmd } -func runSecretCreate(dockerCli *command.DockerCli, opts createOptions) error { +func runSecretCreate(dockerCli *command.DockerCli, options createOptions) error { client := dockerCli.Client() ctx := context.Background() @@ -42,7 +50,8 @@ func runSecretCreate(dockerCli *command.DockerCli, opts createOptions) error { spec := swarm.SecretSpec{ Annotations: swarm.Annotations{ - Name: opts.name, + Name: options.name, + Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()), }, Data: secretData, } diff --git a/components/cli/command/service/parse.go b/components/cli/command/service/parse.go index 4728c773c4..0e3a229f4e 100644 --- a/components/cli/command/service/parse.go +++ b/components/cli/command/service/parse.go @@ -19,7 +19,7 @@ func parseSecrets(client client.APIClient, requestedSecrets []*types.SecretReque for _, secret := range requestedSecrets { secretRef := &swarmtypes.SecretReference{ SecretName: secret.Source, - Target: swarmtypes.SecretReferenceFileTarget{ + Target: &swarmtypes.SecretReferenceFileTarget{ Name: secret.Target, UID: secret.UID, GID: secret.GID, From 00bf1d223d85bbd37c7993021c85ab64b1a08f1e Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Fri, 4 Nov 2016 14:24:44 -0400 Subject: [PATCH 319/978] SecretRequestOptions -> SecretRequestOption Signed-off-by: Evan Hazlett Upstream-commit: 0dc91150065e4219a45edb3e713f50a644510182 Component: cli --- components/cli/command/service/parse.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/service/parse.go b/components/cli/command/service/parse.go index 0e3a229f4e..368bc6d449 100644 --- a/components/cli/command/service/parse.go +++ b/components/cli/command/service/parse.go @@ -12,7 +12,7 @@ import ( // parseSecrets retrieves the secrets from the requested names and converts // them to secret references to use with the spec -func parseSecrets(client client.APIClient, requestedSecrets []*types.SecretRequestOptions) ([]*swarmtypes.SecretReference, error) { +func parseSecrets(client client.APIClient, requestedSecrets []*types.SecretRequestOption) ([]*swarmtypes.SecretReference, error) { secretRefs := make(map[string]*swarmtypes.SecretReference) ctx := context.Background() From 832247227cfeb89a2f9c896c6bac340fe09a9600 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Tue, 8 Nov 2016 11:34:45 -0500 Subject: [PATCH 320/978] more review updates - return err instead of wrap for update secret - add omitempty for data in secret spec Signed-off-by: Evan Hazlett Upstream-commit: c7d7b50003d23f6828cd5b5c8a6985bff8f123f8 Component: cli --- components/cli/command/service/opts.go | 1 - components/cli/command/service/opts_test.go | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index 45adb37672..b81998ec09 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -585,7 +585,6 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { flags.BoolVar(&opts.healthcheck.noHealthcheck, flagNoHealthcheck, false, "Disable any container-specified HEALTHCHECK") flags.BoolVarP(&opts.tty, flagTTY, "t", false, "Allocate a pseudo-TTY") - flags.StringSliceVar(&opts.secrets, flagSecret, []string{}, "Specify secrets to expose to the service") } const ( diff --git a/components/cli/command/service/opts_test.go b/components/cli/command/service/opts_test.go index 85de3ae88a..aa2d999dcf 100644 --- a/components/cli/command/service/opts_test.go +++ b/components/cli/command/service/opts_test.go @@ -6,6 +6,7 @@ import ( "time" "github.com/docker/docker/api/types/container" + "github.com/docker/docker/opts" "github.com/docker/docker/pkg/testutil/assert" ) From 8788cdab0bd156ce1ae2c9adb243abeceb81b2a3 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 3 Nov 2016 14:09:13 -0400 Subject: [PATCH 321/978] 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 Upstream-commit: ab6c38e01414042eb3edc70c39453e7b3f45a563 Component: cli --- components/cli/secret_list.go | 4 ++-- components/cli/secret_list_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/cli/secret_list.go b/components/cli/secret_list.go index 5e9d2b5098..7e9d5ec167 100644 --- a/components/cli/secret_list.go +++ b/components/cli/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/components/cli/secret_list_test.go b/components/cli/secret_list_test.go index 174963c7ee..1ac11cddb3 100644 --- a/components/cli/secret_list_test.go +++ b/components/cli/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 b1a6be626a7e1d95ae2986fd1e138d6d1b0bd957 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Tue, 8 Nov 2016 22:40:46 -0500 Subject: [PATCH 322/978] use human readable units when listing secrets Signed-off-by: Evan Hazlett Upstream-commit: 06666a5a23dcc004eb19af7f4b6028f481f74ab5 Component: cli --- components/cli/command/secret/ls.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/components/cli/command/secret/ls.go b/components/cli/command/secret/ls.go index 1befdad9d0..67fc1daff6 100644 --- a/components/cli/command/secret/ls.go +++ b/components/cli/command/secret/ls.go @@ -4,10 +4,12 @@ import ( "context" "fmt" "text/tabwriter" + "time" "github.com/docker/docker/api/types" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/go-units" "github.com/spf13/cobra" ) @@ -52,7 +54,11 @@ func runSecretList(dockerCli *command.DockerCli, opts listOptions) error { fmt.Fprintf(w, "\n") for _, s := range secrets { - fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d\n", s.ID, s.Spec.Annotations.Name, s.Meta.CreatedAt, s.Meta.UpdatedAt, s.SecretSize) + created := units.HumanDuration(time.Now().UTC().Sub(s.Meta.CreatedAt)) + " ago" + updated := units.HumanDuration(time.Now().UTC().Sub(s.Meta.UpdatedAt)) + " ago" + size := units.HumanSizeWithPrecision(float64(s.SecretSize), 3) + + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", s.ID, s.Spec.Annotations.Name, created, updated, size) } } From 724a0461bcd676aea459454b5e9cba9cd26e826b Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Tue, 8 Nov 2016 18:29:10 -0800 Subject: [PATCH 323/978] Add `--dns-option` to `docker run` and hide `--dns-opt` This fix is a follow up to #27567 based on: https://github.com/docker/docker/pull/27567#issuecomment-259295055 In #27567, `--dns-options` has been added to `service create/update`, together with `--dns` and `--dns-search`. The `--dns-opt` was used in `docker run`. This fix add `--dns-option` (not `--dns-options`) to `docker run/create`, and hide `--dns-opt`. It is still possible to use `--dns-opt` with `docker run/create`, though it will not show up in help output. This fix change `--dns-options`to --dns-option` for `docker service create` and `docker service update`. This fix also updates the docs and bash/zsh completion scripts. Signed-off-by: Yong Tang Upstream-commit: b38ca0f4c3024fb78e86c7a36bc6e624f4c60e18 Component: cli --- components/cli/command/service/create.go | 2 +- components/cli/command/service/opts.go | 22 +++++++++---------- components/cli/command/service/update.go | 20 ++++++++--------- components/cli/command/service/update_test.go | 4 ++-- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/components/cli/command/service/create.go b/components/cli/command/service/create.go index d6c3ebdb9c..2b9a6df991 100644 --- a/components/cli/command/service/create.go +++ b/components/cli/command/service/create.go @@ -42,7 +42,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.VarP(&opts.endpoint.ports, flagPublish, "p", "Publish a port as a node port") flags.Var(&opts.groups, flagGroup, "Set one or more supplementary user groups for the container") flags.Var(&opts.dns, flagDNS, "Set custom DNS servers") - flags.Var(&opts.dnsOptions, flagDNSOptions, "Set DNS options") + flags.Var(&opts.dnsOption, flagDNSOption, "Set DNS options") flags.Var(&opts.dnsSearch, flagDNSSearch, "Set custom DNS search domains") flags.SetInterspersed(false) diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index 827c4e5cdc..6240fdc739 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -318,7 +318,7 @@ type serviceOptions struct { mounts opts.MountOpt dns opts.ListOpts dnsSearch opts.ListOpts - dnsOptions opts.ListOpts + dnsOption opts.ListOpts resources resourceOptions stopGrace DurationOpt @@ -349,12 +349,12 @@ func newServiceOptions() *serviceOptions { endpoint: endpointOptions{ ports: opts.NewListOpts(ValidatePort), }, - groups: opts.NewListOpts(nil), - logDriver: newLogDriverOptions(), - dns: opts.NewListOpts(opts.ValidateIPAddress), - dnsOptions: opts.NewListOpts(nil), - dnsSearch: opts.NewListOpts(opts.ValidateDNSSearch), - networks: opts.NewListOpts(nil), + groups: opts.NewListOpts(nil), + logDriver: newLogDriverOptions(), + dns: opts.NewListOpts(opts.ValidateIPAddress), + dnsOption: opts.NewListOpts(nil), + dnsSearch: opts.NewListOpts(opts.ValidateDNSSearch), + networks: opts.NewListOpts(nil), } } @@ -400,7 +400,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { DNSConfig: &swarm.DNSConfig{ Nameservers: opts.dns.GetAll(), Search: opts.dnsSearch.GetAll(), - Options: opts.dnsOptions.GetAll(), + Options: opts.dnsOption.GetAll(), }, StopGracePeriod: opts.stopGrace.Value(), }, @@ -500,9 +500,9 @@ const ( flagDNS = "dns" flagDNSRemove = "dns-rm" flagDNSAdd = "dns-add" - flagDNSOptions = "dns-options" - flagDNSOptionsRemove = "dns-options-rm" - flagDNSOptionsAdd = "dns-options-add" + flagDNSOption = "dns-option" + flagDNSOptionRemove = "dns-option-rm" + flagDNSOptionAdd = "dns-option-add" flagDNSSearch = "dns-search" flagDNSSearchRemove = "dns-search-rm" flagDNSSearchAdd = "dns-search-add" diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index 4a77229497..d4a672261b 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -48,9 +48,9 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Var(newListOptsVar(), flagMountRemove, "Remove a mount by its target path") flags.Var(newListOptsVar(), flagPublishRemove, "Remove a published port by its target port") flags.Var(newListOptsVar(), flagConstraintRemove, "Remove a constraint") - flags.Var(newListOptsVar(), flagDNSRemove, "Remove custom DNS servers") - flags.Var(newListOptsVar(), flagDNSOptionsRemove, "Remove DNS options") - flags.Var(newListOptsVar(), flagDNSSearchRemove, "Remove DNS search domains") + flags.Var(newListOptsVar(), flagDNSRemove, "Remove a custom DNS server") + flags.Var(newListOptsVar(), flagDNSOptionRemove, "Remove a DNS option") + flags.Var(newListOptsVar(), flagDNSSearchRemove, "Remove a DNS search domain") flags.Var(&opts.labels, flagLabelAdd, "Add or update a service label") flags.Var(&opts.containerLabels, flagContainerLabelAdd, "Add or update a container label") flags.Var(&opts.env, flagEnvAdd, "Add or update an environment variable") @@ -58,9 +58,9 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Var(&opts.constraints, flagConstraintAdd, "Add or update a placement constraint") flags.Var(&opts.endpoint.ports, flagPublishAdd, "Add or update a published port") flags.Var(&opts.groups, flagGroupAdd, "Add an additional supplementary user group to the container") - flags.Var(&opts.dns, flagDNSAdd, "Add or update custom DNS servers") - flags.Var(&opts.dnsOptions, flagDNSOptionsAdd, "Add or update DNS options") - flags.Var(&opts.dnsSearch, flagDNSSearchAdd, "Add or update custom DNS search domains") + flags.Var(&opts.dns, flagDNSAdd, "Add or update a custom DNS server") + flags.Var(&opts.dnsOption, flagDNSOptionAdd, "Add or update a DNS option") + flags.Var(&opts.dnsSearch, flagDNSSearchAdd, "Add or update a custom DNS search domain") return cmd } @@ -264,7 +264,7 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error { } } - if anyChanged(flags, flagDNSAdd, flagDNSRemove, flagDNSOptionsAdd, flagDNSOptionsRemove, flagDNSSearchAdd, flagDNSSearchRemove) { + if anyChanged(flags, flagDNSAdd, flagDNSRemove, flagDNSOptionAdd, flagDNSOptionRemove, flagDNSSearchAdd, flagDNSSearchRemove) { if cspec.DNSConfig == nil { cspec.DNSConfig = &swarm.DNSConfig{} } @@ -555,12 +555,12 @@ func updateDNSConfig(flags *pflag.FlagSet, config **swarm.DNSConfig) error { sort.Strings(newConfig.Search) options := (*config).Options - if flags.Changed(flagDNSOptionsAdd) { - values := flags.Lookup(flagDNSOptionsAdd).Value.(*opts.ListOpts).GetAll() + if flags.Changed(flagDNSOptionAdd) { + values := flags.Lookup(flagDNSOptionAdd).Value.(*opts.ListOpts).GetAll() options = append(options, values...) } options = removeDuplicates(options) - toRemove = buildToRemoveSet(flags, flagDNSOptionsRemove) + toRemove = buildToRemoveSet(flags, flagDNSOptionRemove) for _, option := range options { if _, exists := toRemove[option]; !exists { newConfig.Options = append(newConfig.Options, option) diff --git a/components/cli/command/service/update_test.go b/components/cli/command/service/update_test.go index 91829b8615..b99064352a 100644 --- a/components/cli/command/service/update_test.go +++ b/components/cli/command/service/update_test.go @@ -142,8 +142,8 @@ func TestUpdateDNSConfig(t *testing.T) { // Invalid dns search domain assert.Error(t, flags.Set("dns-search-add", "example$com"), "example$com is not a valid domain") - flags.Set("dns-options-add", "ndots:9") - flags.Set("dns-options-rm", "timeout:3") + flags.Set("dns-option-add", "ndots:9") + flags.Set("dns-option-rm", "timeout:3") config := &swarm.DNSConfig{ Nameservers: []string{"3.3.3.3", "5.5.5.5"}, From 4931dad259ce376df0093e2d7d6c3d1855b0f93b Mon Sep 17 00:00:00 2001 From: John Howard Date: Wed, 9 Nov 2016 14:46:53 -0800 Subject: [PATCH 324/978] Tidy GetDockerOS() function Signed-off-by: John Howard Upstream-commit: c941751fb2b629f7e48699b3983a74d0745aa491 Component: cli --- components/cli/container_stats.go | 2 +- components/cli/image_build.go | 15 +-------------- components/cli/image_build_test.go | 2 +- components/cli/utils.go | 15 +++++++++++++++ 4 files changed, 18 insertions(+), 16 deletions(-) create mode 100644 components/cli/utils.go diff --git a/components/cli/container_stats.go b/components/cli/container_stats.go index 3be7a988f4..4758c66e32 100644 --- a/components/cli/container_stats.go +++ b/components/cli/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/components/cli/image_build.go b/components/cli/image_build.go index 0049e4e290..6fde75dcfd 100644 --- a/components/cli/image_build.go +++ b/components/cli/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/components/cli/image_build_test.go b/components/cli/image_build_test.go index 53dd93376a..ec0cbe2ee4 100644 --- a/components/cli/image_build_test.go +++ b/components/cli/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/components/cli/utils.go b/components/cli/utils.go new file mode 100644 index 0000000000..03bf4c82fa --- /dev/null +++ b/components/cli/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 878834ace080876f840049f94c4f820db104647f Mon Sep 17 00:00:00 2001 From: Anusha Ragunathan Date: Tue, 4 Oct 2016 12:01:19 -0700 Subject: [PATCH 325/978] Add plugin create functionality. Signed-off-by: Anusha Ragunathan Upstream-commit: b825c58ff87acb942b83fdae4c9afa573baf73a4 Component: cli --- components/cli/command/plugin/cmd.go | 1 + components/cli/command/plugin/create.go | 125 ++++++++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 components/cli/command/plugin/create.go diff --git a/components/cli/command/plugin/cmd.go b/components/cli/command/plugin/cmd.go index c78f43a8d4..2bb165db19 100644 --- a/components/cli/command/plugin/cmd.go +++ b/components/cli/command/plugin/cmd.go @@ -29,6 +29,7 @@ func NewPluginCommand(dockerCli *command.DockerCli) *cobra.Command { newRemoveCommand(dockerCli), newSetCommand(dockerCli), newPushCommand(dockerCli), + newCreateCommand(dockerCli), ) return cmd } diff --git a/components/cli/command/plugin/create.go b/components/cli/command/plugin/create.go new file mode 100644 index 0000000000..3b18ed3750 --- /dev/null +++ b/components/cli/command/plugin/create.go @@ -0,0 +1,125 @@ +package plugin + +import ( + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/reference" + "github.com/spf13/cobra" + "golang.org/x/net/context" +) + +// validateTag checks if the given repoName can be resolved. +func validateTag(rawRepo string) error { + _, err := reference.ParseNamed(rawRepo) + + return err +} + +// validateManifest ensures that a valid manifest.json is available in the given path +func validateManifest(path string) error { + dt, err := os.Open(filepath.Join(path, "manifest.json")) + if err != nil { + return err + } + + m := types.PluginManifest{} + err = json.NewDecoder(dt).Decode(&m) + dt.Close() + + return err +} + +// validateContextDir validates the given dir and returns abs path on success. +func validateContextDir(contextDir string) (string, error) { + absContextDir, err := filepath.Abs(contextDir) + + stat, err := os.Lstat(absContextDir) + if err != nil { + return "", err + } + + if !stat.IsDir() { + return "", fmt.Errorf("context must be a directory") + } + + return absContextDir, nil +} + +type pluginCreateOptions struct { + repoName string + context string + compress bool +} + +func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { + options := pluginCreateOptions{} + + cmd := &cobra.Command{ + Use: "create [OPTIONS] reponame[:tag] PATH-TO-ROOTFS (rootfs + manifest.json)", + Short: "Create a plugin from a rootfs and manifest", + Args: cli.RequiresMinArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + options.repoName = args[0] + options.context = args[1] + return runCreate(dockerCli, options) + }, + } + + flags := cmd.Flags() + + flags.BoolVar(&options.compress, "compress", false, "Compress the context using gzip") + + return cmd +} + +func runCreate(dockerCli *command.DockerCli, options pluginCreateOptions) error { + var ( + createCtx io.ReadCloser + err error + ) + + if err := validateTag(options.repoName); err != nil { + return err + } + + absContextDir, err := validateContextDir(options.context) + if err != nil { + return err + } + + if err := validateManifest(options.context); err != nil { + return err + } + + compression := archive.Uncompressed + if options.compress { + logrus.Debugf("compression enabled") + compression = archive.Gzip + } + + createCtx, err = archive.TarWithOptions(absContextDir, &archive.TarOptions{ + Compression: compression, + }) + + if err != nil { + return err + } + + ctx := context.Background() + + createOptions := types.PluginCreateOptions{RepoName: options.repoName} + if err = dockerCli.Client().PluginCreate(ctx, createCtx, createOptions); err != nil { + return err + } + fmt.Fprintln(dockerCli.Out(), options.repoName) + return nil +} From 7c006ff288e1e7889a2687219c55d0d552840efe Mon Sep 17 00:00:00 2001 From: Anusha Ragunathan Date: Tue, 4 Oct 2016 12:01:19 -0700 Subject: [PATCH 326/978] Add plugin create functionality. Signed-off-by: Anusha Ragunathan Upstream-commit: 3d7a95829efba4f088ea8632d6fd1cdfbb5db366 Component: cli --- components/cli/interface_experimental.go | 3 +++ components/cli/plugin_create.go | 26 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 components/cli/plugin_create.go diff --git a/components/cli/interface_experimental.go b/components/cli/interface_experimental.go index 4f5cf853b8..709b5d8ffb 100644 --- a/components/cli/interface_experimental.go +++ b/components/cli/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/components/cli/plugin_create.go b/components/cli/plugin_create.go new file mode 100644 index 0000000000..a660ba5733 --- /dev/null +++ b/components/cli/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 fc02a64b8cc195fa1f36fbea752946449e5071bc Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Fri, 21 Oct 2016 18:07:55 -0700 Subject: [PATCH 327/978] Add support for swarm init lock and swarm unlock Signed-off-by: Tonis Tiigi Upstream-commit: d006a04357b3503bb6550348e79b8ebf6acbb100 Component: cli --- components/cli/command/swarm/cmd.go | 1 + components/cli/command/swarm/init.go | 46 ++++++++++++++++++++++++++ components/cli/command/swarm/opts.go | 1 + components/cli/command/swarm/unlock.go | 35 ++++++++++++++++++++ components/cli/command/system/info.go | 2 +- 5 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 components/cli/command/swarm/unlock.go diff --git a/components/cli/command/swarm/cmd.go b/components/cli/command/swarm/cmd.go index f0a6bcdeb8..5ea973bb78 100644 --- a/components/cli/command/swarm/cmd.go +++ b/components/cli/command/swarm/cmd.go @@ -24,6 +24,7 @@ func NewSwarmCommand(dockerCli *command.DockerCli) *cobra.Command { newJoinTokenCommand(dockerCli), newUpdateCommand(dockerCli), newLeaveCommand(dockerCli), + newUnlockCommand(dockerCli), ) return cmd } diff --git a/components/cli/command/swarm/init.go b/components/cli/command/swarm/init.go index 16f372f8d7..b2590e1568 100644 --- a/components/cli/command/swarm/init.go +++ b/components/cli/command/swarm/init.go @@ -1,10 +1,15 @@ package swarm import ( + "bufio" + "crypto/rand" "errors" "fmt" + "io" + "math/big" "strings" + "golang.org/x/crypto/ssh/terminal" "golang.org/x/net/context" "github.com/docker/docker/api/types/swarm" @@ -20,6 +25,7 @@ type initOptions struct { // Not a NodeAddrOption because it has no default port. advertiseAddr string forceNewCluster bool + lockKey bool } func newInitCommand(dockerCli *command.DockerCli) *cobra.Command { @@ -39,6 +45,7 @@ func newInitCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() flags.Var(&opts.listenAddr, flagListenAddr, "Listen address (format: [:port])") flags.StringVar(&opts.advertiseAddr, flagAdvertiseAddr, "", "Advertised address (format: [:port])") + flags.BoolVar(&opts.lockKey, flagLockKey, false, "Encrypt swarm with optionally provided key from stdin") flags.BoolVar(&opts.forceNewCluster, "force-new-cluster", false, "Force create a new cluster from current state") addSwarmFlags(flags, &opts.swarmOptions) return cmd @@ -48,11 +55,31 @@ func runInit(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts initOption client := dockerCli.Client() ctx := context.Background() + var lockKey string + if opts.lockKey { + var err error + lockKey, err = readKey(dockerCli.In(), "Please enter key for encrypting swarm(leave empty to generate): ") + if err != nil { + return err + } + if len(lockKey) == 0 { + randBytes := make([]byte, 16) + if _, err := rand.Read(randBytes[:]); err != nil { + panic(fmt.Errorf("failed to general random lock key: %v", err)) + } + + var n big.Int + n.SetBytes(randBytes[:]) + lockKey = n.Text(36) + } + } + req := swarm.InitRequest{ ListenAddr: opts.listenAddr.String(), AdvertiseAddr: opts.advertiseAddr, ForceNewCluster: opts.forceNewCluster, Spec: opts.swarmOptions.ToSpec(flags), + LockKey: lockKey, } nodeID, err := client.SwarmInit(ctx, req) @@ -65,6 +92,10 @@ func runInit(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts initOption fmt.Fprintf(dockerCli.Out(), "Swarm initialized: current node (%s) is now a manager.\n\n", nodeID) + if len(lockKey) > 0 { + fmt.Fprintf(dockerCli.Out(), "Swarm is encrypted. When a node is restarted it needs to be unlocked by running command:\n\n echo '%s' | docker swarm unlock\n\n", lockKey) + } + if err := printJoinCommand(ctx, dockerCli, nodeID, true, false); err != nil { return err } @@ -72,3 +103,18 @@ func runInit(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts initOption fmt.Fprint(dockerCli.Out(), "To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.\n\n") return nil } + +func readKey(in *command.InStream, prompt string) (string, error) { + if in.IsTerminal() { + fmt.Print(prompt) + dt, err := terminal.ReadPassword(int(in.FD())) + fmt.Println() + return string(dt), err + } else { + key, err := bufio.NewReader(in).ReadString('\n') + if err == io.EOF { + err = nil + } + return strings.TrimSpace(key), err + } +} diff --git a/components/cli/command/swarm/opts.go b/components/cli/command/swarm/opts.go index ce5a9b1de0..a08c761a6d 100644 --- a/components/cli/command/swarm/opts.go +++ b/components/cli/command/swarm/opts.go @@ -26,6 +26,7 @@ const ( flagExternalCA = "external-ca" flagMaxSnapshots = "max-snapshots" flagSnapshotInterval = "snapshot-interval" + flagLockKey = "lock-key" ) type swarmOptions struct { diff --git a/components/cli/command/swarm/unlock.go b/components/cli/command/swarm/unlock.go new file mode 100644 index 0000000000..03a11da556 --- /dev/null +++ b/components/cli/command/swarm/unlock.go @@ -0,0 +1,35 @@ +package swarm + +import ( + "context" + + "github.com/spf13/cobra" + + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" +) + +func newUnlockCommand(dockerCli *command.DockerCli) *cobra.Command { + cmd := &cobra.Command{ + Use: "unlock", + Short: "Unlock swarm", + Args: cli.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + client := dockerCli.Client() + ctx := context.Background() + + key, err := readKey(dockerCli.In(), "Please enter unlock key: ") + if err != nil { + return err + } + req := swarm.UnlockRequest{ + LockKey: string(key), + } + + return client.SwarmUnlock(ctx, req) + }, + } + + return cmd +} diff --git a/components/cli/command/system/info.go b/components/cli/command/system/info.go index 5ea23ed430..da5a396d64 100644 --- a/components/cli/command/system/info.go +++ b/components/cli/command/system/info.go @@ -96,7 +96,7 @@ func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error { } fmt.Fprintf(dockerCli.Out(), "Swarm: %v\n", info.Swarm.LocalNodeState) - if info.Swarm.LocalNodeState != swarm.LocalNodeStateInactive { + if info.Swarm.LocalNodeState != swarm.LocalNodeStateInactive && info.Swarm.LocalNodeState != swarm.LocalNodeStateLocked { fmt.Fprintf(dockerCli.Out(), " NodeID: %s\n", info.Swarm.NodeID) if info.Swarm.Error != "" { fmt.Fprintf(dockerCli.Out(), " Error: %v\n", info.Swarm.Error) From 798fd349f46707bcbfc7d22206d44245f58ca87c Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Fri, 21 Oct 2016 18:07:55 -0700 Subject: [PATCH 328/978] Add support for swarm init lock and swarm unlock Signed-off-by: Tonis Tiigi Upstream-commit: dd81022c2368e120c0ab4aca0d32e91b78048a11 Component: cli --- components/cli/interface.go | 1 + components/cli/swarm_unlock.go | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 components/cli/swarm_unlock.go diff --git a/components/cli/interface.go b/components/cli/interface.go index 49b66b1d17..d0834afa94 100644 --- a/components/cli/interface.go +++ b/components/cli/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/components/cli/swarm_unlock.go b/components/cli/swarm_unlock.go new file mode 100644 index 0000000000..addfb59f0a --- /dev/null +++ b/components/cli/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 c3469cb075ee26ef774c1a8914e5abf447b1d911 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Thu, 27 Oct 2016 18:50:49 -0700 Subject: [PATCH 329/978] 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 Upstream-commit: 56b7ad90b1d0dcf61ed910eaf4ced22008284a28 Component: cli --- components/cli/command/swarm/cmd.go | 1 + components/cli/command/swarm/init.go | 66 +++++----------------- components/cli/command/swarm/opts.go | 6 ++ components/cli/command/swarm/unlock.go | 21 ++++++- components/cli/command/swarm/unlock_key.go | 57 +++++++++++++++++++ components/cli/command/swarm/update.go | 13 +++++ 6 files changed, 112 insertions(+), 52 deletions(-) create mode 100644 components/cli/command/swarm/unlock_key.go diff --git a/components/cli/command/swarm/cmd.go b/components/cli/command/swarm/cmd.go index 5ea973bb78..6c70459df0 100644 --- a/components/cli/command/swarm/cmd.go +++ b/components/cli/command/swarm/cmd.go @@ -22,6 +22,7 @@ func NewSwarmCommand(dockerCli *command.DockerCli) *cobra.Command { newInitCommand(dockerCli), newJoinCommand(dockerCli), newJoinTokenCommand(dockerCli), + newUnlockKeyCommand(dockerCli), newUpdateCommand(dockerCli), newLeaveCommand(dockerCli), newUnlockCommand(dockerCli), diff --git a/components/cli/command/swarm/init.go b/components/cli/command/swarm/init.go index b2590e1568..93c97c3a74 100644 --- a/components/cli/command/swarm/init.go +++ b/components/cli/command/swarm/init.go @@ -1,20 +1,15 @@ package swarm import ( - "bufio" - "crypto/rand" - "errors" "fmt" - "io" - "math/big" "strings" - "golang.org/x/crypto/ssh/terminal" "golang.org/x/net/context" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -25,7 +20,6 @@ type initOptions struct { // Not a NodeAddrOption because it has no default port. advertiseAddr string forceNewCluster bool - lockKey bool } func newInitCommand(dockerCli *command.DockerCli) *cobra.Command { @@ -45,7 +39,6 @@ func newInitCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() flags.Var(&opts.listenAddr, flagListenAddr, "Listen address (format: [:port])") flags.StringVar(&opts.advertiseAddr, flagAdvertiseAddr, "", "Advertised address (format: [:port])") - flags.BoolVar(&opts.lockKey, flagLockKey, false, "Encrypt swarm with optionally provided key from stdin") flags.BoolVar(&opts.forceNewCluster, "force-new-cluster", false, "Force create a new cluster from current state") addSwarmFlags(flags, &opts.swarmOptions) return cmd @@ -55,31 +48,12 @@ func runInit(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts initOption client := dockerCli.Client() ctx := context.Background() - var lockKey string - if opts.lockKey { - var err error - lockKey, err = readKey(dockerCli.In(), "Please enter key for encrypting swarm(leave empty to generate): ") - if err != nil { - return err - } - if len(lockKey) == 0 { - randBytes := make([]byte, 16) - if _, err := rand.Read(randBytes[:]); err != nil { - panic(fmt.Errorf("failed to general random lock key: %v", err)) - } - - var n big.Int - n.SetBytes(randBytes[:]) - lockKey = n.Text(36) - } - } - req := swarm.InitRequest{ - ListenAddr: opts.listenAddr.String(), - AdvertiseAddr: opts.advertiseAddr, - ForceNewCluster: opts.forceNewCluster, - Spec: opts.swarmOptions.ToSpec(flags), - LockKey: lockKey, + ListenAddr: opts.listenAddr.String(), + AdvertiseAddr: opts.advertiseAddr, + ForceNewCluster: opts.forceNewCluster, + Spec: opts.swarmOptions.ToSpec(flags), + AutoLockManagers: opts.swarmOptions.autolock, } nodeID, err := client.SwarmInit(ctx, req) @@ -92,29 +66,19 @@ func runInit(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts initOption fmt.Fprintf(dockerCli.Out(), "Swarm initialized: current node (%s) is now a manager.\n\n", nodeID) - if len(lockKey) > 0 { - fmt.Fprintf(dockerCli.Out(), "Swarm is encrypted. When a node is restarted it needs to be unlocked by running command:\n\n echo '%s' | docker swarm unlock\n\n", lockKey) - } - if err := printJoinCommand(ctx, dockerCli, nodeID, true, false); err != nil { return err } fmt.Fprint(dockerCli.Out(), "To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.\n\n") + + if req.AutoLockManagers { + unlockKeyResp, err := client.SwarmGetUnlockKey(ctx) + if err != nil { + return errors.Wrap(err, "could not fetch unlock key") + } + printUnlockCommand(ctx, dockerCli, unlockKeyResp.UnlockKey) + } + return nil } - -func readKey(in *command.InStream, prompt string) (string, error) { - if in.IsTerminal() { - fmt.Print(prompt) - dt, err := terminal.ReadPassword(int(in.FD())) - fmt.Println() - return string(dt), err - } else { - key, err := bufio.NewReader(in).ReadString('\n') - if err == io.EOF { - err = nil - } - return strings.TrimSpace(key), err - } -} diff --git a/components/cli/command/swarm/opts.go b/components/cli/command/swarm/opts.go index a08c761a6d..8682375b15 100644 --- a/components/cli/command/swarm/opts.go +++ b/components/cli/command/swarm/opts.go @@ -27,6 +27,7 @@ const ( flagMaxSnapshots = "max-snapshots" flagSnapshotInterval = "snapshot-interval" flagLockKey = "lock-key" + flagAutolock = "autolock" ) type swarmOptions struct { @@ -36,6 +37,7 @@ type swarmOptions struct { externalCA ExternalCAOption maxSnapshots uint64 snapshotInterval uint64 + autolock bool } // NodeAddrOption is a pflag.Value for listening addresses @@ -174,6 +176,7 @@ func addSwarmFlags(flags *pflag.FlagSet, opts *swarmOptions) { flags.Var(&opts.externalCA, flagExternalCA, "Specifications of one or more certificate signing endpoints") flags.Uint64Var(&opts.maxSnapshots, flagMaxSnapshots, 0, "Number of additional Raft snapshots to retain") flags.Uint64Var(&opts.snapshotInterval, flagSnapshotInterval, 10000, "Number of log entries between Raft snapshots") + flags.BoolVar(&opts.autolock, flagAutolock, false, "Enable or disable manager autolocking (requiring an unlock key to start a stopped manager)") } func (opts *swarmOptions) mergeSwarmSpec(spec *swarm.Spec, flags *pflag.FlagSet) { @@ -195,6 +198,9 @@ func (opts *swarmOptions) mergeSwarmSpec(spec *swarm.Spec, flags *pflag.FlagSet) if flags.Changed(flagSnapshotInterval) { spec.Raft.SnapshotInterval = opts.snapshotInterval } + if flags.Changed(flagAutolock) { + spec.EncryptionConfig.AutoLockManagers = opts.autolock + } } func (opts *swarmOptions) ToSpec(flags *pflag.FlagSet) swarm.Spec { diff --git a/components/cli/command/swarm/unlock.go b/components/cli/command/swarm/unlock.go index 03a11da556..51b06d6267 100644 --- a/components/cli/command/swarm/unlock.go +++ b/components/cli/command/swarm/unlock.go @@ -1,9 +1,14 @@ package swarm import ( + "bufio" "context" + "fmt" + "io" + "strings" "github.com/spf13/cobra" + "golang.org/x/crypto/ssh/terminal" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" @@ -24,7 +29,7 @@ func newUnlockCommand(dockerCli *command.DockerCli) *cobra.Command { return err } req := swarm.UnlockRequest{ - LockKey: string(key), + UnlockKey: key, } return client.SwarmUnlock(ctx, req) @@ -33,3 +38,17 @@ func newUnlockCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } + +func readKey(in *command.InStream, prompt string) (string, error) { + if in.IsTerminal() { + fmt.Print(prompt) + dt, err := terminal.ReadPassword(int(in.FD())) + fmt.Println() + return string(dt), err + } + key, err := bufio.NewReader(in).ReadString('\n') + if err == io.EOF { + err = nil + } + return strings.TrimSpace(key), err +} diff --git a/components/cli/command/swarm/unlock_key.go b/components/cli/command/swarm/unlock_key.go new file mode 100644 index 0000000000..19caa0cc2b --- /dev/null +++ b/components/cli/command/swarm/unlock_key.go @@ -0,0 +1,57 @@ +package swarm + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/pkg/errors" + "golang.org/x/net/context" +) + +func newUnlockKeyCommand(dockerCli *command.DockerCli) *cobra.Command { + var rotate, quiet bool + + cmd := &cobra.Command{ + Use: "unlock-key [OPTIONS]", + Short: "Manage the unlock key", + Args: cli.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + client := dockerCli.Client() + ctx := context.Background() + + if rotate { + // FIXME(aaronl) + } + + unlockKeyResp, err := client.SwarmGetUnlockKey(ctx) + if err != nil { + return errors.Wrap(err, "could not fetch unlock key") + } + + if quiet { + fmt.Fprintln(dockerCli.Out(), unlockKeyResp.UnlockKey) + } else { + printUnlockCommand(ctx, dockerCli, unlockKeyResp.UnlockKey) + } + return nil + }, + } + + flags := cmd.Flags() + flags.BoolVar(&rotate, flagRotate, false, "Rotate unlock key") + flags.BoolVarP(&quiet, flagQuiet, "q", false, "Only display token") + + return cmd +} + +func printUnlockCommand(ctx context.Context, dockerCli *command.DockerCli, unlockKey string) { + if len(unlockKey) == 0 { + return + } + + fmt.Fprintf(dockerCli.Out(), "To unlock a swarm manager after it restarts, run the `docker swarm unlock`\ncommand and provide the following key:\n\n %s\n\nPlease remember to store this key in a password manager, since without it you\nwill not be able to restart the manager.\n", unlockKey) + return +} diff --git a/components/cli/command/swarm/update.go b/components/cli/command/swarm/update.go index a39f34c881..7c88760492 100644 --- a/components/cli/command/swarm/update.go +++ b/components/cli/command/swarm/update.go @@ -8,6 +8,7 @@ import ( "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -39,8 +40,12 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts swarmOpt return err } + prevAutoLock := swarm.Spec.EncryptionConfig.AutoLockManagers + opts.mergeSwarmSpec(&swarm.Spec, flags) + curAutoLock := swarm.Spec.EncryptionConfig.AutoLockManagers + err = client.SwarmUpdate(ctx, swarm.Version, swarm.Spec, updateFlags) if err != nil { return err @@ -48,5 +53,13 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts swarmOpt fmt.Fprintln(dockerCli.Out(), "Swarm updated.") + if curAutoLock && !prevAutoLock { + unlockKeyResp, err := client.SwarmGetUnlockKey(ctx) + if err != nil { + return errors.Wrap(err, "could not fetch unlock key") + } + printUnlockCommand(ctx, dockerCli, unlockKeyResp.UnlockKey) + } + return nil } From 50e796a0bea28a595da3ffe62538ed166ae85600 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Thu, 27 Oct 2016 18:50:49 -0700 Subject: [PATCH 330/978] 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 Upstream-commit: a8dc2ff916ff27a699b31353a4912a04a04af31e Component: cli --- components/cli/interface.go | 1 + components/cli/swarm_get_unlock_key.go | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 components/cli/swarm_get_unlock_key.go diff --git a/components/cli/interface.go b/components/cli/interface.go index d0834afa94..f24c9a51f6 100644 --- a/components/cli/interface.go +++ b/components/cli/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/components/cli/swarm_get_unlock_key.go b/components/cli/swarm_get_unlock_key.go new file mode 100644 index 0000000000..be28d32628 --- /dev/null +++ b/components/cli/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 f93e810a9bd2eea97295fe35f01e192f27b625c4 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Fri, 28 Oct 2016 16:35:49 -0700 Subject: [PATCH 331/978] Add unlock key rotation Signed-off-by: Aaron Lehmann Upstream-commit: 65e1e166ee8dd6b2afd3d50072ecb0c06d3e2a5c Component: cli --- components/cli/command/swarm/unlock_key.go | 24 +++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/components/cli/command/swarm/unlock_key.go b/components/cli/command/swarm/unlock_key.go index 19caa0cc2b..96450f55b8 100644 --- a/components/cli/command/swarm/unlock_key.go +++ b/components/cli/command/swarm/unlock_key.go @@ -5,6 +5,7 @@ import ( "github.com/spf13/cobra" + "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/pkg/errors" @@ -23,7 +24,24 @@ func newUnlockKeyCommand(dockerCli *command.DockerCli) *cobra.Command { ctx := context.Background() if rotate { - // FIXME(aaronl) + flags := swarm.UpdateFlags{RotateManagerUnlockKey: true} + + swarm, err := client.SwarmInspect(ctx) + if err != nil { + return err + } + + if !swarm.Spec.EncryptionConfig.AutoLockManagers { + return errors.New("cannot rotate because autolock is not turned on") + } + + err = client.SwarmUpdate(ctx, swarm.Version, swarm.Spec, flags) + if err != nil { + return err + } + if !quiet { + fmt.Fprintf(dockerCli.Out(), "Successfully rotated manager unlock key.\n\n") + } } unlockKeyResp, err := client.SwarmGetUnlockKey(ctx) @@ -31,6 +49,10 @@ func newUnlockKeyCommand(dockerCli *command.DockerCli) *cobra.Command { return errors.Wrap(err, "could not fetch unlock key") } + if unlockKeyResp.UnlockKey == "" { + return errors.New("no unlock key is set") + } + if quiet { fmt.Fprintln(dockerCli.Out(), unlockKeyResp.UnlockKey) } else { From b9674ce3e86bed0aa844d97c28712f9639438a05 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Fri, 28 Oct 2016 16:35:49 -0700 Subject: [PATCH 332/978] Add unlock key rotation Signed-off-by: Aaron Lehmann Upstream-commit: de1b8f94399039109d34068eff875ebf3de27638 Component: cli --- components/cli/swarm_update.go | 1 + 1 file changed, 1 insertion(+) diff --git a/components/cli/swarm_update.go b/components/cli/swarm_update.go index f0be145ba2..cc8eeb6554 100644 --- a/components/cli/swarm_update.go +++ b/components/cli/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 90efedd5e2870a062e38954106a23ff0c5ebfab4 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Wed, 9 Nov 2016 16:59:01 -0800 Subject: [PATCH 333/978] fix manpages Signed-off-by: Victor Vieux Upstream-commit: 96c16101dd6e81eb9427aa970e599d2bb908972a Component: cli --- components/cli/command/secret/create.go | 2 +- components/cli/command/secret/inspect.go | 3 +-- components/cli/command/secret/ls.go | 2 +- components/cli/command/secret/remove.go | 2 +- components/cli/command/secret/utils.go | 3 +-- 5 files changed, 5 insertions(+), 7 deletions(-) diff --git a/components/cli/command/secret/create.go b/components/cli/command/secret/create.go index 9800048341..da1cb9275e 100644 --- a/components/cli/command/secret/create.go +++ b/components/cli/command/secret/create.go @@ -1,7 +1,6 @@ package secret import ( - "context" "fmt" "io/ioutil" "os" @@ -12,6 +11,7 @@ import ( "github.com/docker/docker/opts" runconfigopts "github.com/docker/docker/runconfig/opts" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type createOptions struct { diff --git a/components/cli/command/secret/inspect.go b/components/cli/command/secret/inspect.go index 25da79f16d..ad61706b35 100644 --- a/components/cli/command/secret/inspect.go +++ b/components/cli/command/secret/inspect.go @@ -1,12 +1,11 @@ package secret import ( - "context" - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/inspect" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type inspectOptions struct { diff --git a/components/cli/command/secret/ls.go b/components/cli/command/secret/ls.go index 67fc1daff6..7471f08b19 100644 --- a/components/cli/command/secret/ls.go +++ b/components/cli/command/secret/ls.go @@ -1,7 +1,6 @@ package secret import ( - "context" "fmt" "text/tabwriter" "time" @@ -11,6 +10,7 @@ import ( "github.com/docker/docker/cli/command" "github.com/docker/go-units" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type listOptions struct { diff --git a/components/cli/command/secret/remove.go b/components/cli/command/secret/remove.go index d277eceba2..0ee6d9f574 100644 --- a/components/cli/command/secret/remove.go +++ b/components/cli/command/secret/remove.go @@ -1,12 +1,12 @@ package secret import ( - "context" "fmt" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type removeOptions struct { diff --git a/components/cli/command/secret/utils.go b/components/cli/command/secret/utils.go index d1a7d97c44..621e60aaaa 100644 --- a/components/cli/command/secret/utils.go +++ b/components/cli/command/secret/utils.go @@ -1,12 +1,11 @@ package secret import ( - "context" - "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/client" + "golang.org/x/net/context" ) func getSecretsByName(client client.APIClient, ctx context.Context, names []string) ([]swarm.Secret, error) { From 31f3e07b0b97046dfeed6c81f9c3fab98eab1755 Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Thu, 10 Nov 2016 10:39:23 +0800 Subject: [PATCH 334/978] Remove redundant parameter and fix typos Signed-off-by: yuexiao-wang Upstream-commit: b6fe99530cc885e9841f4ded90b409581886bbd4 Component: cli --- components/cli/command/container/exec.go | 5 ++--- components/cli/command/container/exec_test.go | 7 +++---- components/cli/command/stack/list.go | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/components/cli/command/container/exec.go b/components/cli/command/container/exec.go index 84eba113cf..4bc8c58066 100644 --- a/components/cli/command/container/exec.go +++ b/components/cli/command/container/exec.go @@ -65,7 +65,7 @@ func NewExecCommand(dockerCli *command.DockerCli) *cobra.Command { } func runExec(dockerCli *command.DockerCli, opts *execOptions, container string, execCmd []string) error { - execConfig, err := parseExec(opts, container, execCmd) + execConfig, err := parseExec(opts, execCmd) // just in case the ParseExec does not exit if container == "" || err != nil { return cli.StatusError{StatusCode: 1} @@ -181,14 +181,13 @@ func getExecExitCode(ctx context.Context, client apiclient.ContainerAPIClient, e // parseExec parses the specified args for the specified command and generates // an ExecConfig from it. -func parseExec(opts *execOptions, container string, execCmd []string) (*types.ExecConfig, error) { +func parseExec(opts *execOptions, execCmd []string) (*types.ExecConfig, error) { execConfig := &types.ExecConfig{ User: opts.user, Privileged: opts.privileged, Tty: opts.tty, Cmd: execCmd, Detach: opts.detach, - // container is not used here } // If -d is not set, attach to everything by default diff --git a/components/cli/command/container/exec_test.go b/components/cli/command/container/exec_test.go index 2e122e7386..baeeaf1904 100644 --- a/components/cli/command/container/exec_test.go +++ b/components/cli/command/container/exec_test.go @@ -7,9 +7,8 @@ import ( ) type arguments struct { - options execOptions - container string - execCmd []string + options execOptions + execCmd []string } func TestParseExec(t *testing.T) { @@ -73,7 +72,7 @@ func TestParseExec(t *testing.T) { } for valid, expectedExecConfig := range valids { - execConfig, err := parseExec(&valid.options, valid.container, valid.execCmd) + execConfig, err := parseExec(&valid.options, valid.execCmd) if err != nil { t.Fatal(err) } diff --git a/components/cli/command/stack/list.go b/components/cli/command/stack/list.go index f655b929ad..7be42525dd 100644 --- a/components/cli/command/stack/list.go +++ b/components/cli/command/stack/list.go @@ -72,7 +72,7 @@ func printTable(out io.Writer, stacks []*stack) { type stack struct { // Name is the name of the stack - Name string + Name string // Services is the number of the services Services int } From 3aca7843a9717525cff50e3ab7b2afd6e208c418 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Mon, 7 Nov 2016 18:51:47 -0800 Subject: [PATCH 335/978] rename plugin manifest Signed-off-by: Victor Vieux Upstream-commit: 0ae9598f96597d13e961413b98160678a78dd062 Component: cli --- components/cli/command/plugin/create.go | 14 +++++++------- components/cli/command/plugin/list.go | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/components/cli/command/plugin/create.go b/components/cli/command/plugin/create.go index 3b18ed3750..94c0d2c367 100644 --- a/components/cli/command/plugin/create.go +++ b/components/cli/command/plugin/create.go @@ -24,14 +24,14 @@ func validateTag(rawRepo string) error { return err } -// validateManifest ensures that a valid manifest.json is available in the given path -func validateManifest(path string) error { - dt, err := os.Open(filepath.Join(path, "manifest.json")) +// validateConfig ensures that a valid config.json is available in the given path +func validateConfig(path string) error { + dt, err := os.Open(filepath.Join(path, "config.json")) if err != nil { return err } - m := types.PluginManifest{} + m := types.PluginConfig{} err = json.NewDecoder(dt).Decode(&m) dt.Close() @@ -64,8 +64,8 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { options := pluginCreateOptions{} cmd := &cobra.Command{ - Use: "create [OPTIONS] reponame[:tag] PATH-TO-ROOTFS (rootfs + manifest.json)", - Short: "Create a plugin from a rootfs and manifest", + Use: "create [OPTIONS] reponame[:tag] PATH-TO-ROOTFS (rootfs + config.json)", + Short: "Create a plugin from a rootfs and config", Args: cli.RequiresMinArgs(2), RunE: func(cmd *cobra.Command, args []string) error { options.repoName = args[0] @@ -96,7 +96,7 @@ func runCreate(dockerCli *command.DockerCli, options pluginCreateOptions) error return err } - if err := validateManifest(options.context); err != nil { + if err := validateConfig(options.context); err != nil { return err } diff --git a/components/cli/command/plugin/list.go b/components/cli/command/plugin/list.go index 9d4b46d120..e402d44b31 100644 --- a/components/cli/command/plugin/list.go +++ b/components/cli/command/plugin/list.go @@ -47,7 +47,7 @@ func runList(dockerCli *command.DockerCli, opts listOptions) error { fmt.Fprintf(w, "\n") for _, p := range plugins { - desc := strings.Replace(p.Manifest.Description, "\n", " ", -1) + desc := strings.Replace(p.Config.Description, "\n", " ", -1) desc = strings.Replace(desc, "\r", " ", -1) if !opts.noTrunc { desc = stringutils.Ellipsis(desc, 45) From 3f3a95ef3e35730a62073fcb9067d24505da0045 Mon Sep 17 00:00:00 2001 From: Andrew Hsu Date: Thu, 10 Nov 2016 08:23:19 -0800 Subject: [PATCH 336/978] use "golang.org/x/net/context" instead of "context" Signed-off-by: Andrew Hsu Upstream-commit: dc32cb6c772869d401a9af6f50ccefa77f555252 Component: cli --- components/cli/command/swarm/unlock.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/swarm/unlock.go b/components/cli/command/swarm/unlock.go index 51b06d6267..048fb56e3d 100644 --- a/components/cli/command/swarm/unlock.go +++ b/components/cli/command/swarm/unlock.go @@ -2,7 +2,6 @@ package swarm import ( "bufio" - "context" "fmt" "io" "strings" @@ -13,6 +12,7 @@ import ( "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "golang.org/x/net/context" ) func newUnlockCommand(dockerCli *command.DockerCli) *cobra.Command { From ab7cf401802466ded97ff754703906da8ff5f56b Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 2 Nov 2016 14:57:40 -0400 Subject: [PATCH 337/978] Convert deploy to use a compose-file. Signed-off-by: Daniel Nephin Upstream-commit: f702b722d8e7420820ab1eb1262829e0b590f57a Component: cli --- components/cli/command/service/opts.go | 5 +- components/cli/command/service/update.go | 2 +- components/cli/command/stack/cmd.go | 1 - components/cli/command/stack/config.go | 39 ---- components/cli/command/stack/deploy.go | 253 ++++++++++++++++------- components/cli/command/stack/opts.go | 9 +- 6 files changed, 185 insertions(+), 124 deletions(-) delete mode 100644 components/cli/command/stack/config.go diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index c48c952e0c..2113fdfede 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -297,7 +297,7 @@ func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec { ports, portBindings, _ := nat.ParsePortSpecs(e.ports.GetAll()) for port := range ports { - portConfigs = append(portConfigs, convertPortToPortConfig(port, portBindings)...) + portConfigs = append(portConfigs, ConvertPortToPortConfig(port, portBindings)...) } return &swarm.EndpointSpec{ @@ -306,7 +306,8 @@ func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec { } } -func convertPortToPortConfig( +// ConvertPortToPortConfig converts ports to the swarm type +func ConvertPortToPortConfig( port nat.Port, portBindings map[nat.Port][]nat.PortBinding, ) []swarm.PortConfig { diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index 9741f67d54..d1c695d75d 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -631,7 +631,7 @@ func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) error { ports, portBindings, _ := nat.ParsePortSpecs(values) for port := range ports { - newConfigs := convertPortToPortConfig(port, portBindings) + newConfigs := ConvertPortToPortConfig(port, portBindings) for _, entry := range newConfigs { if v, ok := portSet[portConfigToString(&entry)]; ok && v != entry { return fmt.Errorf("conflicting port mapping between %v:%v/%s and %v:%v/%s", entry.PublishedPort, entry.TargetPort, entry.Protocol, v.PublishedPort, v.TargetPort, v.Protocol) diff --git a/components/cli/command/stack/cmd.go b/components/cli/command/stack/cmd.go index 4189504403..ff71e0ddfa 100644 --- a/components/cli/command/stack/cmd.go +++ b/components/cli/command/stack/cmd.go @@ -19,7 +19,6 @@ func NewStackCommand(dockerCli *command.DockerCli) *cobra.Command { Tags: map[string]string{"experimental": "", "version": "1.25"}, } cmd.AddCommand( - newConfigCommand(dockerCli), newDeployCommand(dockerCli), newListCommand(dockerCli), newRemoveCommand(dockerCli), diff --git a/components/cli/command/stack/config.go b/components/cli/command/stack/config.go deleted file mode 100644 index 56e554a86e..0000000000 --- a/components/cli/command/stack/config.go +++ /dev/null @@ -1,39 +0,0 @@ -package stack - -import ( - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/bundlefile" - "github.com/spf13/cobra" -) - -type configOptions struct { - bundlefile string - namespace string -} - -func newConfigCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts configOptions - - cmd := &cobra.Command{ - Use: "config [OPTIONS] STACK", - Short: "Print the stack configuration", - Args: cli.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - opts.namespace = args[0] - return runConfig(dockerCli, opts) - }, - } - - flags := cmd.Flags() - addBundlefileFlag(&opts.bundlefile, flags) - return cmd -} - -func runConfig(dockerCli *command.DockerCli, opts configOptions) error { - bundle, err := loadBundlefile(dockerCli.Err(), opts.namespace, opts.bundlefile) - if err != nil { - return err - } - return bundlefile.Print(dockerCli.Out(), bundle) -} diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index 435a9193b4..c1faa0521c 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -2,16 +2,22 @@ package stack import ( "fmt" - "strings" + "io/ioutil" + "os" + "time" "github.com/spf13/cobra" "golang.org/x/net/context" + "github.com/aanand/compose-file/loader" + composetypes "github.com/aanand/compose-file/types" "github.com/docker/docker/api/types" + networktypes "github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/bundlefile" + servicecmd "github.com/docker/docker/cli/command/service" + "github.com/docker/go-connections/nat" ) const ( @@ -19,7 +25,7 @@ const ( ) type deployOptions struct { - bundlefile string + composefile string namespace string sendRegistryAuth bool } @@ -30,63 +36,69 @@ func newDeployCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := &cobra.Command{ Use: "deploy [OPTIONS] STACK", Aliases: []string{"up"}, - Short: "Create and update a stack from a Distributed Application Bundle (DAB)", + Short: "Deploy a new stack or update an existing stack", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - opts.namespace = strings.TrimSuffix(args[0], ".dab") + opts.namespace = args[0] return runDeploy(dockerCli, opts) }, Tags: map[string]string{"experimental": "", "version": "1.25"}, } flags := cmd.Flags() - addBundlefileFlag(&opts.bundlefile, flags) + addComposefileFlag(&opts.composefile, flags) addRegistryAuthFlag(&opts.sendRegistryAuth, flags) return cmd } func runDeploy(dockerCli *command.DockerCli, opts deployOptions) error { - bundle, err := loadBundlefile(dockerCli.Err(), opts.namespace, opts.bundlefile) + configDetails, err := getConfigDetails(opts) if err != nil { return err } - info, err := dockerCli.Client().Info(context.Background()) + config, err := loader.Load(configDetails) if err != nil { return err } - if !info.Swarm.ControlAvailable { - return fmt.Errorf("This node is not a swarm manager. Use \"docker swarm init\" or \"docker swarm join\" to connect this node to swarm and try again.") - } - networks := getUniqueNetworkNames(bundle.Services) ctx := context.Background() - - if err := updateNetworks(ctx, dockerCli, networks, opts.namespace); err != nil { + if err := createNetworks(ctx, dockerCli, config.Networks, opts.namespace); err != nil { return err } - return deployServices(ctx, dockerCli, bundle.Services, opts.namespace, opts.sendRegistryAuth) + return deployServices(ctx, dockerCli, config, opts.namespace, opts.sendRegistryAuth) } -func getUniqueNetworkNames(services map[string]bundlefile.Service) []string { - networkSet := make(map[string]bool) - for _, service := range services { - for _, network := range service.Networks { - networkSet[network] = true - } +func getConfigDetails(opts deployOptions) (composetypes.ConfigDetails, error) { + var details composetypes.ConfigDetails + var err error + + details.WorkingDir, err = os.Getwd() + if err != nil { + return details, err } - networks := []string{} - for network := range networkSet { - networks = append(networks, network) + configFile, err := getConfigFile(opts.composefile) + if err != nil { + return details, err } - return networks + // TODO: support multiple files + details.ConfigFiles = []composetypes.ConfigFile{*configFile} + return details, nil } -func updateNetworks( +func getConfigFile(filename string) (*composetypes.ConfigFile, error) { + bytes, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + return loader.ParseYAML(bytes, filename) +} + +func createNetworks( ctx context.Context, dockerCli *command.DockerCli, - networks []string, + networks map[string]composetypes.NetworkConfig, namespace string, ) error { client := dockerCli.Client() @@ -101,17 +113,34 @@ func updateNetworks( existingNetworkMap[network.Name] = network } - createOpts := types.NetworkCreate{ - Labels: getStackLabels(namespace, nil), - Driver: defaultNetworkDriver, - } + for internalName, network := range networks { + if network.ExternalName != "" { + continue + } - for _, internalName := range networks { name := fmt.Sprintf("%s_%s", namespace, internalName) - if _, exists := existingNetworkMap[name]; exists { continue } + + createOpts := types.NetworkCreate{ + // TODO: support network labels from compose file + Labels: getStackLabels(namespace, nil), + Driver: network.Driver, + Options: network.DriverOpts, + } + + if network.Ipam.Driver != "" { + createOpts.IPAM = &networktypes.IPAM{ + Driver: network.Ipam.Driver, + } + } + // TODO: IPAMConfig.Config + + if createOpts.Driver == "" { + createOpts.Driver = defaultNetworkDriver + } + fmt.Fprintf(dockerCli.Out(), "Creating network %s\n", name) if _, err := client.NetworkCreate(ctx, name, createOpts); err != nil { return err @@ -120,12 +149,17 @@ func updateNetworks( return nil } -func convertNetworks(networks []string, namespace string, name string) []swarm.NetworkAttachmentConfig { +func convertNetworks( + networks map[string]*composetypes.ServiceNetworkConfig, + namespace string, + name string, +) []swarm.NetworkAttachmentConfig { nets := []swarm.NetworkAttachmentConfig{} - for _, network := range networks { + for networkName, network := range networks { nets = append(nets, swarm.NetworkAttachmentConfig{ - Target: namespace + "_" + network, - Aliases: []string{name}, + // TODO: only do this name mangling in one function + Target: namespace + "_" + networkName, + Aliases: append(network.Aliases, name), }) } return nets @@ -134,12 +168,14 @@ func convertNetworks(networks []string, namespace string, name string) []swarm.N func deployServices( ctx context.Context, dockerCli *command.DockerCli, - services map[string]bundlefile.Service, + config *composetypes.Config, namespace string, sendAuth bool, ) error { apiClient := dockerCli.Client() out := dockerCli.Out() + services := config.Services + volumes := config.Volumes existingServices, err := getServices(ctx, apiClient, namespace) if err != nil { @@ -151,46 +187,12 @@ func deployServices( existingServiceMap[service.Spec.Name] = service } - for internalName, service := range services { - name := fmt.Sprintf("%s_%s", namespace, internalName) + for _, service := range services { + name := fmt.Sprintf("%s_%s", namespace, service.Name) - var ports []swarm.PortConfig - for _, portSpec := range service.Ports { - ports = append(ports, swarm.PortConfig{ - Protocol: swarm.PortConfigProtocol(portSpec.Protocol), - TargetPort: portSpec.Port, - }) - } - - serviceSpec := swarm.ServiceSpec{ - Annotations: swarm.Annotations{ - Name: name, - Labels: getStackLabels(namespace, service.Labels), - }, - TaskTemplate: swarm.TaskSpec{ - ContainerSpec: swarm.ContainerSpec{ - Image: service.Image, - Command: service.Command, - Args: service.Args, - Env: service.Env, - // Service Labels will not be copied to Containers - // automatically during the deployment so we apply - // it here. - Labels: getStackLabels(namespace, nil), - }, - }, - EndpointSpec: &swarm.EndpointSpec{ - Ports: ports, - }, - Networks: convertNetworks(service.Networks, namespace, internalName), - } - - cspec := &serviceSpec.TaskTemplate.ContainerSpec - if service.WorkingDir != nil { - cspec.Dir = *service.WorkingDir - } - if service.User != nil { - cspec.User = *service.User + serviceSpec, err := convertService(namespace, service, volumes) + if err != nil { + return err } encodedAuth := "" @@ -234,3 +236,100 @@ func deployServices( return nil } + +func convertService( + namespace string, + service composetypes.ServiceConfig, + volumes map[string]composetypes.VolumeConfig, +) (swarm.ServiceSpec, error) { + // TODO: remove this duplication + name := fmt.Sprintf("%s_%s", namespace, service.Name) + + endpoint, err := convertEndpointSpec(service.Ports) + if err != nil { + return swarm.ServiceSpec{}, err + } + + mode, err := convertDeployMode(service.Deploy.Mode, service.Deploy.Replicas) + if err != nil { + return swarm.ServiceSpec{}, err + } + + serviceSpec := swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: name, + Labels: getStackLabels(namespace, service.Labels), + }, + TaskTemplate: swarm.TaskSpec{ + ContainerSpec: swarm.ContainerSpec{ + Image: service.Image, + Command: service.Entrypoint, + Args: service.Command, + Env: convertEnvironment(service.Environment), + Labels: getStackLabels(namespace, service.Deploy.Labels), + Dir: service.WorkingDir, + User: service.User, + }, + Placement: &swarm.Placement{ + Constraints: service.Deploy.Placement.Constraints, + }, + }, + EndpointSpec: endpoint, + Mode: mode, + Networks: convertNetworks(service.Networks, namespace, service.Name), + } + + if service.StopGracePeriod != nil { + stopGrace, err := time.ParseDuration(*service.StopGracePeriod) + if err != nil { + return swarm.ServiceSpec{}, err + } + serviceSpec.TaskTemplate.ContainerSpec.StopGracePeriod = &stopGrace + } + + // TODO: convert mounts + return serviceSpec, nil +} + +func convertEndpointSpec(source []string) (*swarm.EndpointSpec, error) { + portConfigs := []swarm.PortConfig{} + ports, portBindings, err := nat.ParsePortSpecs(source) + if err != nil { + return nil, err + } + + for port := range ports { + portConfigs = append( + portConfigs, + servicecmd.ConvertPortToPortConfig(port, portBindings)...) + } + + return &swarm.EndpointSpec{Ports: portConfigs}, nil +} + +func convertEnvironment(source map[string]string) []string { + var output []string + + for name, value := range source { + output = append(output, fmt.Sprintf("%s=%s", name, value)) + } + + return output +} + +func convertDeployMode(mode string, replicas uint64) (swarm.ServiceMode, error) { + serviceMode := swarm.ServiceMode{} + + switch mode { + case "global": + if replicas != 0 { + return serviceMode, fmt.Errorf("replicas can only be used with replicated mode") + } + serviceMode.Global = &swarm.GlobalService{} + case "replicated": + serviceMode.Replicated = &swarm.ReplicatedService{Replicas: &replicas} + default: + return serviceMode, fmt.Errorf("Unknown mode: %s", mode) + } + return serviceMode, nil +} diff --git a/components/cli/command/stack/opts.go b/components/cli/command/stack/opts.go index 5f2d8b5d0a..c2cc0d1e70 100644 --- a/components/cli/command/stack/opts.go +++ b/components/cli/command/stack/opts.go @@ -9,11 +9,12 @@ import ( "github.com/spf13/pflag" ) +func addComposefileFlag(opt *string, flags *pflag.FlagSet) { + flags.StringVar(opt, "compose-file", "", "Path to a Compose file") +} + func addBundlefileFlag(opt *string, flags *pflag.FlagSet) { - flags.StringVar( - opt, - "file", "", - "Path to a Distributed Application Bundle file (Default: STACK.dab)") + flags.StringVar(opt, "bundle-file", "", "Path to a Distributed Application Bundle file") } func addRegistryAuthFlag(opt *bool, flags *pflag.FlagSet) { From 3054994651ce826fcb8d267a657e507d01080ac7 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 25 Oct 2016 14:41:45 -0700 Subject: [PATCH 338/978] Add support for service-level 'volumes' key Support volume driver + options Support external volumes Support hostname in Compose file Signed-off-by: Aanand Prasad Upstream-commit: a9fc9b60feba5a6962e5583ca0a3a428c6c58b0e Component: cli --- components/cli/command/stack/deploy.go | 108 ++++++++++++++++++++++--- 1 file changed, 99 insertions(+), 9 deletions(-) diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index c1faa0521c..96bd175450 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -4,6 +4,7 @@ import ( "fmt" "io/ioutil" "os" + "strings" "time" "github.com/spf13/cobra" @@ -12,6 +13,7 @@ import ( "github.com/aanand/compose-file/loader" composetypes "github.com/aanand/compose-file/types" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/mount" networktypes "github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" @@ -92,7 +94,14 @@ func getConfigFile(filename string) (*composetypes.ConfigFile, error) { if err != nil { return nil, err } - return loader.ParseYAML(bytes, filename) + config, err := loader.ParseYAML(bytes) + if err != nil { + return nil, err + } + return &composetypes.ConfigFile{ + Filename: filename, + Config: config, + }, nil } func createNetworks( @@ -114,7 +123,7 @@ func createNetworks( } for internalName, network := range networks { - if network.ExternalName != "" { + if network.External.Name != "" { continue } @@ -165,6 +174,80 @@ func convertNetworks( return nets } +func convertVolumes( + serviceVolumes []string, + stackVolumes map[string]composetypes.VolumeConfig, + namespace string, +) ([]mount.Mount, error) { + var mounts []mount.Mount + + for _, volumeString := range serviceVolumes { + var ( + source, target string + mountType mount.Type + readOnly bool + volumeOptions *mount.VolumeOptions + ) + + // TODO: split Windows path mappings properly + parts := strings.SplitN(volumeString, ":", 3) + + if len(parts) == 3 { + source = parts[0] + target = parts[1] + if parts[2] == "ro" { + readOnly = true + } + } else if len(parts) == 2 { + source = parts[0] + target = parts[1] + } else if len(parts) == 1 { + target = parts[0] + } + + // TODO: catch Windows paths here + if strings.HasPrefix(source, "/") { + mountType = mount.TypeBind + } else { + mountType = mount.TypeVolume + + stackVolume, exists := stackVolumes[source] + if !exists { + // TODO: better error message (include service name) + return nil, fmt.Errorf("Undefined volume: %s", source) + } + + if stackVolume.External.Name != "" { + source = stackVolume.External.Name + } else { + volumeOptions = &mount.VolumeOptions{ + Labels: stackVolume.Labels, + } + + if stackVolume.Driver != "" { + volumeOptions.DriverConfig = &mount.Driver{ + Name: stackVolume.Driver, + Options: stackVolume.DriverOpts, + } + } + + // TODO: remove this duplication + source = fmt.Sprintf("%s_%s", namespace, source) + } + } + + mounts = append(mounts, mount.Mount{ + Type: mountType, + Source: source, + Target: target, + ReadOnly: readOnly, + VolumeOptions: volumeOptions, + }) + } + + return mounts, nil +} + func deployServices( ctx context.Context, dockerCli *command.DockerCli, @@ -255,6 +338,11 @@ func convertService( return swarm.ServiceSpec{}, err } + mounts, err := convertVolumes(service.Volumes, volumes, namespace) + if err != nil { + return swarm.ServiceSpec{}, err + } + serviceSpec := swarm.ServiceSpec{ Annotations: swarm.Annotations{ Name: name, @@ -262,13 +350,15 @@ func convertService( }, TaskTemplate: swarm.TaskSpec{ ContainerSpec: swarm.ContainerSpec{ - Image: service.Image, - Command: service.Entrypoint, - Args: service.Command, - Env: convertEnvironment(service.Environment), - Labels: getStackLabels(namespace, service.Deploy.Labels), - Dir: service.WorkingDir, - User: service.User, + Image: service.Image, + Command: service.Entrypoint, + Args: service.Command, + Hostname: service.Hostname, + Env: convertEnvironment(service.Environment), + Labels: getStackLabels(namespace, service.Deploy.Labels), + Dir: service.WorkingDir, + User: service.User, + Mounts: mounts, }, Placement: &swarm.Placement{ Constraints: service.Deploy.Placement.Constraints, From aed389c1e8329e27a1c94ce9c57bdc0f9b12f90c Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 28 Oct 2016 17:30:20 -0400 Subject: [PATCH 339/978] Add swarmkit fields to stack service. Signed-off-by: Daniel Nephin Upstream-commit: e1b96b6447d3c64fce5d3b92508aac7bb504f3ea Component: cli --- components/cli/command/stack/deploy.go | 118 ++++++++++++++++++++----- 1 file changed, 95 insertions(+), 23 deletions(-) diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index 96bd175450..e72abcc8cc 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -5,7 +5,6 @@ import ( "io/ioutil" "os" "strings" - "time" "github.com/spf13/cobra" "golang.org/x/net/context" @@ -19,6 +18,8 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" servicecmd "github.com/docker/docker/cli/command/service" + runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/docker/docker/opts" "github.com/docker/go-connections/nat" ) @@ -343,23 +344,37 @@ func convertService( return swarm.ServiceSpec{}, err } + resources, err := convertResources(service.Deploy.Resources) + if err != nil { + return swarm.ServiceSpec{}, err + } + + restartPolicy, err := convertRestartPolicy( + service.Restart, service.Deploy.RestartPolicy) + if err != nil { + return swarm.ServiceSpec{}, err + } + serviceSpec := swarm.ServiceSpec{ Annotations: swarm.Annotations{ Name: name, - Labels: getStackLabels(namespace, service.Labels), + Labels: getStackLabels(namespace, service.Deploy.Labels), }, TaskTemplate: swarm.TaskSpec{ ContainerSpec: swarm.ContainerSpec{ - Image: service.Image, - Command: service.Entrypoint, - Args: service.Command, - Hostname: service.Hostname, - Env: convertEnvironment(service.Environment), - Labels: getStackLabels(namespace, service.Deploy.Labels), - Dir: service.WorkingDir, - User: service.User, - Mounts: mounts, + Image: service.Image, + Command: service.Entrypoint, + Args: service.Command, + Hostname: service.Hostname, + Env: convertEnvironment(service.Environment), + Labels: getStackLabels(namespace, service.Labels), + Dir: service.WorkingDir, + User: service.User, + Mounts: mounts, + StopGracePeriod: service.StopGracePeriod, }, + Resources: resources, + RestartPolicy: restartPolicy, Placement: &swarm.Placement{ Constraints: service.Deploy.Placement.Constraints, }, @@ -367,20 +382,77 @@ func convertService( EndpointSpec: endpoint, Mode: mode, Networks: convertNetworks(service.Networks, namespace, service.Name), + UpdateConfig: convertUpdateConfig(service.Deploy.UpdateConfig), } - if service.StopGracePeriod != nil { - stopGrace, err := time.ParseDuration(*service.StopGracePeriod) - if err != nil { - return swarm.ServiceSpec{}, err - } - serviceSpec.TaskTemplate.ContainerSpec.StopGracePeriod = &stopGrace - } - - // TODO: convert mounts return serviceSpec, nil } +func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (*swarm.RestartPolicy, error) { + // TODO: log if restart is being ignored + if source == nil { + policy, err := runconfigopts.ParseRestartPolicy(restart) + if err != nil { + return nil, err + } + // TODO: is this an accurate convertion? + switch { + case policy.IsNone(), policy.IsAlways(), policy.IsUnlessStopped(): + return nil, nil + case policy.IsOnFailure(): + attempts := uint64(policy.MaximumRetryCount) + return &swarm.RestartPolicy{ + Condition: swarm.RestartPolicyConditionOnFailure, + MaxAttempts: &attempts, + }, nil + } + } + return &swarm.RestartPolicy{ + Condition: swarm.RestartPolicyCondition(source.Condition), + Delay: source.Delay, + MaxAttempts: source.MaxAttempts, + Window: source.Window, + }, nil +} + +func convertUpdateConfig(source *composetypes.UpdateConfig) *swarm.UpdateConfig { + if source == nil { + return nil + } + return &swarm.UpdateConfig{ + Parallelism: source.Parallelism, + Delay: source.Delay, + FailureAction: source.FailureAction, + Monitor: source.Monitor, + MaxFailureRatio: source.MaxFailureRatio, + } +} + +func convertResources(source composetypes.Resources) (*swarm.ResourceRequirements, error) { + resources := &swarm.ResourceRequirements{} + if source.Limits != nil { + cpus, err := opts.ParseCPUs(source.Limits.NanoCPUs) + if err != nil { + return nil, err + } + resources.Limits = &swarm.Resources{ + NanoCPUs: cpus, + MemoryBytes: int64(source.Limits.MemoryBytes), + } + } + if source.Reservations != nil { + cpus, err := opts.ParseCPUs(source.Reservations.NanoCPUs) + if err != nil { + return nil, err + } + resources.Reservations = &swarm.Resources{ + NanoCPUs: cpus, + MemoryBytes: int64(source.Reservations.MemoryBytes), + } + } + return resources, nil +} + func convertEndpointSpec(source []string) (*swarm.EndpointSpec, error) { portConfigs := []swarm.PortConfig{} ports, portBindings, err := nat.ParsePortSpecs(source) @@ -407,17 +479,17 @@ func convertEnvironment(source map[string]string) []string { return output } -func convertDeployMode(mode string, replicas uint64) (swarm.ServiceMode, error) { +func convertDeployMode(mode string, replicas *uint64) (swarm.ServiceMode, error) { serviceMode := swarm.ServiceMode{} switch mode { case "global": - if replicas != 0 { + if replicas != nil { return serviceMode, fmt.Errorf("replicas can only be used with replicated mode") } serviceMode.Global = &swarm.GlobalService{} case "replicated": - serviceMode.Replicated = &swarm.ReplicatedService{Replicas: &replicas} + serviceMode.Replicated = &swarm.ReplicatedService{Replicas: replicas} default: return serviceMode, fmt.Errorf("Unknown mode: %s", mode) } From 7a88ddd32e48d1eeec196b5981b6c42be35bc055 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 31 Oct 2016 12:43:47 -0700 Subject: [PATCH 340/978] Handle unsupported, deprecated and forbidden properties Signed-off-by: Aanand Prasad Upstream-commit: dfab8f2bd42e6ca94fb3ad06b800402abd927198 Component: cli --- components/cli/command/stack/deploy.go | 30 +++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index e72abcc8cc..6a15609231 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -4,6 +4,7 @@ import ( "fmt" "io/ioutil" "os" + "sort" "strings" "github.com/spf13/cobra" @@ -62,9 +63,26 @@ func runDeploy(dockerCli *command.DockerCli, opts deployOptions) error { config, err := loader.Load(configDetails) if err != nil { + if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok { + return fmt.Errorf("Compose file contains unsupported options:\n\n%s\n", + propertyWarnings(fpe.Properties)) + } + return err } + unsupportedProperties := loader.GetUnsupportedProperties(configDetails) + if len(unsupportedProperties) > 0 { + fmt.Printf("Ignoring unsupported options: %s\n\n", + strings.Join(unsupportedProperties, ", ")) + } + + deprecatedProperties := loader.GetDeprecatedProperties(configDetails) + if len(deprecatedProperties) > 0 { + fmt.Printf("Ignoring deprecated options:\n\n%s\n\n", + propertyWarnings(deprecatedProperties)) + } + ctx := context.Background() if err := createNetworks(ctx, dockerCli, config.Networks, opts.namespace); err != nil { return err @@ -72,6 +90,15 @@ func runDeploy(dockerCli *command.DockerCli, opts deployOptions) error { return deployServices(ctx, dockerCli, config, opts.namespace, opts.sendRegistryAuth) } +func propertyWarnings(properties map[string]string) string { + var msgs []string + for name, description := range properties { + msgs = append(msgs, fmt.Sprintf("%s: %s", name, description)) + } + sort.Strings(msgs) + return strings.Join(msgs, "\n\n") +} + func getConfigDetails(opts deployOptions) (composetypes.ConfigDetails, error) { var details composetypes.ConfigDetails var err error @@ -407,10 +434,11 @@ func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (* }, nil } } + attempts := uint64(*source.MaxAttempts) return &swarm.RestartPolicy{ Condition: swarm.RestartPolicyCondition(source.Condition), Delay: source.Delay, - MaxAttempts: source.MaxAttempts, + MaxAttempts: &attempts, Window: source.Window, }, nil } From 002c2a50404f04ed6287b5f2c6a25a334a1a9216 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 2 Nov 2016 13:10:34 +0000 Subject: [PATCH 341/978] Default to replicated mode Signed-off-by: Aanand Prasad Upstream-commit: 25c93d4ebb906b29736b63dc5bf9aff53ff682b8 Component: cli --- components/cli/command/stack/deploy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index 6a15609231..83d55324de 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -516,7 +516,7 @@ func convertDeployMode(mode string, replicas *uint64) (swarm.ServiceMode, error) return serviceMode, fmt.Errorf("replicas can only be used with replicated mode") } serviceMode.Global = &swarm.GlobalService{} - case "replicated": + case "replicated", "": serviceMode.Replicated = &swarm.ReplicatedService{Replicas: replicas} default: return serviceMode, fmt.Errorf("Unknown mode: %s", mode) From 3474fbaadebf4a32256918cad4d42400e3364284 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 2 Nov 2016 12:19:37 -0400 Subject: [PATCH 342/978] Send warnings to stderr. Signed-off-by: Daniel Nephin Upstream-commit: ae8f00182973637c07c7bc852ee9c401b2c9ba91 Component: cli --- components/cli/command/stack/deploy.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index 83d55324de..b92662c3c5 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -73,13 +73,13 @@ func runDeploy(dockerCli *command.DockerCli, opts deployOptions) error { unsupportedProperties := loader.GetUnsupportedProperties(configDetails) if len(unsupportedProperties) > 0 { - fmt.Printf("Ignoring unsupported options: %s\n\n", + fmt.Fprintf(dockerCli.Err(), "Ignoring unsupported options: %s\n\n", strings.Join(unsupportedProperties, ", ")) } deprecatedProperties := loader.GetDeprecatedProperties(configDetails) if len(deprecatedProperties) > 0 { - fmt.Printf("Ignoring deprecated options:\n\n%s\n\n", + fmt.Fprintf(dockerCli.Err(), "Ignoring deprecated options:\n\n%s\n\n", propertyWarnings(deprecatedProperties)) } @@ -434,11 +434,10 @@ func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (* }, nil } } - attempts := uint64(*source.MaxAttempts) return &swarm.RestartPolicy{ Condition: swarm.RestartPolicyCondition(source.Condition), Delay: source.Delay, - MaxAttempts: &attempts, + MaxAttempts: source.MaxAttempts, Window: source.Window, }, nil } From 8692a2c8eb08ad4062782c894649836b4d60e00f Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 3 Nov 2016 17:40:48 -0600 Subject: [PATCH 343/978] Always use a default network if no other networks are set. also add network labels. Signed-off-by: Daniel Nephin Upstream-commit: d89cb4c62fa3c7422bf9a7f0b81f8e1dbbfc512a Component: cli --- components/cli/command/stack/deploy.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index b92662c3c5..bb3e73e6e1 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -150,6 +150,9 @@ func createNetworks( existingNetworkMap[network.Name] = network } + // TODO: only add default network if it's used + networks["default"] = composetypes.NetworkConfig{} + for internalName, network := range networks { if network.External.Name != "" { continue @@ -161,8 +164,7 @@ func createNetworks( } createOpts := types.NetworkCreate{ - // TODO: support network labels from compose file - Labels: getStackLabels(namespace, nil), + Labels: getStackLabels(namespace, network.Labels), Driver: network.Driver, Options: network.DriverOpts, } @@ -191,6 +193,16 @@ func convertNetworks( namespace string, name string, ) []swarm.NetworkAttachmentConfig { + if len(networks) == 0 { + return []swarm.NetworkAttachmentConfig{ + { + // TODO: only do this name mangling in one function + Target: namespace + "_" + "default", + Aliases: []string{name}, + }, + } + } + nets := []swarm.NetworkAttachmentConfig{} for networkName, network := range networks { nets = append(nets, swarm.NetworkAttachmentConfig{ From bab5319d52f40ad5dfe166ab02850ff28f0c675c Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 3 Nov 2016 17:50:03 -0600 Subject: [PATCH 344/978] Remove duplication of name mangling. Signed-off-by: Daniel Nephin Upstream-commit: ef845be6a52cdc93900885cffad48e62ccc4f385 Component: cli --- components/cli/command/stack/common.go | 8 +++++ components/cli/command/stack/deploy.go | 41 ++++++++++++-------------- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/components/cli/command/stack/common.go b/components/cli/command/stack/common.go index 4776ec1b42..b94c108667 100644 --- a/components/cli/command/stack/common.go +++ b/components/cli/command/stack/common.go @@ -46,3 +46,11 @@ func getNetworks( ctx, types.NetworkListOptions{Filters: getStackFilter(namespace)}) } + +type namespace struct { + name string +} + +func (n namespace) scope(name string) string { + return n.name + "_" + name +} diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index bb3e73e6e1..fccd89eb5e 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -84,10 +84,11 @@ func runDeploy(dockerCli *command.DockerCli, opts deployOptions) error { } ctx := context.Background() - if err := createNetworks(ctx, dockerCli, config.Networks, opts.namespace); err != nil { + namespace := namespace{name: opts.namespace} + if err := createNetworks(ctx, dockerCli, config.Networks, namespace); err != nil { return err } - return deployServices(ctx, dockerCli, config, opts.namespace, opts.sendRegistryAuth) + return deployServices(ctx, dockerCli, config, namespace, opts.sendRegistryAuth) } func propertyWarnings(properties map[string]string) string { @@ -136,11 +137,11 @@ func createNetworks( ctx context.Context, dockerCli *command.DockerCli, networks map[string]composetypes.NetworkConfig, - namespace string, + namespace namespace, ) error { client := dockerCli.Client() - existingNetworks, err := getNetworks(ctx, client, namespace) + existingNetworks, err := getNetworks(ctx, client, namespace.name) if err != nil { return err } @@ -158,13 +159,13 @@ func createNetworks( continue } - name := fmt.Sprintf("%s_%s", namespace, internalName) + name := namespace.scope(internalName) if _, exists := existingNetworkMap[name]; exists { continue } createOpts := types.NetworkCreate{ - Labels: getStackLabels(namespace, network.Labels), + Labels: getStackLabels(namespace.name, network.Labels), Driver: network.Driver, Options: network.DriverOpts, } @@ -190,14 +191,13 @@ func createNetworks( func convertNetworks( networks map[string]*composetypes.ServiceNetworkConfig, - namespace string, + namespace namespace, name string, ) []swarm.NetworkAttachmentConfig { if len(networks) == 0 { return []swarm.NetworkAttachmentConfig{ { - // TODO: only do this name mangling in one function - Target: namespace + "_" + "default", + Target: namespace.scope("default"), Aliases: []string{name}, }, } @@ -206,8 +206,7 @@ func convertNetworks( nets := []swarm.NetworkAttachmentConfig{} for networkName, network := range networks { nets = append(nets, swarm.NetworkAttachmentConfig{ - // TODO: only do this name mangling in one function - Target: namespace + "_" + networkName, + Target: namespace.scope(networkName), Aliases: append(network.Aliases, name), }) } @@ -217,7 +216,7 @@ func convertNetworks( func convertVolumes( serviceVolumes []string, stackVolumes map[string]composetypes.VolumeConfig, - namespace string, + namespace namespace, ) ([]mount.Mount, error) { var mounts []mount.Mount @@ -271,8 +270,7 @@ func convertVolumes( } } - // TODO: remove this duplication - source = fmt.Sprintf("%s_%s", namespace, source) + source = namespace.scope(source) } } @@ -292,7 +290,7 @@ func deployServices( ctx context.Context, dockerCli *command.DockerCli, config *composetypes.Config, - namespace string, + namespace namespace, sendAuth bool, ) error { apiClient := dockerCli.Client() @@ -300,7 +298,7 @@ func deployServices( services := config.Services volumes := config.Volumes - existingServices, err := getServices(ctx, apiClient, namespace) + existingServices, err := getServices(ctx, apiClient, namespace.name) if err != nil { return err } @@ -311,7 +309,7 @@ func deployServices( } for _, service := range services { - name := fmt.Sprintf("%s_%s", namespace, service.Name) + name := namespace.scope(service.Name) serviceSpec, err := convertService(namespace, service, volumes) if err != nil { @@ -361,12 +359,11 @@ func deployServices( } func convertService( - namespace string, + namespace namespace, service composetypes.ServiceConfig, volumes map[string]composetypes.VolumeConfig, ) (swarm.ServiceSpec, error) { - // TODO: remove this duplication - name := fmt.Sprintf("%s_%s", namespace, service.Name) + name := namespace.scope(service.Name) endpoint, err := convertEndpointSpec(service.Ports) if err != nil { @@ -397,7 +394,7 @@ func convertService( serviceSpec := swarm.ServiceSpec{ Annotations: swarm.Annotations{ Name: name, - Labels: getStackLabels(namespace, service.Deploy.Labels), + Labels: getStackLabels(namespace.name, service.Deploy.Labels), }, TaskTemplate: swarm.TaskSpec{ ContainerSpec: swarm.ContainerSpec{ @@ -406,7 +403,7 @@ func convertService( Args: service.Command, Hostname: service.Hostname, Env: convertEnvironment(service.Environment), - Labels: getStackLabels(namespace, service.Labels), + Labels: getStackLabels(namespace.name, service.Labels), Dir: service.WorkingDir, User: service.User, Mounts: mounts, From 681106a47f6be4f137b3b80273a2c6c7d185051c Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 4 Nov 2016 13:59:14 -0600 Subject: [PATCH 345/978] Remove bundlefile Signed-off-by: Daniel Nephin Upstream-commit: 3875355a3e593fac42bea3f88025c76acd3c18dc Component: cli --- .../cli/command/bundlefile/bundlefile.go | 69 ----------------- .../cli/command/bundlefile/bundlefile_test.go | 77 ------------------- components/cli/command/stack/opts.go | 39 +--------- 3 files changed, 1 insertion(+), 184 deletions(-) delete mode 100644 components/cli/command/bundlefile/bundlefile.go delete mode 100644 components/cli/command/bundlefile/bundlefile_test.go diff --git a/components/cli/command/bundlefile/bundlefile.go b/components/cli/command/bundlefile/bundlefile.go deleted file mode 100644 index 7fd1e4f6c4..0000000000 --- a/components/cli/command/bundlefile/bundlefile.go +++ /dev/null @@ -1,69 +0,0 @@ -package bundlefile - -import ( - "encoding/json" - "fmt" - "io" -) - -// Bundlefile stores the contents of a bundlefile -type Bundlefile struct { - Version string - Services map[string]Service -} - -// Service is a service from a bundlefile -type Service struct { - Image string - Command []string `json:",omitempty"` - Args []string `json:",omitempty"` - Env []string `json:",omitempty"` - Labels map[string]string `json:",omitempty"` - Ports []Port `json:",omitempty"` - WorkingDir *string `json:",omitempty"` - User *string `json:",omitempty"` - Networks []string `json:",omitempty"` -} - -// Port is a port as defined in a bundlefile -type Port struct { - Protocol string - Port uint32 -} - -// LoadFile loads a bundlefile from a path to the file -func LoadFile(reader io.Reader) (*Bundlefile, error) { - bundlefile := &Bundlefile{} - - decoder := json.NewDecoder(reader) - if err := decoder.Decode(bundlefile); err != nil { - switch jsonErr := err.(type) { - case *json.SyntaxError: - return nil, fmt.Errorf( - "JSON syntax error at byte %v: %s", - jsonErr.Offset, - jsonErr.Error()) - case *json.UnmarshalTypeError: - return nil, fmt.Errorf( - "Unexpected type at byte %v. Expected %s but received %s.", - jsonErr.Offset, - jsonErr.Type, - jsonErr.Value) - } - return nil, err - } - - return bundlefile, nil -} - -// Print writes the contents of the bundlefile to the output writer -// as human readable json -func Print(out io.Writer, bundle *Bundlefile) error { - bytes, err := json.MarshalIndent(*bundle, "", " ") - if err != nil { - return err - } - - _, err = out.Write(bytes) - return err -} diff --git a/components/cli/command/bundlefile/bundlefile_test.go b/components/cli/command/bundlefile/bundlefile_test.go deleted file mode 100644 index c343410df3..0000000000 --- a/components/cli/command/bundlefile/bundlefile_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package bundlefile - -import ( - "bytes" - "strings" - "testing" - - "github.com/docker/docker/pkg/testutil/assert" -) - -func TestLoadFileV01Success(t *testing.T) { - reader := strings.NewReader(`{ - "Version": "0.1", - "Services": { - "redis": { - "Image": "redis@sha256:4b24131101fa0117bcaa18ac37055fffd9176aa1a240392bb8ea85e0be50f2ce", - "Networks": ["default"] - }, - "web": { - "Image": "dockercloud/hello-world@sha256:fe79a2cfbd17eefc344fb8419420808df95a1e22d93b7f621a7399fd1e9dca1d", - "Networks": ["default"], - "User": "web" - } - } - }`) - - bundle, err := LoadFile(reader) - assert.NilError(t, err) - assert.Equal(t, bundle.Version, "0.1") - assert.Equal(t, len(bundle.Services), 2) -} - -func TestLoadFileSyntaxError(t *testing.T) { - reader := strings.NewReader(`{ - "Version": "0.1", - "Services": unquoted string - }`) - - _, err := LoadFile(reader) - assert.Error(t, err, "syntax error at byte 37: invalid character 'u'") -} - -func TestLoadFileTypeError(t *testing.T) { - reader := strings.NewReader(`{ - "Version": "0.1", - "Services": { - "web": { - "Image": "redis", - "Networks": "none" - } - } - }`) - - _, err := LoadFile(reader) - assert.Error(t, err, "Unexpected type at byte 94. Expected []string but received string") -} - -func TestPrint(t *testing.T) { - var buffer bytes.Buffer - bundle := &Bundlefile{ - Version: "0.1", - Services: map[string]Service{ - "web": { - Image: "image", - Command: []string{"echo", "something"}, - }, - }, - } - assert.NilError(t, Print(&buffer, bundle)) - output := buffer.String() - assert.Contains(t, output, "\"Image\": \"image\"") - assert.Contains(t, output, - `"Command": [ - "echo", - "something" - ]`) -} diff --git a/components/cli/command/stack/opts.go b/components/cli/command/stack/opts.go index c2cc0d1e70..a33e7707e8 100644 --- a/components/cli/command/stack/opts.go +++ b/components/cli/command/stack/opts.go @@ -1,48 +1,11 @@ package stack -import ( - "fmt" - "io" - "os" - - "github.com/docker/docker/cli/command/bundlefile" - "github.com/spf13/pflag" -) +import "github.com/spf13/pflag" func addComposefileFlag(opt *string, flags *pflag.FlagSet) { flags.StringVar(opt, "compose-file", "", "Path to a Compose file") } -func addBundlefileFlag(opt *string, flags *pflag.FlagSet) { - flags.StringVar(opt, "bundle-file", "", "Path to a Distributed Application Bundle file") -} - func addRegistryAuthFlag(opt *bool, flags *pflag.FlagSet) { flags.BoolVar(opt, "with-registry-auth", false, "Send registry authentication details to Swarm agents") } - -func loadBundlefile(stderr io.Writer, namespace string, path string) (*bundlefile.Bundlefile, error) { - defaultPath := fmt.Sprintf("%s.dab", namespace) - - if path == "" { - path = defaultPath - } - if _, err := os.Stat(path); err != nil { - return nil, fmt.Errorf( - "Bundle %s not found. Specify the path with --file", - path) - } - - fmt.Fprintf(stderr, "Loading bundle from %s\n", path) - reader, err := os.Open(path) - if err != nil { - return nil, err - } - defer reader.Close() - - bundle, err := bundlefile.LoadFile(reader) - if err != nil { - return nil, fmt.Errorf("Error reading %s: %v\n", path, err) - } - return bundle, err -} From ae6497c7b8eccfc25ee7b5544b849ccacda65570 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 4 Nov 2016 14:55:24 -0600 Subject: [PATCH 346/978] Add integration test for stack deploy. Signed-off-by: Daniel Nephin Upstream-commit: d05510d954ea006ac985ce25d44e2dfcdf502c0c Component: cli --- components/cli/command/stack/deploy.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index fccd89eb5e..6201c2bd2e 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -19,8 +19,8 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" servicecmd "github.com/docker/docker/cli/command/service" - runconfigopts "github.com/docker/docker/runconfig/opts" "github.com/docker/docker/opts" + runconfigopts "github.com/docker/docker/runconfig/opts" "github.com/docker/go-connections/nat" ) @@ -85,7 +85,12 @@ func runDeploy(dockerCli *command.DockerCli, opts deployOptions) error { ctx := context.Background() namespace := namespace{name: opts.namespace} - if err := createNetworks(ctx, dockerCli, config.Networks, namespace); err != nil { + + networks := config.Networks + if networks == nil { + networks = make(map[string]composetypes.NetworkConfig) + } + if err := createNetworks(ctx, dockerCli, networks, namespace); err != nil { return err } return deployServices(ctx, dockerCli, config, namespace, opts.sendRegistryAuth) From 4821df0ab0c15f258727106d527342404ad42792 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 8 Nov 2016 17:05:23 +0000 Subject: [PATCH 347/978] Reinstate --bundle-file argument to 'docker deploy' Signed-off-by: Aanand Prasad Upstream-commit: 791b68784858c74def4a8648a962d35d80d88d9c Component: cli --- .../cli/command/bundlefile/bundlefile.go | 69 ++++++ .../cli/command/bundlefile/bundlefile_test.go | 77 +++++++ components/cli/command/stack/deploy.go | 205 ++++++++++++++---- components/cli/command/stack/opts.go | 39 +++- 4 files changed, 351 insertions(+), 39 deletions(-) create mode 100644 components/cli/command/bundlefile/bundlefile.go create mode 100644 components/cli/command/bundlefile/bundlefile_test.go diff --git a/components/cli/command/bundlefile/bundlefile.go b/components/cli/command/bundlefile/bundlefile.go new file mode 100644 index 0000000000..7fd1e4f6c4 --- /dev/null +++ b/components/cli/command/bundlefile/bundlefile.go @@ -0,0 +1,69 @@ +package bundlefile + +import ( + "encoding/json" + "fmt" + "io" +) + +// Bundlefile stores the contents of a bundlefile +type Bundlefile struct { + Version string + Services map[string]Service +} + +// Service is a service from a bundlefile +type Service struct { + Image string + Command []string `json:",omitempty"` + Args []string `json:",omitempty"` + Env []string `json:",omitempty"` + Labels map[string]string `json:",omitempty"` + Ports []Port `json:",omitempty"` + WorkingDir *string `json:",omitempty"` + User *string `json:",omitempty"` + Networks []string `json:",omitempty"` +} + +// Port is a port as defined in a bundlefile +type Port struct { + Protocol string + Port uint32 +} + +// LoadFile loads a bundlefile from a path to the file +func LoadFile(reader io.Reader) (*Bundlefile, error) { + bundlefile := &Bundlefile{} + + decoder := json.NewDecoder(reader) + if err := decoder.Decode(bundlefile); err != nil { + switch jsonErr := err.(type) { + case *json.SyntaxError: + return nil, fmt.Errorf( + "JSON syntax error at byte %v: %s", + jsonErr.Offset, + jsonErr.Error()) + case *json.UnmarshalTypeError: + return nil, fmt.Errorf( + "Unexpected type at byte %v. Expected %s but received %s.", + jsonErr.Offset, + jsonErr.Type, + jsonErr.Value) + } + return nil, err + } + + return bundlefile, nil +} + +// Print writes the contents of the bundlefile to the output writer +// as human readable json +func Print(out io.Writer, bundle *Bundlefile) error { + bytes, err := json.MarshalIndent(*bundle, "", " ") + if err != nil { + return err + } + + _, err = out.Write(bytes) + return err +} diff --git a/components/cli/command/bundlefile/bundlefile_test.go b/components/cli/command/bundlefile/bundlefile_test.go new file mode 100644 index 0000000000..c343410df3 --- /dev/null +++ b/components/cli/command/bundlefile/bundlefile_test.go @@ -0,0 +1,77 @@ +package bundlefile + +import ( + "bytes" + "strings" + "testing" + + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestLoadFileV01Success(t *testing.T) { + reader := strings.NewReader(`{ + "Version": "0.1", + "Services": { + "redis": { + "Image": "redis@sha256:4b24131101fa0117bcaa18ac37055fffd9176aa1a240392bb8ea85e0be50f2ce", + "Networks": ["default"] + }, + "web": { + "Image": "dockercloud/hello-world@sha256:fe79a2cfbd17eefc344fb8419420808df95a1e22d93b7f621a7399fd1e9dca1d", + "Networks": ["default"], + "User": "web" + } + } + }`) + + bundle, err := LoadFile(reader) + assert.NilError(t, err) + assert.Equal(t, bundle.Version, "0.1") + assert.Equal(t, len(bundle.Services), 2) +} + +func TestLoadFileSyntaxError(t *testing.T) { + reader := strings.NewReader(`{ + "Version": "0.1", + "Services": unquoted string + }`) + + _, err := LoadFile(reader) + assert.Error(t, err, "syntax error at byte 37: invalid character 'u'") +} + +func TestLoadFileTypeError(t *testing.T) { + reader := strings.NewReader(`{ + "Version": "0.1", + "Services": { + "web": { + "Image": "redis", + "Networks": "none" + } + } + }`) + + _, err := LoadFile(reader) + assert.Error(t, err, "Unexpected type at byte 94. Expected []string but received string") +} + +func TestPrint(t *testing.T) { + var buffer bytes.Buffer + bundle := &Bundlefile{ + Version: "0.1", + Services: map[string]Service{ + "web": { + Image: "image", + Command: []string{"echo", "something"}, + }, + }, + } + assert.NilError(t, Print(&buffer, bundle)) + output := buffer.String() + assert.Contains(t, output, "\"Image\": \"image\"") + assert.Contains(t, output, + `"Command": [ + "echo", + "something" + ]`) +} diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index 6201c2bd2e..895442a04d 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -29,6 +29,7 @@ const ( ) type deployOptions struct { + bundlefile string composefile string namespace string sendRegistryAuth bool @@ -50,12 +51,108 @@ func newDeployCommand(dockerCli *command.DockerCli) *cobra.Command { } flags := cmd.Flags() + addBundlefileFlag(&opts.bundlefile, flags) addComposefileFlag(&opts.composefile, flags) addRegistryAuthFlag(&opts.sendRegistryAuth, flags) return cmd } func runDeploy(dockerCli *command.DockerCli, opts deployOptions) error { + if opts.bundlefile == "" && opts.composefile == "" { + return fmt.Errorf("Please specify either a bundle file (with --bundle-file) or a Compose file (with --compose-file).") + } + + if opts.bundlefile != "" && opts.composefile != "" { + return fmt.Errorf("You cannot specify both a bundle file and a Compose file.") + } + + info, err := dockerCli.Client().Info(context.Background()) + if err != nil { + return err + } + if !info.Swarm.ControlAvailable { + return fmt.Errorf("This node is not a swarm manager. Use \"docker swarm init\" or \"docker swarm join\" to connect this node to swarm and try again.") + } + + if opts.bundlefile != "" { + return deployBundle(dockerCli, opts) + } else { + return deployCompose(dockerCli, opts) + } +} + +func deployBundle(dockerCli *command.DockerCli, opts deployOptions) error { + bundle, err := loadBundlefile(dockerCli.Err(), opts.namespace, opts.bundlefile) + if err != nil { + return err + } + + namespace := namespace{name: opts.namespace} + + networks := make(map[string]types.NetworkCreate) + for _, service := range bundle.Services { + for _, networkName := range service.Networks { + networks[networkName] = types.NetworkCreate{ + Labels: getStackLabels(namespace.name, nil), + } + } + } + + services := make(map[string]swarm.ServiceSpec) + for internalName, service := range bundle.Services { + name := namespace.scope(internalName) + + var ports []swarm.PortConfig + for _, portSpec := range service.Ports { + ports = append(ports, swarm.PortConfig{ + Protocol: swarm.PortConfigProtocol(portSpec.Protocol), + TargetPort: portSpec.Port, + }) + } + + nets := []swarm.NetworkAttachmentConfig{} + for _, networkName := range service.Networks { + nets = append(nets, swarm.NetworkAttachmentConfig{ + Target: namespace.scope(networkName), + Aliases: []string{networkName}, + }) + } + + serviceSpec := swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: name, + Labels: getStackLabels(namespace.name, service.Labels), + }, + TaskTemplate: swarm.TaskSpec{ + ContainerSpec: swarm.ContainerSpec{ + Image: service.Image, + Command: service.Command, + Args: service.Args, + Env: service.Env, + // Service Labels will not be copied to Containers + // automatically during the deployment so we apply + // it here. + Labels: getStackLabels(namespace.name, nil), + }, + }, + EndpointSpec: &swarm.EndpointSpec{ + Ports: ports, + }, + Networks: nets, + } + + services[internalName] = serviceSpec + } + + ctx := context.Background() + + if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil { + return err + } + return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth) +} + +func deployCompose(dockerCli *command.DockerCli, opts deployOptions) error { configDetails, err := getConfigDetails(opts) if err != nil { return err @@ -86,14 +183,15 @@ func runDeploy(dockerCli *command.DockerCli, opts deployOptions) error { ctx := context.Background() namespace := namespace{name: opts.namespace} - networks := config.Networks - if networks == nil { - networks = make(map[string]composetypes.NetworkConfig) - } - if err := createNetworks(ctx, dockerCli, networks, namespace); err != nil { + networks := convertNetworks(namespace, config.Networks) + if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil { return err } - return deployServices(ctx, dockerCli, config, namespace, opts.sendRegistryAuth) + services, err := convertServices(namespace, config) + if err != nil { + return err + } + return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth) } func propertyWarnings(properties map[string]string) string { @@ -138,37 +236,24 @@ func getConfigFile(filename string) (*composetypes.ConfigFile, error) { }, nil } -func createNetworks( - ctx context.Context, - dockerCli *command.DockerCli, - networks map[string]composetypes.NetworkConfig, +func convertNetworks( namespace namespace, -) error { - client := dockerCli.Client() - - existingNetworks, err := getNetworks(ctx, client, namespace.name) - if err != nil { - return err - } - - existingNetworkMap := make(map[string]types.NetworkResource) - for _, network := range existingNetworks { - existingNetworkMap[network.Name] = network + networks map[string]composetypes.NetworkConfig, +) map[string]types.NetworkCreate { + if networks == nil { + networks = make(map[string]composetypes.NetworkConfig) } // TODO: only add default network if it's used networks["default"] = composetypes.NetworkConfig{} + result := make(map[string]types.NetworkCreate) + for internalName, network := range networks { if network.External.Name != "" { continue } - name := namespace.scope(internalName) - if _, exists := existingNetworkMap[name]; exists { - continue - } - createOpts := types.NetworkCreate{ Labels: getStackLabels(namespace.name, network.Labels), Driver: network.Driver, @@ -182,6 +267,36 @@ func createNetworks( } // TODO: IPAMConfig.Config + result[internalName] = createOpts + } + + return result +} + +func createNetworks( + ctx context.Context, + dockerCli *command.DockerCli, + namespace namespace, + networks map[string]types.NetworkCreate, +) error { + client := dockerCli.Client() + + existingNetworks, err := getNetworks(ctx, client, namespace.name) + if err != nil { + return err + } + + existingNetworkMap := make(map[string]types.NetworkResource) + for _, network := range existingNetworks { + existingNetworkMap[network.Name] = network + } + + for internalName, createOpts := range networks { + name := namespace.scope(internalName) + if _, exists := existingNetworkMap[name]; exists { + continue + } + if createOpts.Driver == "" { createOpts.Driver = defaultNetworkDriver } @@ -191,10 +306,11 @@ func createNetworks( return err } } + return nil } -func convertNetworks( +func convertServiceNetworks( networks map[string]*composetypes.ServiceNetworkConfig, namespace namespace, name string, @@ -294,14 +410,12 @@ func convertVolumes( func deployServices( ctx context.Context, dockerCli *command.DockerCli, - config *composetypes.Config, + services map[string]swarm.ServiceSpec, namespace namespace, sendAuth bool, ) error { apiClient := dockerCli.Client() out := dockerCli.Out() - services := config.Services - volumes := config.Volumes existingServices, err := getServices(ctx, apiClient, namespace.name) if err != nil { @@ -313,13 +427,8 @@ func deployServices( existingServiceMap[service.Spec.Name] = service } - for _, service := range services { - name := namespace.scope(service.Name) - - serviceSpec, err := convertService(namespace, service, volumes) - if err != nil { - return err - } + for internalName, serviceSpec := range services { + name := namespace.scope(internalName) encodedAuth := "" if sendAuth { @@ -363,6 +472,26 @@ func deployServices( return nil } +func convertServices( + namespace namespace, + config *composetypes.Config, +) (map[string]swarm.ServiceSpec, error) { + result := make(map[string]swarm.ServiceSpec) + + services := config.Services + volumes := config.Volumes + + for _, service := range services { + serviceSpec, err := convertService(namespace, service, volumes) + if err != nil { + return nil, err + } + result[service.Name] = serviceSpec + } + + return result, nil +} + func convertService( namespace namespace, service composetypes.ServiceConfig, @@ -422,7 +551,7 @@ func convertService( }, EndpointSpec: endpoint, Mode: mode, - Networks: convertNetworks(service.Networks, namespace, service.Name), + Networks: convertServiceNetworks(service.Networks, namespace, service.Name), UpdateConfig: convertUpdateConfig(service.Deploy.UpdateConfig), } diff --git a/components/cli/command/stack/opts.go b/components/cli/command/stack/opts.go index a33e7707e8..c2cc0d1e70 100644 --- a/components/cli/command/stack/opts.go +++ b/components/cli/command/stack/opts.go @@ -1,11 +1,48 @@ package stack -import "github.com/spf13/pflag" +import ( + "fmt" + "io" + "os" + + "github.com/docker/docker/cli/command/bundlefile" + "github.com/spf13/pflag" +) func addComposefileFlag(opt *string, flags *pflag.FlagSet) { flags.StringVar(opt, "compose-file", "", "Path to a Compose file") } +func addBundlefileFlag(opt *string, flags *pflag.FlagSet) { + flags.StringVar(opt, "bundle-file", "", "Path to a Distributed Application Bundle file") +} + func addRegistryAuthFlag(opt *bool, flags *pflag.FlagSet) { flags.BoolVar(opt, "with-registry-auth", false, "Send registry authentication details to Swarm agents") } + +func loadBundlefile(stderr io.Writer, namespace string, path string) (*bundlefile.Bundlefile, error) { + defaultPath := fmt.Sprintf("%s.dab", namespace) + + if path == "" { + path = defaultPath + } + if _, err := os.Stat(path); err != nil { + return nil, fmt.Errorf( + "Bundle %s not found. Specify the path with --file", + path) + } + + fmt.Fprintf(stderr, "Loading bundle from %s\n", path) + reader, err := os.Open(path) + if err != nil { + return nil, err + } + defer reader.Close() + + bundle, err := bundlefile.LoadFile(reader) + if err != nil { + return nil, fmt.Errorf("Error reading %s: %v\n", path, err) + } + return bundle, err +} From fe7cf88721734c4c98e7226cafedb565506a8951 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 8 Nov 2016 15:20:16 -0500 Subject: [PATCH 348/978] Restore stack deploy integration test with dab Signed-off-by: Daniel Nephin Upstream-commit: 458ffcd2e66871e35b7ff0dc6fc5dad07cb3a2cf Component: cli --- components/cli/command/stack/deploy.go | 92 +------------------ .../cli/command/stack/deploy_bundlefile.go | 80 ++++++++++++++++ 2 files changed, 85 insertions(+), 87 deletions(-) create mode 100644 components/cli/command/stack/deploy_bundlefile.go diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index 895442a04d..6a633c9a8f 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -58,100 +58,18 @@ func newDeployCommand(dockerCli *command.DockerCli) *cobra.Command { } func runDeploy(dockerCli *command.DockerCli, opts deployOptions) error { - if opts.bundlefile == "" && opts.composefile == "" { + switch { + case opts.bundlefile == "" && opts.composefile == "": return fmt.Errorf("Please specify either a bundle file (with --bundle-file) or a Compose file (with --compose-file).") - } - - if opts.bundlefile != "" && opts.composefile != "" { + case opts.bundlefile != "" && opts.composefile != "": return fmt.Errorf("You cannot specify both a bundle file and a Compose file.") - } - - info, err := dockerCli.Client().Info(context.Background()) - if err != nil { - return err - } - if !info.Swarm.ControlAvailable { - return fmt.Errorf("This node is not a swarm manager. Use \"docker swarm init\" or \"docker swarm join\" to connect this node to swarm and try again.") - } - - if opts.bundlefile != "" { + case opts.bundlefile != "": return deployBundle(dockerCli, opts) - } else { + default: return deployCompose(dockerCli, opts) } } -func deployBundle(dockerCli *command.DockerCli, opts deployOptions) error { - bundle, err := loadBundlefile(dockerCli.Err(), opts.namespace, opts.bundlefile) - if err != nil { - return err - } - - namespace := namespace{name: opts.namespace} - - networks := make(map[string]types.NetworkCreate) - for _, service := range bundle.Services { - for _, networkName := range service.Networks { - networks[networkName] = types.NetworkCreate{ - Labels: getStackLabels(namespace.name, nil), - } - } - } - - services := make(map[string]swarm.ServiceSpec) - for internalName, service := range bundle.Services { - name := namespace.scope(internalName) - - var ports []swarm.PortConfig - for _, portSpec := range service.Ports { - ports = append(ports, swarm.PortConfig{ - Protocol: swarm.PortConfigProtocol(portSpec.Protocol), - TargetPort: portSpec.Port, - }) - } - - nets := []swarm.NetworkAttachmentConfig{} - for _, networkName := range service.Networks { - nets = append(nets, swarm.NetworkAttachmentConfig{ - Target: namespace.scope(networkName), - Aliases: []string{networkName}, - }) - } - - serviceSpec := swarm.ServiceSpec{ - Annotations: swarm.Annotations{ - Name: name, - Labels: getStackLabels(namespace.name, service.Labels), - }, - TaskTemplate: swarm.TaskSpec{ - ContainerSpec: swarm.ContainerSpec{ - Image: service.Image, - Command: service.Command, - Args: service.Args, - Env: service.Env, - // Service Labels will not be copied to Containers - // automatically during the deployment so we apply - // it here. - Labels: getStackLabels(namespace.name, nil), - }, - }, - EndpointSpec: &swarm.EndpointSpec{ - Ports: ports, - }, - Networks: nets, - } - - services[internalName] = serviceSpec - } - - ctx := context.Background() - - if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil { - return err - } - return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth) -} - func deployCompose(dockerCli *command.DockerCli, opts deployOptions) error { configDetails, err := getConfigDetails(opts) if err != nil { diff --git a/components/cli/command/stack/deploy_bundlefile.go b/components/cli/command/stack/deploy_bundlefile.go new file mode 100644 index 0000000000..5ec8a2a05b --- /dev/null +++ b/components/cli/command/stack/deploy_bundlefile.go @@ -0,0 +1,80 @@ +package stack + +import ( + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/command" +) + +func deployBundle(dockerCli *command.DockerCli, opts deployOptions) error { + bundle, err := loadBundlefile(dockerCli.Err(), opts.namespace, opts.bundlefile) + if err != nil { + return err + } + + namespace := namespace{name: opts.namespace} + + networks := make(map[string]types.NetworkCreate) + for _, service := range bundle.Services { + for _, networkName := range service.Networks { + networks[networkName] = types.NetworkCreate{ + Labels: getStackLabels(namespace.name, nil), + } + } + } + + services := make(map[string]swarm.ServiceSpec) + for internalName, service := range bundle.Services { + name := namespace.scope(internalName) + + var ports []swarm.PortConfig + for _, portSpec := range service.Ports { + ports = append(ports, swarm.PortConfig{ + Protocol: swarm.PortConfigProtocol(portSpec.Protocol), + TargetPort: portSpec.Port, + }) + } + + nets := []swarm.NetworkAttachmentConfig{} + for _, networkName := range service.Networks { + nets = append(nets, swarm.NetworkAttachmentConfig{ + Target: namespace.scope(networkName), + Aliases: []string{networkName}, + }) + } + + serviceSpec := swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: name, + Labels: getStackLabels(namespace.name, service.Labels), + }, + TaskTemplate: swarm.TaskSpec{ + ContainerSpec: swarm.ContainerSpec{ + Image: service.Image, + Command: service.Command, + Args: service.Args, + Env: service.Env, + // Service Labels will not be copied to Containers + // automatically during the deployment so we apply + // it here. + Labels: getStackLabels(namespace.name, nil), + }, + }, + EndpointSpec: &swarm.EndpointSpec{ + Ports: ports, + }, + Networks: nets, + } + + services[internalName] = serviceSpec + } + + ctx := context.Background() + + if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil { + return err + } + return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth) +} From c9fe088e1377ba1f757a6b32f2f58e15ad188a3d Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 10 Nov 2016 11:33:00 -0500 Subject: [PATCH 349/978] Handle bind options and volume options Signed-off-by: Daniel Nephin Upstream-commit: 0333117b88cd8fd43ff47b9d20feb62c1977e907 Component: cli --- components/cli/command/stack/deploy.go | 154 +++++++++++++++---------- 1 file changed, 96 insertions(+), 58 deletions(-) diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index 6a633c9a8f..f68ca85555 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -259,70 +259,107 @@ func convertVolumes( ) ([]mount.Mount, error) { var mounts []mount.Mount - for _, volumeString := range serviceVolumes { - var ( - source, target string - mountType mount.Type - readOnly bool - volumeOptions *mount.VolumeOptions - ) - - // TODO: split Windows path mappings properly - parts := strings.SplitN(volumeString, ":", 3) - - if len(parts) == 3 { - source = parts[0] - target = parts[1] - if parts[2] == "ro" { - readOnly = true - } - } else if len(parts) == 2 { - source = parts[0] - target = parts[1] - } else if len(parts) == 1 { - target = parts[0] + for _, volumeSpec := range serviceVolumes { + mount, err := convertVolumeToMount(volumeSpec, stackVolumes, namespace) + if err != nil { + return nil, err } + mounts = append(mounts, mount) + } + return mounts, nil +} - // TODO: catch Windows paths here - if strings.HasPrefix(source, "/") { - mountType = mount.TypeBind - } else { - mountType = mount.TypeVolume +func convertVolumeToMount( + volumeSpec string, + stackVolumes map[string]composetypes.VolumeConfig, + namespace namespace, +) (mount.Mount, error) { + var source, target string + var mode []string - stackVolume, exists := stackVolumes[source] - if !exists { - // TODO: better error message (include service name) - return nil, fmt.Errorf("Undefined volume: %s", source) - } + // TODO: split Windows path mappings properly + parts := strings.SplitN(volumeSpec, ":", 3) - if stackVolume.External.Name != "" { - source = stackVolume.External.Name - } else { - volumeOptions = &mount.VolumeOptions{ - Labels: stackVolume.Labels, - } - - if stackVolume.Driver != "" { - volumeOptions.DriverConfig = &mount.Driver{ - Name: stackVolume.Driver, - Options: stackVolume.DriverOpts, - } - } - - source = namespace.scope(source) - } - } - - mounts = append(mounts, mount.Mount{ - Type: mountType, - Source: source, - Target: target, - ReadOnly: readOnly, - VolumeOptions: volumeOptions, - }) + switch len(parts) { + case 3: + source = parts[0] + target = parts[1] + mode = strings.Split(parts[2], ",") + case 2: + source = parts[0] + target = parts[1] + case 1: + target = parts[0] + default: + return mount.Mount{}, fmt.Errorf("invald volume: %s", volumeSpec) } - return mounts, nil + // TODO: catch Windows paths here + if strings.HasPrefix(source, "/") { + return mount.Mount{ + Type: mount.TypeBind, + Source: source, + Target: target, + ReadOnly: isReadOnly(mode), + BindOptions: getBindOptions(mode), + }, nil + } + + stackVolume, exists := stackVolumes[source] + if !exists { + return mount.Mount{}, fmt.Errorf("undefined volume: %s", source) + } + + var volumeOptions *mount.VolumeOptions + if stackVolume.External.Name != "" { + source = stackVolume.External.Name + } else { + volumeOptions = &mount.VolumeOptions{ + Labels: stackVolume.Labels, + NoCopy: isNoCopy(mode), + } + + if stackVolume.Driver != "" { + volumeOptions.DriverConfig = &mount.Driver{ + Name: stackVolume.Driver, + Options: stackVolume.DriverOpts, + } + } + source = namespace.scope(source) + } + return mount.Mount{ + Type: mount.TypeVolume, + Source: source, + Target: target, + ReadOnly: isReadOnly(mode), + VolumeOptions: volumeOptions, + }, nil +} + +func modeHas(mode []string, field string) bool { + for _, item := range mode { + if item == field { + return true + } + } + return false +} + +func isReadOnly(mode []string) bool { + return modeHas(mode, "ro") +} + +func isNoCopy(mode []string) bool { + return modeHas(mode, "nocopy") +} + +func getBindOptions(mode []string) *mount.BindOptions { + for _, item := range mode { + if strings.Contains(item, "private") || strings.Contains(item, "shared") || strings.Contains(item, "slave") { + return &mount.BindOptions{Propagation: mount.Propagation(item)} + } + } + return nil } func deployServices( @@ -429,6 +466,7 @@ func convertService( mounts, err := convertVolumes(service.Volumes, volumes, namespace) if err != nil { + // TODO: better error message (include service name) return swarm.ServiceSpec{}, err } From 1026429c8f55608a6b15f074fdabb669a4ddd40e Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Thu, 3 Nov 2016 08:05:00 -0700 Subject: [PATCH 350/978] Add flag `--host` to `service create` and `--host-add/--host-rm` to `service update` This fix tries to address 27902 by adding a flag `--host` to `docker service create` and `--host-add/--host-rm` to `docker service update`, so that it is possible to specify extra `host:ip` settings in `/etc/hosts`. This fix adds `Hosts` in swarmkit's `ContainerSpec` so that it is possible to specify extra hosts during service creation. Related docs has been updated. An integration test has been added. This fix fixes 27902. Signed-off-by: Yong Tang Upstream-commit: 6f3ee9c5687d06306744fb619064791c11890399 Component: cli --- components/cli/command/service/create.go | 1 + components/cli/command/service/opts.go | 20 ++++++++ components/cli/command/service/update.go | 49 +++++++++++++++++++ components/cli/command/service/update_test.go | 20 ++++++++ 4 files changed, 90 insertions(+) diff --git a/components/cli/command/service/create.go b/components/cli/command/service/create.go index 3efee10f41..17cf19625f 100644 --- a/components/cli/command/service/create.go +++ b/components/cli/command/service/create.go @@ -45,6 +45,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Var(&opts.dns, flagDNS, "Set custom DNS servers") flags.Var(&opts.dnsOption, flagDNSOption, "Set DNS options") flags.Var(&opts.dnsSearch, flagDNSSearch, "Set custom DNS search domains") + flags.Var(&opts.hosts, flagHost, "Set one or more custom host-to-IP mappings (host:ip)") flags.SetInterspersed(false) return cmd diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index c48c952e0c..52971ae833 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -397,6 +397,20 @@ func ValidatePort(value string) (string, error) { return value, err } +// convertExtraHostsToSwarmHosts converts an array of extra hosts in cli +// : +// into a swarmkit host format: +// IP_address canonical_hostname [aliases...] +// This assumes input value (:) has already been validated +func convertExtraHostsToSwarmHosts(extraHosts []string) []string { + hosts := []string{} + for _, extraHost := range extraHosts { + parts := strings.SplitN(extraHost, ":", 2) + hosts = append(hosts, fmt.Sprintf("%s %s", parts[1], parts[0])) + } + return hosts +} + type serviceOptions struct { name string labels opts.ListOpts @@ -414,6 +428,7 @@ type serviceOptions struct { dns opts.ListOpts dnsSearch opts.ListOpts dnsOption opts.ListOpts + hosts opts.ListOpts resources resourceOptions stopGrace DurationOpt @@ -450,6 +465,7 @@ func newServiceOptions() *serviceOptions { dns: opts.NewListOpts(opts.ValidateIPAddress), dnsOption: opts.NewListOpts(nil), dnsSearch: opts.NewListOpts(opts.ValidateDNSSearch), + hosts: opts.NewListOpts(runconfigopts.ValidateExtraHost), networks: opts.NewListOpts(nil), } } @@ -498,6 +514,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { Search: opts.dnsSearch.GetAll(), Options: opts.dnsOption.GetAll(), }, + Hosts: convertExtraHostsToSwarmHosts(opts.hosts.GetAll()), StopGracePeriod: opts.stopGrace.Value(), Secrets: nil, }, @@ -604,6 +621,9 @@ const ( flagDNSSearchRemove = "dns-search-rm" flagDNSSearchAdd = "dns-search-add" flagEndpointMode = "endpoint-mode" + flagHost = "host" + flagHostAdd = "host-add" + flagHostRemove = "host-rm" flagHostname = "hostname" flagEnv = "env" flagEnvFile = "env-file" diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index 9741f67d54..f5acc2c511 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -52,6 +52,7 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Var(newListOptsVar(), flagDNSRemove, "Remove a custom DNS server") flags.Var(newListOptsVar(), flagDNSOptionRemove, "Remove a DNS option") flags.Var(newListOptsVar(), flagDNSSearchRemove, "Remove a DNS search domain") + flags.Var(newListOptsVar(), flagHostRemove, "Remove a custom host-to-IP mapping (host:ip)") flags.Var(&opts.labels, flagLabelAdd, "Add or update a service label") flags.Var(&opts.containerLabels, flagContainerLabelAdd, "Add or update a container label") flags.Var(&opts.env, flagEnvAdd, "Add or update an environment variable") @@ -64,6 +65,7 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Var(&opts.dns, flagDNSAdd, "Add or update a custom DNS server") flags.Var(&opts.dnsOption, flagDNSOptionAdd, "Add or update a DNS option") flags.Var(&opts.dnsSearch, flagDNSSearchAdd, "Add or update a custom DNS search domain") + flags.Var(&opts.hosts, flagHostAdd, "Add or update a custom host-to-IP mapping (host:ip)") return cmd } @@ -283,6 +285,12 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error { } } + if anyChanged(flags, flagHostAdd, flagHostRemove) { + if err := updateHosts(flags, &cspec.Hosts); err != nil { + return err + } + } + if err := updateLogDriver(flags, &spec.TaskTemplate); err != nil { return err } @@ -683,6 +691,47 @@ func updateReplicas(flags *pflag.FlagSet, serviceMode *swarm.ServiceMode) error return nil } +func updateHosts(flags *pflag.FlagSet, hosts *[]string) error { + // Combine existing Hosts (in swarmkit format) with the host to add (convert to swarmkit format) + if flags.Changed(flagHostAdd) { + values := convertExtraHostsToSwarmHosts(flags.Lookup(flagHostAdd).Value.(*opts.ListOpts).GetAll()) + *hosts = append(*hosts, values...) + } + // Remove duplicate + *hosts = removeDuplicates(*hosts) + + keysToRemove := make(map[string]struct{}) + if flags.Changed(flagHostRemove) { + var empty struct{} + extraHostsToRemove := flags.Lookup(flagHostRemove).Value.(*opts.ListOpts).GetAll() + for _, entry := range extraHostsToRemove { + key := strings.SplitN(entry, ":", 2)[0] + keysToRemove[key] = empty + } + } + + newHosts := []string{} + for _, entry := range *hosts { + // Since this is in swarmkit format, we need to find the key, which is canonical_hostname of: + // IP_address canonical_hostname [aliases...] + parts := strings.Fields(entry) + if len(parts) > 1 { + key := parts[1] + if _, exists := keysToRemove[key]; !exists { + newHosts = append(newHosts, entry) + } + } else { + newHosts = append(newHosts, entry) + } + } + + // Sort so that result is predictable. + sort.Strings(newHosts) + + *hosts = newHosts + return nil +} + // updateLogDriver updates the log driver only if the log driver flag is set. // All options will be replaced with those provided on the command line. func updateLogDriver(flags *pflag.FlagSet, taskTemplate *swarm.TaskSpec) error { diff --git a/components/cli/command/service/update_test.go b/components/cli/command/service/update_test.go index b99064352a..a3736090ae 100644 --- a/components/cli/command/service/update_test.go +++ b/components/cli/command/service/update_test.go @@ -339,3 +339,23 @@ func TestUpdateHealthcheckTable(t *testing.T) { } } } + +func TestUpdateHosts(t *testing.T) { + flags := newUpdateCommand(nil).Flags() + flags.Set("host-add", "example.net:2.2.2.2") + flags.Set("host-add", "ipv6.net:2001:db8:abc8::1") + // remove with ipv6 should work + flags.Set("host-rm", "example.net:2001:db8:abc8::1") + // just hostname should work as well + flags.Set("host-rm", "example.net") + // bad format error + assert.Error(t, flags.Set("host-add", "$example.com$"), "bad format for add-host:") + + hosts := []string{"1.2.3.4 example.com", "4.3.2.1 example.org", "2001:db8:abc8::1 example.net"} + + updateHosts(flags, &hosts) + assert.Equal(t, len(hosts), 3) + assert.Equal(t, hosts[0], "1.2.3.4 example.com") + assert.Equal(t, hosts[1], "2001:db8:abc8::1 ipv6.net") + assert.Equal(t, hosts[2], "4.3.2.1 example.org") +} From c0ed996a0b5239bf7695a6638cc304e8ce875e9e Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Thu, 10 Nov 2016 12:05:19 -0800 Subject: [PATCH 351/978] cli: Change autolock flag description This change incorporates feedback from @thaJeztah in the PR that added the autolock flag. It changes the descriptions to be different for "swarm init" and "swarm update" so that the boolean nature so that the purpose of the flag in both contexts is clearer. Signed-off-by: Aaron Lehmann Upstream-commit: fd5673eeb955f79eca9ce84469afb3dd688dde50 Component: cli --- components/cli/command/swarm/init.go | 1 + components/cli/command/swarm/opts.go | 1 - components/cli/command/swarm/update.go | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/components/cli/command/swarm/init.go b/components/cli/command/swarm/init.go index 93c97c3a74..2550feeb47 100644 --- a/components/cli/command/swarm/init.go +++ b/components/cli/command/swarm/init.go @@ -40,6 +40,7 @@ func newInitCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Var(&opts.listenAddr, flagListenAddr, "Listen address (format: [:port])") flags.StringVar(&opts.advertiseAddr, flagAdvertiseAddr, "", "Advertised address (format: [:port])") flags.BoolVar(&opts.forceNewCluster, "force-new-cluster", false, "Force create a new cluster from current state") + flags.BoolVar(&opts.autolock, flagAutolock, false, "Enable manager autolocking (requiring an unlock key to start a stopped manager)") addSwarmFlags(flags, &opts.swarmOptions) return cmd } diff --git a/components/cli/command/swarm/opts.go b/components/cli/command/swarm/opts.go index 8682375b15..885a3cd04e 100644 --- a/components/cli/command/swarm/opts.go +++ b/components/cli/command/swarm/opts.go @@ -176,7 +176,6 @@ func addSwarmFlags(flags *pflag.FlagSet, opts *swarmOptions) { flags.Var(&opts.externalCA, flagExternalCA, "Specifications of one or more certificate signing endpoints") flags.Uint64Var(&opts.maxSnapshots, flagMaxSnapshots, 0, "Number of additional Raft snapshots to retain") flags.Uint64Var(&opts.snapshotInterval, flagSnapshotInterval, 10000, "Number of log entries between Raft snapshots") - flags.BoolVar(&opts.autolock, flagAutolock, false, "Enable or disable manager autolocking (requiring an unlock key to start a stopped manager)") } func (opts *swarmOptions) mergeSwarmSpec(spec *swarm.Spec, flags *pflag.FlagSet) { diff --git a/components/cli/command/swarm/update.go b/components/cli/command/swarm/update.go index 7c88760492..cb0d83ef26 100644 --- a/components/cli/command/swarm/update.go +++ b/components/cli/command/swarm/update.go @@ -25,6 +25,7 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { }, } + cmd.Flags().BoolVar(&opts.autolock, flagAutolock, false, "Change manager autolocking setting (true|false)") addSwarmFlags(cmd.Flags(), &opts) return cmd } From 2dbf6f72b035db4a3505b142db531e341f446bdd Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 10 Nov 2016 16:22:31 -0500 Subject: [PATCH 352/978] Implement ipamconfig.subnet and be more explicit about restart policy always. Signed-off-by: Daniel Nephin Upstream-commit: cb1783590c93a828fa1545a9cd3b8674ef2bdf67 Component: cli --- components/cli/command/stack/deploy.go | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index f68ca85555..33dd15e5a7 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -178,13 +178,19 @@ func convertNetworks( Options: network.DriverOpts, } - if network.Ipam.Driver != "" { - createOpts.IPAM = &networktypes.IPAM{ - Driver: network.Ipam.Driver, - } + if network.Ipam.Driver != "" || len(network.Ipam.Config) > 0 { + createOpts.IPAM = &networktypes.IPAM{} } - // TODO: IPAMConfig.Config + if network.Ipam.Driver != "" { + createOpts.IPAM.Driver = network.Ipam.Driver + } + for _, ipamConfig := range network.Ipam.Config { + config := networktypes.IPAMConfig{ + Subnet: ipamConfig.Subnet, + } + createOpts.IPAM.Config = append(createOpts.IPAM.Config, config) + } result[internalName] = createOpts } @@ -523,8 +529,12 @@ func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (* } // TODO: is this an accurate convertion? switch { - case policy.IsNone(), policy.IsAlways(), policy.IsUnlessStopped(): + case policy.IsNone(): return nil, nil + case policy.IsAlways(), policy.IsUnlessStopped(): + return &swarm.RestartPolicy{ + Condition: swarm.RestartPolicyConditionAny, + }, nil case policy.IsOnFailure(): attempts := uint64(policy.MaximumRetryCount) return &swarm.RestartPolicy{ From 0caec6c7c496dd3d70297d33424f9a496eae1777 Mon Sep 17 00:00:00 2001 From: Andrea Luzzardi Date: Wed, 26 Oct 2016 01:19:32 -0700 Subject: [PATCH 353/978] cli: docker service logs support Signed-off-by: Andrea Luzzardi Upstream-commit: b059cf5286f407ad4f7ce317d65391da290a72ed Component: cli --- components/cli/command/service/cmd.go | 1 + components/cli/command/service/logs.go | 163 +++++++++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 components/cli/command/service/logs.go diff --git a/components/cli/command/service/cmd.go b/components/cli/command/service/cmd.go index f4f7d00f91..63f2db7171 100644 --- a/components/cli/command/service/cmd.go +++ b/components/cli/command/service/cmd.go @@ -26,6 +26,7 @@ func NewServiceCommand(dockerCli *command.DockerCli) *cobra.Command { newRemoveCommand(dockerCli), newScaleCommand(dockerCli), newUpdateCommand(dockerCli), + newLogsCommand(dockerCli), ) return cmd } diff --git a/components/cli/command/service/logs.go b/components/cli/command/service/logs.go new file mode 100644 index 0000000000..19d3d9a488 --- /dev/null +++ b/components/cli/command/service/logs.go @@ -0,0 +1,163 @@ +package service + +import ( + "bytes" + "fmt" + "io" + "strings" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/idresolver" + "github.com/docker/docker/pkg/stdcopy" + "github.com/spf13/cobra" +) + +type logsOptions struct { + noResolve bool + follow bool + since string + timestamps bool + details bool + tail string + + service string +} + +func newLogsCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts logsOptions + + cmd := &cobra.Command{ + Use: "logs [OPTIONS] SERVICE", + Short: "Fetch the logs of a service", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.service = args[0] + return runLogs(dockerCli, &opts) + }, + Tags: map[string]string{"experimental": ""}, + } + + flags := cmd.Flags() + flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names") + flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output") + flags.StringVar(&opts.since, "since", "", "Show logs since timestamp") + flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps") + flags.BoolVar(&opts.details, "details", false, "Show extra details provided to logs") + flags.StringVar(&opts.tail, "tail", "all", "Number of lines to show from the end of the logs") + return cmd +} + +func runLogs(dockerCli *command.DockerCli, opts *logsOptions) error { + ctx := context.Background() + + options := types.ContainerLogsOptions{ + ShowStdout: true, + ShowStderr: true, + Since: opts.since, + Timestamps: opts.timestamps, + Follow: opts.follow, + Tail: opts.tail, + Details: opts.details, + } + + client := dockerCli.Client() + responseBody, err := client.ServiceLogs(ctx, opts.service, options) + if err != nil { + return err + } + defer responseBody.Close() + + resolver := idresolver.New(client, opts.noResolve) + + stdout := &logWriter{ctx: ctx, opts: opts, r: resolver, w: dockerCli.Out()} + stderr := &logWriter{ctx: ctx, opts: opts, r: resolver, w: dockerCli.Err()} + + // TODO(aluzzardi): Do an io.Copy for services with TTY enabled. + _, err = stdcopy.StdCopy(stdout, stderr, responseBody) + return err +} + +type logWriter struct { + ctx context.Context + opts *logsOptions + r *idresolver.IDResolver + w io.Writer +} + +func (lw *logWriter) Write(buf []byte) (int, error) { + contextIndex := 0 + numParts := 2 + if lw.opts.timestamps { + contextIndex++ + numParts++ + } + + parts := bytes.SplitN(buf, []byte(" "), numParts) + if len(parts) != numParts { + return 0, fmt.Errorf("invalid context in log message: %v", string(buf)) + } + + taskName, nodeName, err := lw.parseContext(string(parts[contextIndex])) + if err != nil { + return 0, err + } + + output := []byte{} + for i, part := range parts { + // First part doesn't get space separation. + if i > 0 { + output = append(output, []byte(" ")...) + } + + if i == contextIndex { + // TODO(aluzzardi): Consider constant padding. + output = append(output, []byte(fmt.Sprintf("%s@%s |", taskName, nodeName))...) + } else { + output = append(output, part...) + } + } + _, err = lw.w.Write(output) + if err != nil { + return 0, err + } + + return len(buf), nil +} + +func (lw *logWriter) parseContext(input string) (string, string, error) { + context := make(map[string]string) + + components := strings.Split(input, ",") + for _, component := range components { + parts := strings.SplitN(component, "=", 2) + if len(parts) != 2 { + return "", "", fmt.Errorf("invalid context: %s", input) + } + context[parts[0]] = parts[1] + } + + taskID, ok := context["com.docker.swarm.task.id"] + if !ok { + return "", "", fmt.Errorf("missing task id in context: %s", input) + } + taskName, err := lw.r.Resolve(lw.ctx, swarm.Task{}, taskID) + if err != nil { + return "", "", err + } + + nodeID, ok := context["com.docker.swarm.node.id"] + if !ok { + return "", "", fmt.Errorf("missing node id in context: %s", input) + } + nodeName, err := lw.r.Resolve(lw.ctx, swarm.Node{}, nodeID) + if err != nil { + return "", "", err + } + + return taskName, nodeName, nil +} From 915d9ab39104c4942da318de0fe3d2498dc946eb Mon Sep 17 00:00:00 2001 From: Andrea Luzzardi Date: Wed, 26 Oct 2016 01:17:31 -0700 Subject: [PATCH 354/978] api: Service Logs support Signed-off-by: Andrea Luzzardi Upstream-commit: f88c041647f89d815e3289431dd50f5fafa2e9aa Component: cli --- components/cli/interface.go | 1 + components/cli/service_logs.go | 52 +++++++++++ components/cli/service_logs_test.go | 133 ++++++++++++++++++++++++++++ 3 files changed, 186 insertions(+) create mode 100644 components/cli/service_logs.go create mode 100644 components/cli/service_logs_test.go diff --git a/components/cli/interface.go b/components/cli/interface.go index f24c9a51f6..883e8801fa 100644 --- a/components/cli/interface.go +++ b/components/cli/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/components/cli/service_logs.go b/components/cli/service_logs.go new file mode 100644 index 0000000000..24384e3ec0 --- /dev/null +++ b/components/cli/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/components/cli/service_logs_test.go b/components/cli/service_logs_test.go new file mode 100644 index 0000000000..a6d002ba75 --- /dev/null +++ b/components/cli/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 66e137540f7b337706a2a7a1c7b1948f2acb4552 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Wed, 9 Nov 2016 17:49:09 -0800 Subject: [PATCH 355/978] move plugins out of experimental Signed-off-by: Victor Vieux Upstream-commit: f5cea67e3317fd7f439bd10fcd15f80b8a4b780b Component: cli --- components/cli/command/plugin/cmd.go | 1 - 1 file changed, 1 deletion(-) diff --git a/components/cli/command/plugin/cmd.go b/components/cli/command/plugin/cmd.go index e55f4ef327..6e1160fd91 100644 --- a/components/cli/command/plugin/cmd.go +++ b/components/cli/command/plugin/cmd.go @@ -16,7 +16,6 @@ func NewPluginCommand(dockerCli *command.DockerCli) *cobra.Command { cmd.SetOutput(dockerCli.Err()) cmd.HelpFunc()(cmd, args) }, - Tags: map[string]string{"experimental": ""}, } cmd.AddCommand( From a867bf217d664381282264969f3b32447494843d Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Wed, 9 Nov 2016 17:49:09 -0800 Subject: [PATCH 356/978] move plugins out of experimental Signed-off-by: Victor Vieux Upstream-commit: 1f6f5bec49e95981169c9713abe10b5ea6e4aaa1 Component: cli --- components/cli/interface.go | 14 ++++++++++++++ components/cli/interface_experimental.go | 16 ---------------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/components/cli/interface.go b/components/cli/interface.go index 883e8801fa..7a3ebe8b4c 100644 --- a/components/cli/interface.go +++ b/components/cli/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/components/cli/interface_experimental.go b/components/cli/interface_experimental.go index 709b5d8ffb..51da98ecdd 100644 --- a/components/cli/interface_experimental.go +++ b/components/cli/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 bfaa4fa16396b19379542e94c7fffb2a21267cd2 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Thu, 10 Nov 2016 15:59:02 -0800 Subject: [PATCH 357/978] Update for distribution vendor Handle updates to reference package. Updates for refactoring of challenge manager. Signed-off-by: Derek McGowan (github: dmcgowan) Upstream-commit: 43bcd982cdee5a190b5b340d9852c1331e781331 Component: cli --- components/cli/command/image/trust.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/components/cli/command/image/trust.go b/components/cli/command/image/trust.go index b8de6a5245..d1106b532e 100644 --- a/components/cli/command/image/trust.go +++ b/components/cli/command/image/trust.go @@ -20,6 +20,7 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/distribution/digest" "github.com/docker/distribution/registry/client/auth" + "github.com/docker/distribution/registry/client/auth/challenge" "github.com/docker/distribution/registry/client/transport" "github.com/docker/docker/api/types" registrytypes "github.com/docker/docker/api/types/registry" @@ -291,7 +292,7 @@ func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry } fmt.Fprintf(cli.Out(), "Pull (%d of %d): %s%s@%s\n", i+1, len(refs), repoInfo.Name(), displayTag, r.digest) - ref, err := reference.WithDigest(repoInfo, r.digest) + ref, err := reference.WithDigest(reference.TrimNamed(repoInfo), r.digest) if err != nil { return err } @@ -305,7 +306,7 @@ func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry if err != nil { return err } - trustedRef, err := reference.WithDigest(repoInfo, r.digest) + trustedRef, err := reference.WithDigest(reference.TrimNamed(repoInfo), r.digest) if err != nil { return err } @@ -434,7 +435,7 @@ func GetNotaryRepository(streams command.Streams, repoInfo *registry.RepositoryI return nil, err } - challengeManager := auth.NewSimpleChallengeManager() + challengeManager := challenge.NewSimpleManager() resp, err := pingClient.Do(req) if err != nil { @@ -523,7 +524,7 @@ func TrustedReference(ctx context.Context, cli *command.DockerCli, ref reference } - return reference.WithDigest(ref, r.digest) + return reference.WithDigest(reference.TrimNamed(ref), r.digest) } func convertTarget(t client.Target) (target, error) { From 69a2e4be9f51f1274f47e48be28c0b787b88cf10 Mon Sep 17 00:00:00 2001 From: Jana Radhakrishnan Date: Thu, 10 Nov 2016 12:13:26 -0800 Subject: [PATCH 358/978] Add support for host port PublishMode in services Add api/cli support for adding host port PublishMode in services. Signed-off-by: Jana Radhakrishnan Upstream-commit: 148dc157f6d4f947595b7a13c138276913cdd35a Component: cli --- components/cli/command/service/create.go | 4 +- components/cli/command/service/opts.go | 14 +++-- components/cli/command/service/update.go | 55 ++++++++++++++++++- components/cli/command/service/update_test.go | 18 +----- components/cli/command/task/print.go | 20 ++++++- 5 files changed, 84 insertions(+), 27 deletions(-) diff --git a/components/cli/command/service/create.go b/components/cli/command/service/create.go index 17cf19625f..335867186a 100644 --- a/components/cli/command/service/create.go +++ b/components/cli/command/service/create.go @@ -40,12 +40,14 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Var(&opts.constraints, flagConstraint, "Placement constraints") flags.Var(&opts.networks, flagNetwork, "Network attachments") flags.Var(&opts.secrets, flagSecret, "Specify secrets to expose to the service") - flags.VarP(&opts.endpoint.ports, flagPublish, "p", "Publish a port as a node port") + flags.VarP(&opts.endpoint.publishPorts, flagPublish, "p", "Publish a port as a node port") + flags.MarkHidden(flagPublish) flags.Var(&opts.groups, flagGroup, "Set one or more supplementary user groups for the container") flags.Var(&opts.dns, flagDNS, "Set custom DNS servers") flags.Var(&opts.dnsOption, flagDNSOption, "Set DNS options") flags.Var(&opts.dnsSearch, flagDNSSearch, "Set custom DNS search domains") flags.Var(&opts.hosts, flagHost, "Set one or more custom host-to-IP mappings (host:ip)") + flags.Var(&opts.endpoint.expandedPorts, flagPort, "Publish a port") flags.SetInterspersed(false) return cmd diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index 4ea78c6af7..7da8338512 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -287,14 +287,15 @@ func convertNetworks(networks []string) []swarm.NetworkAttachmentConfig { } type endpointOptions struct { - mode string - ports opts.ListOpts + mode string + publishPorts opts.ListOpts + expandedPorts opts.PortOpt } func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec { portConfigs := []swarm.PortConfig{} // We can ignore errors because the format was already validated by ValidatePort - ports, portBindings, _ := nat.ParsePortSpecs(e.ports.GetAll()) + ports, portBindings, _ := nat.ParsePortSpecs(e.publishPorts.GetAll()) for port := range ports { portConfigs = append(portConfigs, ConvertPortToPortConfig(port, portBindings)...) @@ -302,7 +303,7 @@ func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec { return &swarm.EndpointSpec{ Mode: swarm.ResolutionMode(strings.ToLower(e.mode)), - Ports: portConfigs, + Ports: append(portConfigs, e.expandedPorts.Value()...), } } @@ -459,7 +460,7 @@ func newServiceOptions() *serviceOptions { env: opts.NewListOpts(runconfigopts.ValidateEnv), envFile: opts.NewListOpts(nil), endpoint: endpointOptions{ - ports: opts.NewListOpts(ValidatePort), + publishPorts: opts.NewListOpts(ValidatePort), }, groups: opts.NewListOpts(nil), logDriver: newLogDriverOptions(), @@ -647,6 +648,9 @@ const ( flagPublish = "publish" flagPublishRemove = "publish-rm" flagPublishAdd = "publish-add" + flagPort = "port" + flagPortAdd = "port-add" + flagPortRemove = "port-rm" flagReplicas = "replicas" flagReserveCPU = "reserve-cpu" flagReserveMemory = "reserve-memory" diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index 1214b03a53..d2639a62db 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -48,6 +48,8 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Var(newListOptsVar(), flagContainerLabelRemove, "Remove a container label by its key") flags.Var(newListOptsVar(), flagMountRemove, "Remove a mount by its target path") flags.Var(newListOptsVar(), flagPublishRemove, "Remove a published port by its target port") + flags.MarkHidden(flagPublishRemove) + flags.Var(newListOptsVar(), flagPortRemove, "Remove a port(target-port mandatory)") flags.Var(newListOptsVar(), flagConstraintRemove, "Remove a constraint") flags.Var(newListOptsVar(), flagDNSRemove, "Remove a custom DNS server") flags.Var(newListOptsVar(), flagDNSOptionRemove, "Remove a DNS option") @@ -60,7 +62,9 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Var(&opts.secrets, flagSecretAdd, "Add or update a secret on a service") flags.Var(&opts.mounts, flagMountAdd, "Add or update a mount on a service") flags.Var(&opts.constraints, flagConstraintAdd, "Add or update a placement constraint") - flags.Var(&opts.endpoint.ports, flagPublishAdd, "Add or update a published port") + flags.Var(&opts.endpoint.publishPorts, flagPublishAdd, "Add or update a published port") + flags.MarkHidden(flagPublishAdd) + flags.Var(&opts.endpoint.expandedPorts, flagPortAdd, "Add or update a port") flags.Var(&opts.groups, flagGroupAdd, "Add an additional supplementary user group to the container") flags.Var(&opts.dns, flagDNSAdd, "Add or update a custom DNS server") flags.Var(&opts.dnsOption, flagDNSOptionAdd, "Add or update a DNS option") @@ -267,7 +271,7 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error { } } - if anyChanged(flags, flagPublishAdd, flagPublishRemove) { + if anyChanged(flags, flagPublishAdd, flagPublishRemove, flagPortAdd, flagPortRemove) { if spec.EndpointSpec == nil { spec.EndpointSpec = &swarm.EndpointSpec{} } @@ -627,7 +631,13 @@ func portConfigToString(portConfig *swarm.PortConfig) string { if protocol == "" { protocol = "tcp" } - return fmt.Sprintf("%v/%s", portConfig.PublishedPort, protocol) + + mode := portConfig.PublishMode + if mode == "" { + mode = "ingress" + } + + return fmt.Sprintf("%v:%v/%s/%s", portConfig.PublishedPort, portConfig.TargetPort, protocol, mode) } func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) error { @@ -649,6 +659,15 @@ func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) error { } } + if flags.Changed(flagPortAdd) { + for _, entry := range flags.Lookup(flagPortAdd).Value.(*opts.PortOpt).Value() { + if v, ok := portSet[portConfigToString(&entry)]; ok && v != entry { + return fmt.Errorf("conflicting port mapping between %v:%v/%s and %v:%v/%s", entry.PublishedPort, entry.TargetPort, entry.Protocol, v.PublishedPort, v.TargetPort, v.Protocol) + } + portSet[portConfigToString(&entry)] = entry + } + } + // Override previous PortConfig in service if there is any duplicate for _, entry := range *portConfig { if _, ok := portSet[portConfigToString(&entry)]; !ok { @@ -657,6 +676,14 @@ func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) error { } toRemove := flags.Lookup(flagPublishRemove).Value.(*opts.ListOpts).GetAll() + removePortCSV := flags.Lookup(flagPortRemove).Value.(*opts.ListOpts).GetAll() + removePortOpts := &opts.PortOpt{} + for _, portCSV := range removePortCSV { + if err := removePortOpts.Set(portCSV); err != nil { + return err + } + } + newPorts := []swarm.PortConfig{} portLoop: for _, port := range portSet { @@ -666,14 +693,36 @@ portLoop: continue portLoop } } + + for _, pConfig := range removePortOpts.Value() { + if equalProtocol(port.Protocol, pConfig.Protocol) && + port.TargetPort == pConfig.TargetPort && + equalPublishMode(port.PublishMode, pConfig.PublishMode) { + continue portLoop + } + } + newPorts = append(newPorts, port) } + // Sort the PortConfig to avoid unnecessary updates sort.Sort(byPortConfig(newPorts)) *portConfig = newPorts return nil } +func equalProtocol(prot1, prot2 swarm.PortConfigProtocol) bool { + return prot1 == prot2 || + (prot1 == swarm.PortConfigProtocol("") && prot2 == swarm.PortConfigProtocolTCP) || + (prot2 == swarm.PortConfigProtocol("") && prot1 == swarm.PortConfigProtocolTCP) +} + +func equalPublishMode(mode1, mode2 swarm.PortConfigPublishMode) bool { + return mode1 == mode2 || + (mode1 == swarm.PortConfigPublishMode("") && mode2 == swarm.PortConfigPublishModeIngress) || + (mode2 == swarm.PortConfigPublishMode("") && mode1 == swarm.PortConfigPublishModeIngress) +} + func equalPort(targetPort nat.Port, port swarm.PortConfig) bool { return (string(port.Protocol) == targetPort.Proto() && port.TargetPort == uint32(targetPort.Int())) diff --git a/components/cli/command/service/update_test.go b/components/cli/command/service/update_test.go index a3736090ae..998d06d3bd 100644 --- a/components/cli/command/service/update_test.go +++ b/components/cli/command/service/update_test.go @@ -238,7 +238,7 @@ func TestUpdatePortsDuplicateEntries(t *testing.T) { func TestUpdatePortsDuplicateKeys(t *testing.T) { // Test case for #25375 flags := newUpdateCommand(nil).Flags() - flags.Set("publish-add", "80:20") + flags.Set("publish-add", "80:80") portConfigs := []swarm.PortConfig{ {TargetPort: 80, PublishedPort: 80}, @@ -247,21 +247,7 @@ func TestUpdatePortsDuplicateKeys(t *testing.T) { err := updatePorts(flags, &portConfigs) assert.Equal(t, err, nil) assert.Equal(t, len(portConfigs), 1) - assert.Equal(t, portConfigs[0].TargetPort, uint32(20)) -} - -func TestUpdatePortsConflictingFlags(t *testing.T) { - // Test case for #25375 - flags := newUpdateCommand(nil).Flags() - flags.Set("publish-add", "80:80") - flags.Set("publish-add", "80:20") - - portConfigs := []swarm.PortConfig{ - {TargetPort: 80, PublishedPort: 80}, - } - - err := updatePorts(flags, &portConfigs) - assert.Error(t, err, "conflicting port mapping") + assert.Equal(t, portConfigs[0].TargetPort, uint32(80)) } func TestUpdateHealthcheckTable(t *testing.T) { diff --git a/components/cli/command/task/print.go b/components/cli/command/task/print.go index 45af178a42..2c5b2eecdd 100644 --- a/components/cli/command/task/print.go +++ b/components/cli/command/task/print.go @@ -17,10 +17,25 @@ import ( ) const ( - psTaskItemFmt = "%s\t%s\t%s\t%s\t%s %s ago\t%s\n" + psTaskItemFmt = "%s\t%s\t%s\t%s\t%s %s ago\t%s\t%s\n" maxErrLength = 30 ) +type portStatus swarm.PortStatus + +func (ps portStatus) String() string { + if len(ps.Ports) == 0 { + return "" + } + + str := fmt.Sprintf("*:%d->%d/%s", ps.Ports[0].PublishedPort, ps.Ports[0].TargetPort, ps.Ports[0].Protocol) + for _, pConfig := range ps.Ports[1:] { + str += fmt.Sprintf(",*:%d->%d/%s", pConfig.PublishedPort, pConfig.TargetPort, pConfig.Protocol) + } + + return str +} + type tasksBySlot []swarm.Task func (t tasksBySlot) Len() int { @@ -51,7 +66,7 @@ func Print(dockerCli *command.DockerCli, ctx context.Context, tasks []swarm.Task // Ignore flushing errors defer writer.Flush() - fmt.Fprintln(writer, strings.Join([]string{"NAME", "IMAGE", "NODE", "DESIRED STATE", "CURRENT STATE", "ERROR"}, "\t")) + fmt.Fprintln(writer, strings.Join([]string{"NAME", "IMAGE", "NODE", "DESIRED STATE", "CURRENT STATE", "ERROR", "PORTS"}, "\t")) if err := print(writer, ctx, tasks, resolver, noTrunc); err != nil { return err @@ -113,6 +128,7 @@ func print(out io.Writer, ctx context.Context, tasks []swarm.Task, resolver *idr command.PrettyPrint(task.Status.State), strings.ToLower(units.HumanDuration(time.Since(task.Status.Timestamp))), taskErr, + portStatus(task.Status.PortStatus), ) } return nil From cc37942a6e69a4b1b67336888743a1b381754175 Mon Sep 17 00:00:00 2001 From: Wang Long Date: Fri, 11 Nov 2016 10:22:32 +0800 Subject: [PATCH 359/978] Use '.' directly Signed-off-by: Wang Long Upstream-commit: 076bfc0647de666cf04c40c506dcfe08ac90ebf3 Component: cli --- components/cli/docker.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/docker.go b/components/cli/docker.go index 18cd0e833c..1978fc33bc 100644 --- a/components/cli/docker.go +++ b/components/cli/docker.go @@ -79,7 +79,7 @@ func noArgs(cmd *cobra.Command, args []string) error { return nil } return fmt.Errorf( - "docker: '%s' is not a docker command.\nSee 'docker --help'%s", args[0], ".") + "docker: '%s' is not a docker command.\nSee 'docker --help'.", args[0]) } func main() { From ea25b458461038a1fb21c23a24f1ed7262a93fb7 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 11 Nov 2016 11:27:21 +0100 Subject: [PATCH 360/978] Add support for tty in composefile v3 Signed-off-by: Vincent Demeester Upstream-commit: 356421b7dac6dabf0aa278f1ec5921004a63f7e5 Component: cli --- components/cli/command/stack/deploy.go | 1 + 1 file changed, 1 insertion(+) diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index 33dd15e5a7..94ef6bac2c 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -504,6 +504,7 @@ func convertService( User: service.User, Mounts: mounts, StopGracePeriod: service.StopGracePeriod, + TTY: service.Tty, }, Resources: resources, RestartPolicy: restartPolicy, From 03ba4d6a333405753bb6d56a00ce5465ca76f559 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 11 Nov 2016 15:15:10 +0100 Subject: [PATCH 361/978] Add support for stdin_open in composefile v3 Signed-off-by: Vincent Demeester Upstream-commit: f24ff647e150dd1094d525b0bbcd2e994b75d45f Component: cli --- components/cli/command/stack/deploy.go | 1 + 1 file changed, 1 insertion(+) diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index 94ef6bac2c..147df1a0b9 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -505,6 +505,7 @@ func convertService( Mounts: mounts, StopGracePeriod: service.StopGracePeriod, TTY: service.Tty, + OpenStdin: service.StdinOpen, }, Resources: resources, RestartPolicy: restartPolicy, From a03dcc83b4348c7a7ead567fc0c3ee3727308a2d Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 11 Nov 2016 14:19:41 +0100 Subject: [PATCH 362/978] Add support for extra_hosts in composefile v3 Signed-off-by: Vincent Demeester Upstream-commit: 84a795bf05c878d4929d8831f4e6d06af96d4454 Component: cli --- components/cli/command/stack/deploy.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index 94ef6bac2c..9996f128b3 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -498,6 +498,7 @@ func convertService( Command: service.Entrypoint, Args: service.Command, Hostname: service.Hostname, + Hosts: convertExtraHosts(service.ExtraHosts), Env: convertEnvironment(service.Environment), Labels: getStackLabels(namespace.name, service.Labels), Dir: service.WorkingDir, @@ -521,6 +522,14 @@ func convertService( return serviceSpec, nil } +func convertExtraHosts(extraHosts map[string]string) []string { + hosts := []string{} + for host, ip := range extraHosts { + hosts = append(hosts, fmt.Sprintf("%s %s", host, ip)) + } + return hosts +} + func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (*swarm.RestartPolicy, error) { // TODO: log if restart is being ignored if source == nil { From d6a4ee3c7d38994b4a553f993d9c75ef01caca30 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 11 Nov 2016 15:34:01 +0100 Subject: [PATCH 363/978] =?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 Upstream-commit: 3c61af0f76bcf2ff0342f01c09bef771fbd567f6 Component: cli --- components/cli/command/image/list.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/components/cli/command/image/list.go b/components/cli/command/image/list.go index 587869fdf1..679604fc02 100644 --- a/components/cli/command/image/list.go +++ b/components/cli/command/image/list.go @@ -60,10 +60,14 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command { func runImages(dockerCli *command.DockerCli, opts imagesOptions) error { ctx := context.Background() + filters := opts.filter.Value() + if opts.matchName != "" { + filters.Add("reference", opts.matchName) + } + options := types.ImageListOptions{ - MatchName: opts.matchName, - All: opts.all, - Filters: opts.filter.Value(), + All: opts.all, + Filters: filters, } images, err := dockerCli.Client().ImageList(ctx, options) From 608f23172fcd01bedb4861552fe7ffa6eae6382c Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 11 Nov 2016 15:34:01 +0100 Subject: [PATCH 364/978] =?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 Upstream-commit: 85e72de60c165ef682a8b5cee88188a85ca34261 Component: cli --- components/cli/image_list.go | 4 ---- components/cli/image_list_test.go | 11 ----------- 2 files changed, 15 deletions(-) diff --git a/components/cli/image_list.go b/components/cli/image_list.go index 6ebb460541..63c71b1dd1 100644 --- a/components/cli/image_list.go +++ b/components/cli/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/components/cli/image_list_test.go b/components/cli/image_list_test.go index 1ea6f1f05a..1c9406ddda 100644 --- a/components/cli/image_list_test.go +++ b/components/cli/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 ea03f9e605ca09dfb01284c91d2de7fd381999ae Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Fri, 11 Nov 2016 09:56:25 -0500 Subject: [PATCH 365/978] only check secrets for service create if requested Signed-off-by: Evan Hazlett Upstream-commit: 885c5f174725bee8e3a6e1c42c8c06a18cfb5fa9 Component: cli --- components/cli/command/service/create.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/components/cli/command/service/create.go b/components/cli/command/service/create.go index 335867186a..061a36f06c 100644 --- a/components/cli/command/service/create.go +++ b/components/cli/command/service/create.go @@ -62,12 +62,16 @@ func runCreate(dockerCli *command.DockerCli, opts *serviceOptions) error { return err } - // parse and validate secrets - secrets, err := parseSecrets(apiClient, opts.secrets.Value()) - if err != nil { - return err + specifiedSecrets := opts.secrets.Value() + if len(specifiedSecrets) > 0 { + // parse and validate secrets + secrets, err := parseSecrets(apiClient, specifiedSecrets) + if err != nil { + return err + } + service.TaskTemplate.ContainerSpec.Secrets = secrets + } - service.TaskTemplate.ContainerSpec.Secrets = secrets ctx := context.Background() From cf7711be4df722f4c0e56eaec4635c9673c5c6f7 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Thu, 3 Nov 2016 11:23:58 -0700 Subject: [PATCH 366/978] Add information for `Manager Addresses` in the output of `docker info` As is specified in 28018, it would be useful to know the manager's addresses even in a worker node. This is especially useful when there are many worker nodes in a big cluster. The information is available in `info.Swarm.RemoteManagers`. This fix add the information of `Manager Addresses` to the output of `docker info`, to explicitly show it. A test has been added for this fix. This fix fixes 28018. Signed-off-by: Yong Tang Upstream-commit: cd71257cfd03e08df671040d7e60a937b8f8e073 Component: cli --- components/cli/command/system/info.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/components/cli/command/system/info.go b/components/cli/command/system/info.go index 36b09a9cc5..b751bbff13 100644 --- a/components/cli/command/system/info.go +++ b/components/cli/command/system/info.go @@ -2,6 +2,7 @@ package system import ( "fmt" + "sort" "strings" "time" @@ -131,6 +132,17 @@ func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error { } } fmt.Fprintf(dockerCli.Out(), " Node Address: %s\n", info.Swarm.NodeAddr) + managers := []string{} + for _, entry := range info.Swarm.RemoteManagers { + managers = append(managers, entry.Addr) + } + if len(managers) > 0 { + sort.Strings(managers) + fmt.Fprintf(dockerCli.Out(), " Manager Addresses:\n") + for _, entry := range managers { + fmt.Fprintf(dockerCli.Out(), " %s\n", entry) + } + } } if len(info.Runtimes) > 0 { From d1393049a21e5eeb7fe2936e7227a4eef6b302c3 Mon Sep 17 00:00:00 2001 From: John Howard Date: Fri, 23 Sep 2016 11:52:57 -0700 Subject: [PATCH 367/978] Planned 1.13 deprecation: email from login Signed-off-by: John Howard Upstream-commit: 4088f3bcff5c9704b33afa6345f41c02aa8409e8 Component: cli --- components/cli/command/registry/login.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/components/cli/command/registry/login.go b/components/cli/command/registry/login.go index 93e1b40e36..f161f2d403 100644 --- a/components/cli/command/registry/login.go +++ b/components/cli/command/registry/login.go @@ -35,14 +35,9 @@ func NewLoginCommand(dockerCli *command.DockerCli) *cobra.Command { } flags := cmd.Flags() - flags.StringVarP(&opts.user, "username", "u", "", "Username") flags.StringVarP(&opts.password, "password", "p", "", "Password") - // Deprecated in 1.11: Should be removed in docker 1.14 - flags.StringVarP(&opts.email, "email", "e", "", "Email") - flags.MarkDeprecated("email", "will be removed in 1.14.") - return cmd } From c6c57b1d27c9f1cbfa9cf3842703fe21443ccf75 Mon Sep 17 00:00:00 2001 From: John Stephens Date: Fri, 11 Nov 2016 17:43:06 -0800 Subject: [PATCH 368/978] Show experimental flags and subcommands if enabled Signed-off-by: John Stephens Upstream-commit: 13d6a1bb679285ac6501125c8f0d9bcf722f13ef Component: cli --- components/cli/docker.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/components/cli/docker.go b/components/cli/docker.go index 1978fc33bc..02d2918f0c 100644 --- a/components/cli/docker.go +++ b/components/cli/docker.go @@ -126,8 +126,10 @@ func dockerPreRun(opts *cliflags.ClientOptions) { func hideUnsupportedFeatures(cmd *cobra.Command, clientVersion string, hasExperimental bool) { cmd.Flags().VisitAll(func(f *pflag.Flag) { // hide experimental flags - if _, ok := f.Annotations["experimental"]; ok { - f.Hidden = true + if !hasExperimental { + if _, ok := f.Annotations["experimental"]; ok { + f.Hidden = true + } } // hide flags not supported by the server @@ -139,8 +141,10 @@ func hideUnsupportedFeatures(cmd *cobra.Command, clientVersion string, hasExperi for _, subcmd := range cmd.Commands() { // hide experimental subcommands - if _, ok := subcmd.Tags["experimental"]; ok { - subcmd.Hidden = true + if !hasExperimental { + if _, ok := subcmd.Tags["experimental"]; ok { + subcmd.Hidden = true + } } // hide subcommands not supported by the server From 85c67043dc8b462953c269fff2ef16eb02e1b539 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Fri, 11 Nov 2016 17:44:42 -0800 Subject: [PATCH 369/978] cli: Add valid suffixes for remaining duration options A recent PR added `(ns|us|ms|s|m|h)` to the descriptions of some duration options, but not all. Add it to the remaining options for consistency. Signed-off-by: Aaron Lehmann Upstream-commit: 7e7c4eefa4a650af8ec08fa851cab0a2ec627439 Component: cli --- components/cli/command/service/opts.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index 7da8338512..90d0f99249 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -575,14 +575,14 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { flags.Var(&opts.resources.limitMemBytes, flagLimitMemory, "Limit Memory") flags.Var(&opts.resources.resCPU, flagReserveCPU, "Reserve CPUs") flags.Var(&opts.resources.resMemBytes, flagReserveMemory, "Reserve Memory") - flags.Var(&opts.stopGrace, flagStopGracePeriod, "Time to wait before force killing a container") + flags.Var(&opts.stopGrace, flagStopGracePeriod, "Time to wait before force killing a container (ns|us|ms|s|m|h)") flags.Var(&opts.replicas, flagReplicas, "Number of tasks") flags.StringVar(&opts.restartPolicy.condition, flagRestartCondition, "", "Restart when condition is met (none, on-failure, or any)") - flags.Var(&opts.restartPolicy.delay, flagRestartDelay, "Delay between restart attempts") + flags.Var(&opts.restartPolicy.delay, flagRestartDelay, "Delay between restart attempts (ns|us|ms|s|m|h)") flags.Var(&opts.restartPolicy.maxAttempts, flagRestartMaxAttempts, "Maximum number of restarts before giving up") - flags.Var(&opts.restartPolicy.window, flagRestartWindow, "Window used to evaluate the restart policy") + flags.Var(&opts.restartPolicy.window, flagRestartWindow, "Window used to evaluate the restart policy (ns|us|ms|s|m|h)") flags.Uint64Var(&opts.update.parallelism, flagUpdateParallelism, 1, "Maximum number of tasks updated simultaneously (0 to update all at once)") flags.DurationVar(&opts.update.delay, flagUpdateDelay, time.Duration(0), "Delay between updates (ns|us|ms|s|m|h) (default 0s)") @@ -598,8 +598,8 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { flags.Var(&opts.logDriver.opts, flagLogOpt, "Logging driver options") flags.StringVar(&opts.healthcheck.cmd, flagHealthCmd, "", "Command to run to check health") - flags.Var(&opts.healthcheck.interval, flagHealthInterval, "Time between running the check") - flags.Var(&opts.healthcheck.timeout, flagHealthTimeout, "Maximum time to allow one check to run") + flags.Var(&opts.healthcheck.interval, flagHealthInterval, "Time between running the check (ns|us|ms|s|m|h)") + flags.Var(&opts.healthcheck.timeout, flagHealthTimeout, "Maximum time to allow one check to run (ns|us|ms|s|m|h)") flags.IntVar(&opts.healthcheck.retries, flagHealthRetries, 0, "Consecutive failures needed to report unhealthy") flags.BoolVar(&opts.healthcheck.noHealthcheck, flagNoHealthcheck, false, "Disable any container-specified HEALTHCHECK") From f1ed942575f3d28a98d65264c7d093ffb46dbb5c Mon Sep 17 00:00:00 2001 From: yupeng Date: Sat, 12 Nov 2016 14:14:34 +0800 Subject: [PATCH 370/978] context.Context should be the first parameter of a function Signed-off-by: yupeng Upstream-commit: 0f6af2074c64cc41a3d96520b8921291af1a4a48 Component: cli --- components/cli/command/container/attach.go | 2 +- components/cli/command/container/run.go | 2 +- components/cli/command/container/start.go | 2 +- components/cli/command/container/stats.go | 8 ++++---- components/cli/command/container/stats_helpers.go | 2 +- components/cli/command/container/utils.go | 4 ++-- components/cli/command/secret/inspect.go | 2 +- components/cli/command/secret/remove.go | 2 +- components/cli/command/secret/utils.go | 2 +- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/components/cli/command/container/attach.go b/components/cli/command/container/attach.go index a1fe58dea7..31bb109344 100644 --- a/components/cli/command/container/attach.go +++ b/components/cli/command/container/attach.go @@ -118,7 +118,7 @@ func runAttach(dockerCli *command.DockerCli, opts *attachOptions) error { return errAttach } - _, status, err := getExitCode(dockerCli, ctx, opts.container) + _, status, err := getExitCode(ctx, dockerCli, opts.container) if err != nil { return err } diff --git a/components/cli/command/container/run.go b/components/cli/command/container/run.go index 2f1181659d..0fad93e688 100644 --- a/components/cli/command/container/run.go +++ b/components/cli/command/container/run.go @@ -211,7 +211,7 @@ func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions }) } - statusChan := waitExitOrRemoved(dockerCli, ctx, createResponse.ID, hostConfig.AutoRemove) + statusChan := waitExitOrRemoved(ctx, dockerCli, createResponse.ID, hostConfig.AutoRemove) //start the container if err := client.ContainerStart(ctx, createResponse.ID, types.ContainerStartOptions{}); err != nil { diff --git a/components/cli/command/container/start.go b/components/cli/command/container/start.go index 77bb9ddb93..3521a41949 100644 --- a/components/cli/command/container/start.go +++ b/components/cli/command/container/start.go @@ -111,7 +111,7 @@ func runStart(dockerCli *command.DockerCli, opts *startOptions) error { // 3. We should open a channel for receiving status code of the container // no matter it's detached, removed on daemon side(--rm) or exit normally. - statusChan := waitExitOrRemoved(dockerCli, ctx, c.ID, c.HostConfig.AutoRemove) + statusChan := waitExitOrRemoved(ctx, dockerCli, c.ID, c.HostConfig.AutoRemove) startOptions := types.ContainerStartOptions{ CheckpointID: opts.checkpoint, CheckpointDir: opts.checkpointDir, diff --git a/components/cli/command/container/stats.go b/components/cli/command/container/stats.go index 5e743a4832..12d5c68522 100644 --- a/components/cli/command/container/stats.go +++ b/components/cli/command/container/stats.go @@ -108,7 +108,7 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error { s := formatter.NewContainerStats(container.ID[:12], daemonOSType) if cStats.add(s) { waitFirst.Add(1) - go collect(s, ctx, dockerCli.Client(), !opts.noStream, waitFirst) + go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst) } } } @@ -125,7 +125,7 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error { s := formatter.NewContainerStats(e.ID[:12], daemonOSType) if cStats.add(s) { waitFirst.Add(1) - go collect(s, ctx, dockerCli.Client(), !opts.noStream, waitFirst) + go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst) } } }) @@ -134,7 +134,7 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error { s := formatter.NewContainerStats(e.ID[:12], daemonOSType) if cStats.add(s) { waitFirst.Add(1) - go collect(s, ctx, dockerCli.Client(), !opts.noStream, waitFirst) + go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst) } }) @@ -160,7 +160,7 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error { s := formatter.NewContainerStats(name, daemonOSType) if cStats.add(s) { waitFirst.Add(1) - go collect(s, ctx, dockerCli.Client(), !opts.noStream, waitFirst) + go collect(ctx, s, dockerCli.Client(), !opts.noStream, waitFirst) } } diff --git a/components/cli/command/container/stats_helpers.go b/components/cli/command/container/stats_helpers.go index 8bc537ad3c..4b57e3fe05 100644 --- a/components/cli/command/container/stats_helpers.go +++ b/components/cli/command/container/stats_helpers.go @@ -53,7 +53,7 @@ func (s *stats) isKnownContainer(cid string) (int, bool) { return -1, false } -func collect(s *formatter.ContainerStats, ctx context.Context, cli client.APIClient, streamStats bool, waitFirst *sync.WaitGroup) { +func collect(ctx context.Context, s *formatter.ContainerStats, cli client.APIClient, streamStats bool, waitFirst *sync.WaitGroup) { logrus.Debugf("collecting stats for %s", s.Container) var ( getFirst bool diff --git a/components/cli/command/container/utils.go b/components/cli/command/container/utils.go index d8a8ef3555..6161e07140 100644 --- a/components/cli/command/container/utils.go +++ b/components/cli/command/container/utils.go @@ -13,7 +13,7 @@ import ( clientapi "github.com/docker/docker/client" ) -func waitExitOrRemoved(dockerCli *command.DockerCli, ctx context.Context, containerID string, waitRemove bool) chan int { +func waitExitOrRemoved(ctx context.Context, dockerCli *command.DockerCli, containerID string, waitRemove bool) chan int { if len(containerID) == 0 { // containerID can never be empty panic("Internal Error: waitExitOrRemoved needs a containerID as parameter") @@ -87,7 +87,7 @@ func waitExitOrRemoved(dockerCli *command.DockerCli, ctx context.Context, contai // getExitCode performs an inspect on the container. It returns // the running state and the exit code. -func getExitCode(dockerCli *command.DockerCli, ctx context.Context, containerID string) (bool, int, error) { +func getExitCode(ctx context.Context, dockerCli *command.DockerCli, containerID string) (bool, int, error) { c, err := dockerCli.Client().ContainerInspect(ctx, containerID) if err != nil { // If we can't connect, then the daemon probably died. diff --git a/components/cli/command/secret/inspect.go b/components/cli/command/secret/inspect.go index ad61706b35..04a5bd8a88 100644 --- a/components/cli/command/secret/inspect.go +++ b/components/cli/command/secret/inspect.go @@ -34,7 +34,7 @@ func runSecretInspect(dockerCli *command.DockerCli, opts inspectOptions) error { ctx := context.Background() // attempt to lookup secret by name - secrets, err := getSecretsByName(client, ctx, []string{opts.name}) + secrets, err := getSecretsByName(ctx, client, []string{opts.name}) if err != nil { return err } diff --git a/components/cli/command/secret/remove.go b/components/cli/command/secret/remove.go index 0ee6d9f574..44a71ef013 100644 --- a/components/cli/command/secret/remove.go +++ b/components/cli/command/secret/remove.go @@ -32,7 +32,7 @@ func runSecretRemove(dockerCli *command.DockerCli, opts removeOptions) error { ctx := context.Background() // attempt to lookup secret by name - secrets, err := getSecretsByName(client, ctx, opts.ids) + secrets, err := getSecretsByName(ctx, client, opts.ids) if err != nil { return err } diff --git a/components/cli/command/secret/utils.go b/components/cli/command/secret/utils.go index 621e60aaaa..c6e3cb61a2 100644 --- a/components/cli/command/secret/utils.go +++ b/components/cli/command/secret/utils.go @@ -8,7 +8,7 @@ import ( "golang.org/x/net/context" ) -func getSecretsByName(client client.APIClient, ctx context.Context, names []string) ([]swarm.Secret, error) { +func getSecretsByName(ctx context.Context, client client.APIClient, names []string) ([]swarm.Secret, error) { args := filters.NewArgs() for _, n := range names { args.Add("names", n) From 2036ffc43b7bb8fc59a45c0b77e0951a82a72154 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Sat, 12 Nov 2016 11:10:27 -0500 Subject: [PATCH 371/978] Fix issue with missing fields for `ps` template Signed-off-by: Brian Goff Upstream-commit: d3169abc3b0604a52d42549f1359686473b58d4f Component: cli --- components/cli/command/container/list.go | 13 +++++----- .../cli/command/formatter/container_test.go | 26 +++++++++++++++++++ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/components/cli/command/container/list.go b/components/cli/command/container/list.go index 80de7c5ff4..b4cdfa2eb3 100644 --- a/components/cli/command/container/list.go +++ b/components/cli/command/container/list.go @@ -62,6 +62,12 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command { type preProcessor struct { types.Container opts *types.ContainerListOptions + + // Fields that need to exist so the template doesn't error out + // These are needed since they are available on the final object but are not + // fields in types.Container + // TODO(cpuguy83): this seems rather broken + Networks, CreatedAt, RunningFor bool } // Size sets the size option when called by a template execution. @@ -70,13 +76,6 @@ func (p *preProcessor) Size() bool { return true } -// Networks does nothing but return true. -// It is needed to avoid the template check to fail as this field -// doesn't exist in `types.Container` -func (p *preProcessor) Networks() bool { - return true -} - func buildContainerListOptions(opts *psOptions) (*types.ContainerListOptions, error) { options := &types.ContainerListOptions{ All: opts.all, diff --git a/components/cli/command/formatter/container_test.go b/components/cli/command/formatter/container_test.go index 0a844efb65..cdfc911a94 100644 --- a/components/cli/command/formatter/container_test.go +++ b/components/cli/command/formatter/container_test.go @@ -370,3 +370,29 @@ func TestContainerContextWriteJSONField(t *testing.T) { assert.Equal(t, s, containers[i].ID) } } + +func TestContainerBackCompat(t *testing.T) { + containers := []types.Container{types.Container{ID: "brewhaha"}} + cases := []string{ + "ID", + "Names", + "Image", + "Command", + "CreatedAt", + "RunningFor", + "Ports", + "Status", + "Size", + "Labels", + "Mounts", + } + buf := bytes.NewBuffer(nil) + for _, c := range cases { + ctx := Context{Format: Format(fmt.Sprintf("{{ .%s }}", c)), Output: buf} + if err := ContainerWrite(ctx, containers); err != nil { + t.Log("could not render template for field '%s': %v", c, err) + t.Fail() + } + buf.Reset() + } +} From 27d5ed0e7aef58e694647e2ee81b0bdea37bd2da Mon Sep 17 00:00:00 2001 From: wefine Date: Mon, 14 Nov 2016 17:01:17 +0800 Subject: [PATCH 372/978] fix t.Errorf to t.Error in serveral _test.go Signed-off-by: wefine Upstream-commit: 2eb3e2ce0ffb45fbb3698b8bb618ec90f73f6890 Component: cli --- components/cli/client_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/client_test.go b/components/cli/client_test.go index ee199c2bec..3a6575c9cc 100644 --- a/components/cli/client_test.go +++ b/components/cli/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 e1b7e9bf69a28f0a92b1c2cdb631ec639c34b62f Mon Sep 17 00:00:00 2001 From: Anusha Ragunathan Date: Mon, 14 Nov 2016 08:38:06 -0800 Subject: [PATCH 373/978] Add docs for plugin push Signed-off-by: Anusha Ragunathan Upstream-commit: f1598f8b828b67ccfd3269bffd267c3d4dabb806 Component: cli --- components/cli/command/plugin/push.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/command/plugin/push.go b/components/cli/command/plugin/push.go index 4e176bea3e..e37a0483a6 100644 --- a/components/cli/command/plugin/push.go +++ b/components/cli/command/plugin/push.go @@ -14,8 +14,8 @@ import ( func newPushCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := &cobra.Command{ - Use: "push PLUGIN", - Short: "Push a plugin", + Use: "push NAME[:TAG]", + Short: "Push a plugin to a registry", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { return runPush(dockerCli, args[0]) From b930443d65f1cbc5a526f49b33f10139e135a457 Mon Sep 17 00:00:00 2001 From: John Howard Date: Fri, 11 Nov 2016 11:51:26 -0800 Subject: [PATCH 374/978] Bump API to v1.26 Signed-off-by: John Howard Upstream-commit: b5b095c01a74c0de4808a24d3a460fe45bcfb77a Component: cli --- components/cli/client.go | 4 ++-- components/cli/request.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/cli/client.go b/components/cli/client.go index 76a1ac07c0..814c537c65 100644 --- a/components/cli/client.go +++ b/components/cli/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/components/cli/request.go b/components/cli/request.go index ac05363655..f15e380339 100644 --- a/components/cli/request.go +++ b/components/cli/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 0112a67d6b1b8f8cdfe88fc830848c0f1df4690d Mon Sep 17 00:00:00 2001 From: John Howard Date: Tue, 1 Nov 2016 15:44:06 -0700 Subject: [PATCH 375/978] Windows: Use sequential file access Signed-off-by: John Howard Upstream-commit: 5723c85b1de7bbbaf1a9f1f627629a21a299f71e Component: cli --- components/cli/command/image/load.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/components/cli/command/image/load.go b/components/cli/command/image/load.go index 4f88faf094..988f5106e2 100644 --- a/components/cli/command/image/load.go +++ b/components/cli/command/image/load.go @@ -3,13 +3,13 @@ package image import ( "fmt" "io" - "os" "golang.org/x/net/context" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/pkg/jsonmessage" + "github.com/docker/docker/pkg/system" "github.com/spf13/cobra" ) @@ -43,7 +43,9 @@ func runLoad(dockerCli *command.DockerCli, opts loadOptions) error { var input io.Reader = dockerCli.In() if opts.input != "" { - file, err := os.Open(opts.input) + // We use system.OpenSequential to use sequential file access on Windows, avoiding + // depleting the standby list un-necessarily. On Linux, this equates to a regular os.Open. + file, err := system.OpenSequential(opts.input) if err != nil { return err } From cfe7cb61c6eb9c17e89944ad2d1d8a7be2eca8b2 Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Wed, 16 Nov 2016 13:19:45 -0800 Subject: [PATCH 376/978] Skip cli initialization for daemon command Cli initialization pings back to remote API and creates a deadlock if socket is already being listened by systemd. Signed-off-by: Tonis Tiigi Upstream-commit: 28f8f9296320580da26628fcd2f7adf46c4a033c Component: cli --- components/cli/docker.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/components/cli/docker.go b/components/cli/docker.go index 02d2918f0c..e6b5048564 100644 --- a/components/cli/docker.go +++ b/components/cli/docker.go @@ -39,6 +39,10 @@ func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command { return nil }, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + // daemon command is special, we redirect directly to another binary + if cmd.Name() == "daemon" { + return nil + } // flags must be the top-level command flags, not cmd.Flags() opts.Common.SetDefaultOptions(flags) dockerPreRun(opts) From 75b9b9e6bd92f8b98b59e800c395c7f0e03a143a Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sat, 12 Nov 2016 16:44:51 -0800 Subject: [PATCH 377/978] Use `map[string]bool` for `preProcessor` to ignore unknwon field This fix is an attempt to address the issue raised in 28339. In `docker ps`, the formatter needs to expose all fields of `types.Container` to `preProcessor` so that template could be executed. This direct exposing is unreliable and could cause issues as user may incorrectly assume all fields in `types.Container` will be available for templating. However, the purpose of `preProcessor` is to only find out if `.Size` is defined (so that opts.size could be set accordingly). This fix defines `preProcessor` as `map[string]bool` with a func `Size()`. In this way, any unknown fields will be ignored. This fix adds several test cases to the existing `TestBuildContainerListOptions`. This fix fixes 28339. Signed-off-by: Yong Tang Upstream-commit: 3c564598013e4df79d210ef93b732c162d70ed65 Component: cli --- components/cli/command/container/list.go | 32 ++++++------- components/cli/command/container/ps_test.go | 51 +++++++++++++++++++++ 2 files changed, 66 insertions(+), 17 deletions(-) diff --git a/components/cli/command/container/list.go b/components/cli/command/container/list.go index b4cdfa2eb3..60c2462986 100644 --- a/components/cli/command/container/list.go +++ b/components/cli/command/container/list.go @@ -59,20 +59,18 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command { return &cmd } -type preProcessor struct { - types.Container - opts *types.ContainerListOptions +// listOptionsProcessor is used to set any container list options which may only +// be embedded in the format template. +// This is passed directly into tmpl.Execute in order to allow the preprocessor +// to set any list options that were not provided by flags (e.g. `.Size`). +// It is using a `map[string]bool` so that unknown fields passed into the +// template format do not cause errors. These errors will get picked up when +// running through the actual template processor. +type listOptionsProcessor map[string]bool - // Fields that need to exist so the template doesn't error out - // These are needed since they are available on the final object but are not - // fields in types.Container - // TODO(cpuguy83): this seems rather broken - Networks, CreatedAt, RunningFor bool -} - -// Size sets the size option when called by a template execution. -func (p *preProcessor) Size() bool { - p.opts.Size = true +// Size sets the size of the map when called by a template execution. +func (o listOptionsProcessor) Size() bool { + o["size"] = true return true } @@ -88,20 +86,20 @@ func buildContainerListOptions(opts *psOptions) (*types.ContainerListOptions, er options.Limit = 1 } - // Currently only used with Size, so we can determine if the user - // put {{.Size}} in their format. - pre := &preProcessor{opts: options} tmpl, err := templates.Parse(opts.format) if err != nil { return nil, err } + optionsProcessor := listOptionsProcessor{} // This shouldn't error out but swallowing the error makes it harder // to track down if preProcessor issues come up. Ref #24696 - if err := tmpl.Execute(ioutil.Discard, pre); err != nil { + if err := tmpl.Execute(ioutil.Discard, optionsProcessor); err != nil { return nil, err } + // At the moment all we need is to capture .Size for preprocessor + options.Size = opts.size || optionsProcessor["size"] return options, nil } diff --git a/components/cli/command/container/ps_test.go b/components/cli/command/container/ps_test.go index 9df4dfd5fa..62b0545274 100644 --- a/components/cli/command/container/ps_test.go +++ b/components/cli/command/container/ps_test.go @@ -46,6 +46,57 @@ func TestBuildContainerListOptions(t *testing.T) { expectedLimit: 1, expectedFilters: make(map[string]string), }, + { + psOpts: &psOptions{ + all: true, + size: false, + last: 5, + filter: filters, + // With .Size, size should be true + format: "{{.Size}}", + }, + expectedAll: true, + expectedSize: true, + expectedLimit: 5, + expectedFilters: map[string]string{ + "foo": "bar", + "baz": "foo", + }, + }, + { + psOpts: &psOptions{ + all: true, + size: false, + last: 5, + filter: filters, + // With .Size, size should be true + format: "{{.Size}} {{.CreatedAt}} {{.Networks}}", + }, + expectedAll: true, + expectedSize: true, + expectedLimit: 5, + expectedFilters: map[string]string{ + "foo": "bar", + "baz": "foo", + }, + }, + { + psOpts: &psOptions{ + all: true, + size: false, + last: 5, + filter: filters, + // Without .Size, size should be false + format: "{{.CreatedAt}} {{.Networks}}", + }, + expectedAll: true, + expectedSize: false, + expectedLimit: 5, + expectedFilters: map[string]string{ + "foo": "bar", + "baz": "foo", + }, + }, } for _, c := range contexts { From 656a0c42be3e4b240ad2aa02d489b5fa0554c695 Mon Sep 17 00:00:00 2001 From: Anusha Ragunathan Date: Wed, 16 Nov 2016 14:42:46 -0800 Subject: [PATCH 378/978] 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 Upstream-commit: 4749582510fdda9189ecb582b131945fb1b1297b Component: cli --- components/cli/plugin_install.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/components/cli/plugin_install.go b/components/cli/plugin_install.go index d0a3d517fc..407f1cddf2 100644 --- a/components/cli/plugin_install.go +++ b/components/cli/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 7899bc9ada35f5450e08813b94fa570b2c3c7381 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 16 Nov 2016 16:46:31 -0800 Subject: [PATCH 379/978] Fix crash caused by `docker service inspect --pretty` This fix tries to fix the crash caused by `docker service inspect --pretty`, by performing necessary nil pointer check. Signed-off-by: Yong Tang Upstream-commit: 4488d9f9fb416c9fe90acb774d1cd913fec80c22 Component: cli --- components/cli/command/formatter/service.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/components/cli/command/formatter/service.go b/components/cli/command/formatter/service.go index 1549047b72..aaa78386cb 100644 --- a/components/cli/command/formatter/service.go +++ b/components/cli/command/formatter/service.go @@ -263,6 +263,9 @@ func (ctx *serviceInspectContext) HasResources() bool { } func (ctx *serviceInspectContext) HasResourceReservations() bool { + if ctx.Service.Spec.TaskTemplate.Resources == nil || ctx.Service.Spec.TaskTemplate.Resources.Reservations == nil { + return false + } return ctx.Service.Spec.TaskTemplate.Resources.Reservations.NanoCPUs > 0 || ctx.Service.Spec.TaskTemplate.Resources.Reservations.MemoryBytes > 0 } @@ -281,6 +284,9 @@ func (ctx *serviceInspectContext) ResourceReservationMemory() string { } func (ctx *serviceInspectContext) HasResourceLimits() bool { + if ctx.Service.Spec.TaskTemplate.Resources == nil || ctx.Service.Spec.TaskTemplate.Resources.Limits == nil { + return false + } return ctx.Service.Spec.TaskTemplate.Resources.Limits.NanoCPUs > 0 || ctx.Service.Spec.TaskTemplate.Resources.Limits.MemoryBytes > 0 } From 7e9ea5342a96b455a5a0353a7addb261ef3b3d76 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 16 Nov 2016 22:17:40 -0800 Subject: [PATCH 380/978] Fix several issues with `go vet` and `go fmt` For some reason, `go vet` and `go fmt` validate does not capture several issues. The following was the output of `go vet`: ``` ubuntu@ubuntu:~/docker$ go vet ./... 2>&1 | grep -v ^vendor | grep -v '^exit status 1$' cli/command/formatter/container_test.go:393: possible formatting directive in Log call volume/volume_test.go:257: arg mp.RW for printf verb %s of wrong type: bool ``` The following was the output of `go fmt -s`: ``` ubuntu@ubuntu:~/docker$ gofmt -s -l . | grep -v ^vendor cli/command/stack/list.go daemon/commit.go ``` Fixed above issues with `go vet` and `go fmt -s` Signed-off-by: Yong Tang Upstream-commit: d691bce8c9c849aeb52521c0c174d8aa3707dbaa Component: cli --- components/cli/command/formatter/container_test.go | 4 ++-- components/cli/command/stack/list.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/cli/command/formatter/container_test.go b/components/cli/command/formatter/container_test.go index cdfc911a94..16137897b9 100644 --- a/components/cli/command/formatter/container_test.go +++ b/components/cli/command/formatter/container_test.go @@ -372,7 +372,7 @@ func TestContainerContextWriteJSONField(t *testing.T) { } func TestContainerBackCompat(t *testing.T) { - containers := []types.Container{types.Container{ID: "brewhaha"}} + containers := []types.Container{{ID: "brewhaha"}} cases := []string{ "ID", "Names", @@ -390,7 +390,7 @@ func TestContainerBackCompat(t *testing.T) { for _, c := range cases { ctx := Context{Format: Format(fmt.Sprintf("{{ .%s }}", c)), Output: buf} if err := ContainerWrite(ctx, containers); err != nil { - t.Log("could not render template for field '%s': %v", c, err) + t.Logf("could not render template for field '%s': %v", c, err) t.Fail() } buf.Reset() diff --git a/components/cli/command/stack/list.go b/components/cli/command/stack/list.go index 7be42525dd..f655b929ad 100644 --- a/components/cli/command/stack/list.go +++ b/components/cli/command/stack/list.go @@ -72,7 +72,7 @@ func printTable(out io.Writer, stacks []*stack) { type stack struct { // Name is the name of the stack - Name string + Name string // Services is the number of the services Services int } From 7487e3205444f39714764f0aaa5c1ca5fd7fc285 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Thu, 17 Nov 2016 10:54:10 -0800 Subject: [PATCH 381/978] refactor help func in CLI Signed-off-by: Victor Vieux Upstream-commit: 55908f8a82d8fe88d3377df6d8b8dd2d03e46143 Component: cli --- components/cli/command/checkpoint/cmd.go | 7 ++----- components/cli/command/cli.go | 8 ++++++++ components/cli/command/container/cmd.go | 5 +---- components/cli/command/image/cmd.go | 5 +---- components/cli/command/network/cmd.go | 5 +---- components/cli/command/node/cmd.go | 5 +---- components/cli/command/plugin/cmd.go | 5 +---- components/cli/command/secret/cmd.go | 6 +----- components/cli/command/service/cmd.go | 5 +---- components/cli/command/stack/cmd.go | 7 ++----- components/cli/command/swarm/cmd.go | 5 +---- components/cli/command/system/cmd.go | 5 +---- components/cli/command/volume/cmd.go | 5 +---- 13 files changed, 22 insertions(+), 51 deletions(-) diff --git a/components/cli/command/checkpoint/cmd.go b/components/cli/command/checkpoint/cmd.go index f186232a4d..d5705a4dad 100644 --- a/components/cli/command/checkpoint/cmd.go +++ b/components/cli/command/checkpoint/cmd.go @@ -12,11 +12,8 @@ func NewCheckpointCommand(dockerCli *command.DockerCli) *cobra.Command { Use: "checkpoint", Short: "Manage checkpoints", Args: cli.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - cmd.SetOutput(dockerCli.Err()) - cmd.HelpFunc()(cmd, args) - }, - Tags: map[string]string{"experimental": "", "version": "1.25"}, + RunE: dockerCli.ShowHelp, + Tags: map[string]string{"experimental": "", "version": "1.25"}, } cmd.AddCommand( newCreateCommand(dockerCli), diff --git a/components/cli/command/cli.go b/components/cli/command/cli.go index ef9de2edf1..99ea6331af 100644 --- a/components/cli/command/cli.go +++ b/components/cli/command/cli.go @@ -20,6 +20,7 @@ import ( dopts "github.com/docker/docker/opts" "github.com/docker/go-connections/sockets" "github.com/docker/go-connections/tlsconfig" + "github.com/spf13/cobra" "golang.org/x/net/context" ) @@ -73,6 +74,13 @@ func (cli *DockerCli) In() *InStream { return cli.in } +// ShowHelp shows the command help. +func (cli *DockerCli) ShowHelp(cmd *cobra.Command, args []string) error { + cmd.SetOutput(cli.err) + cmd.HelpFunc()(cmd, args) + return nil +} + // ConfigFile returns the ConfigFile func (cli *DockerCli) ConfigFile() *configfile.ConfigFile { return cli.configFile diff --git a/components/cli/command/container/cmd.go b/components/cli/command/container/cmd.go index 075f936bd9..3e9b4880ac 100644 --- a/components/cli/command/container/cmd.go +++ b/components/cli/command/container/cmd.go @@ -13,10 +13,7 @@ func NewContainerCommand(dockerCli *command.DockerCli) *cobra.Command { Use: "container", Short: "Manage containers", Args: cli.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - cmd.SetOutput(dockerCli.Err()) - cmd.HelpFunc()(cmd, args) - }, + RunE: dockerCli.ShowHelp, } cmd.AddCommand( NewAttachCommand(dockerCli), diff --git a/components/cli/command/image/cmd.go b/components/cli/command/image/cmd.go index dc98257438..c3ca61f85b 100644 --- a/components/cli/command/image/cmd.go +++ b/components/cli/command/image/cmd.go @@ -13,10 +13,7 @@ func NewImageCommand(dockerCli *command.DockerCli) *cobra.Command { Use: "image", Short: "Manage images", Args: cli.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - cmd.SetOutput(dockerCli.Err()) - cmd.HelpFunc()(cmd, args) - }, + RunE: dockerCli.ShowHelp, } cmd.AddCommand( NewBuildCommand(dockerCli), diff --git a/components/cli/command/network/cmd.go b/components/cli/command/network/cmd.go index c2a7e83dd8..ab8393cded 100644 --- a/components/cli/command/network/cmd.go +++ b/components/cli/command/network/cmd.go @@ -13,10 +13,7 @@ func NewNetworkCommand(dockerCli *command.DockerCli) *cobra.Command { Use: "network", Short: "Manage networks", Args: cli.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - cmd.SetOutput(dockerCli.Err()) - cmd.HelpFunc()(cmd, args) - }, + RunE: dockerCli.ShowHelp, } cmd.AddCommand( newConnectCommand(dockerCli), diff --git a/components/cli/command/node/cmd.go b/components/cli/command/node/cmd.go index d70ee81789..e71b9199ad 100644 --- a/components/cli/command/node/cmd.go +++ b/components/cli/command/node/cmd.go @@ -14,10 +14,7 @@ func NewNodeCommand(dockerCli *command.DockerCli) *cobra.Command { Use: "node", Short: "Manage Swarm nodes", Args: cli.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - cmd.SetOutput(dockerCli.Err()) - cmd.HelpFunc()(cmd, args) - }, + RunE: dockerCli.ShowHelp, } cmd.AddCommand( newDemoteCommand(dockerCli), diff --git a/components/cli/command/plugin/cmd.go b/components/cli/command/plugin/cmd.go index 6e1160fd91..2173943f89 100644 --- a/components/cli/command/plugin/cmd.go +++ b/components/cli/command/plugin/cmd.go @@ -12,10 +12,7 @@ func NewPluginCommand(dockerCli *command.DockerCli) *cobra.Command { Use: "plugin", Short: "Manage plugins", Args: cli.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - cmd.SetOutput(dockerCli.Err()) - cmd.HelpFunc()(cmd, args) - }, + RunE: dockerCli.ShowHelp, } cmd.AddCommand( diff --git a/components/cli/command/secret/cmd.go b/components/cli/command/secret/cmd.go index 995300ad77..79e669858c 100644 --- a/components/cli/command/secret/cmd.go +++ b/components/cli/command/secret/cmd.go @@ -1,8 +1,6 @@ package secret import ( - "fmt" - "github.com/spf13/cobra" "github.com/docker/docker/cli" @@ -15,9 +13,7 @@ func NewSecretCommand(dockerCli *command.DockerCli) *cobra.Command { Use: "secret", Short: "Manage Docker secrets", Args: cli.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString()) - }, + RunE: dockerCli.ShowHelp, } cmd.AddCommand( newSecretListCommand(dockerCli), diff --git a/components/cli/command/service/cmd.go b/components/cli/command/service/cmd.go index 63f2db7171..796fe926c3 100644 --- a/components/cli/command/service/cmd.go +++ b/components/cli/command/service/cmd.go @@ -13,10 +13,7 @@ func NewServiceCommand(dockerCli *command.DockerCli) *cobra.Command { Use: "service", Short: "Manage services", Args: cli.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - cmd.SetOutput(dockerCli.Err()) - cmd.HelpFunc()(cmd, args) - }, + RunE: dockerCli.ShowHelp, } cmd.AddCommand( newCreateCommand(dockerCli), diff --git a/components/cli/command/stack/cmd.go b/components/cli/command/stack/cmd.go index ff71e0ddfa..8626dc7fe4 100644 --- a/components/cli/command/stack/cmd.go +++ b/components/cli/command/stack/cmd.go @@ -12,11 +12,8 @@ func NewStackCommand(dockerCli *command.DockerCli) *cobra.Command { Use: "stack", Short: "Manage Docker stacks", Args: cli.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - cmd.SetOutput(dockerCli.Err()) - cmd.HelpFunc()(cmd, args) - }, - Tags: map[string]string{"experimental": "", "version": "1.25"}, + RunE: dockerCli.ShowHelp, + Tags: map[string]string{"experimental": "", "version": "1.25"}, } cmd.AddCommand( newDeployCommand(dockerCli), diff --git a/components/cli/command/swarm/cmd.go b/components/cli/command/swarm/cmd.go index 6c70459df0..632679c4b6 100644 --- a/components/cli/command/swarm/cmd.go +++ b/components/cli/command/swarm/cmd.go @@ -13,10 +13,7 @@ func NewSwarmCommand(dockerCli *command.DockerCli) *cobra.Command { Use: "swarm", Short: "Manage Swarm", Args: cli.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - cmd.SetOutput(dockerCli.Err()) - cmd.HelpFunc()(cmd, args) - }, + RunE: dockerCli.ShowHelp, } cmd.AddCommand( newInitCommand(dockerCli), diff --git a/components/cli/command/system/cmd.go b/components/cli/command/system/cmd.go index 9cd74b5d4b..ab3beb895a 100644 --- a/components/cli/command/system/cmd.go +++ b/components/cli/command/system/cmd.go @@ -13,10 +13,7 @@ func NewSystemCommand(dockerCli *command.DockerCli) *cobra.Command { Use: "system", Short: "Manage Docker", Args: cli.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - cmd.SetOutput(dockerCli.Err()) - cmd.HelpFunc()(cmd, args) - }, + RunE: dockerCli.ShowHelp, } cmd.AddCommand( NewEventsCommand(dockerCli), diff --git a/components/cli/command/volume/cmd.go b/components/cli/command/volume/cmd.go index 39e4b7f46e..40862f29d1 100644 --- a/components/cli/command/volume/cmd.go +++ b/components/cli/command/volume/cmd.go @@ -14,10 +14,7 @@ func NewVolumeCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Manage volumes", Long: volumeDescription, Args: cli.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - cmd.SetOutput(dockerCli.Err()) - cmd.HelpFunc()(cmd, args) - }, + RunE: dockerCli.ShowHelp, } cmd.AddCommand( newCreateCommand(dockerCli), From 3164628a336a5d60e94d49e4173b9db25bc4bd8b Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Thu, 17 Nov 2016 10:54:10 -0800 Subject: [PATCH 382/978] refactor help func in CLI Signed-off-by: Victor Vieux Upstream-commit: 05ddb16e5913ae92814dd21d43bd79de9496941a Component: cli --- components/cli/docker.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/components/cli/docker.go b/components/cli/docker.go index e6b5048564..1e07cc8d7b 100644 --- a/components/cli/docker.go +++ b/components/cli/docker.go @@ -34,9 +34,7 @@ func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command { showVersion() return nil } - cmd.SetOutput(dockerCli.Err()) - cmd.HelpFunc()(cmd, args) - return nil + return dockerCli.ShowHelp(cmd, args) }, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { // daemon command is special, we redirect directly to another binary From fb2ded55b63866b60b71e29753badb0f162bd3c8 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Tue, 15 Nov 2016 10:04:36 -0500 Subject: [PATCH 383/978] do not force target type for secret references Signed-off-by: Evan Hazlett use secret store interface instead of embedded secret data into container Signed-off-by: Evan Hazlett Upstream-commit: bc542f365c3534cbf086a55bf65dadcf7f87be91 Component: cli --- components/cli/command/service/parse.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/components/cli/command/service/parse.go b/components/cli/command/service/parse.go index 368bc6d449..ff3249e581 100644 --- a/components/cli/command/service/parse.go +++ b/components/cli/command/service/parse.go @@ -17,19 +17,19 @@ func parseSecrets(client client.APIClient, requestedSecrets []*types.SecretReque ctx := context.Background() for _, secret := range requestedSecrets { + if _, exists := secretRefs[secret.Target]; exists { + return nil, fmt.Errorf("duplicate secret target for %s not allowed", secret.Source) + } secretRef := &swarmtypes.SecretReference{ - SecretName: secret.Source, - Target: &swarmtypes.SecretReferenceFileTarget{ + File: &swarmtypes.SecretReferenceFileTarget{ Name: secret.Target, UID: secret.UID, GID: secret.GID, Mode: secret.Mode, }, + SecretName: secret.Source, } - if _, exists := secretRefs[secret.Target]; exists { - return nil, fmt.Errorf("duplicate secret target for %s not allowed", secret.Source) - } secretRefs[secret.Target] = secretRef } From c0c99dbe131ef694ae2329b2f76227fe15621e38 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Wed, 16 Nov 2016 16:38:28 -0800 Subject: [PATCH 384/978] error on cli when trying to use experimental feature with non experimental daemon Signed-off-by: Victor Vieux Upstream-commit: 1ab47a8be8a106dc68cbae3b2d019f9e77870a1f Component: cli --- components/cli/docker.go | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/components/cli/docker.go b/components/cli/docker.go index 1e07cc8d7b..d82c2526f4 100644 --- a/components/cli/docker.go +++ b/components/cli/docker.go @@ -1,6 +1,7 @@ package main import ( + "errors" "fmt" "os" @@ -44,19 +45,27 @@ func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command { // flags must be the top-level command flags, not cmd.Flags() opts.Common.SetDefaultOptions(flags) dockerPreRun(opts) - return dockerCli.Initialize(opts) + if err := dockerCli.Initialize(opts); err != nil { + return err + } + return isSupported(cmd, dockerCli.Client().ClientVersion(), dockerCli.HasExperimental()) }, } cli.SetupRootCommand(cmd) cmd.SetHelpFunc(func(ccmd *cobra.Command, args []string) { - if dockerCli.Client() == nil { + if dockerCli.Client() == nil { // when using --help, PersistenPreRun is not called, so initialization is needed. // flags must be the top-level command flags, not cmd.Flags() opts.Common.SetDefaultOptions(flags) dockerPreRun(opts) dockerCli.Initialize(opts) } + if err := isSupported(ccmd, dockerCli.Client().ClientVersion(), dockerCli.HasExperimental()); err != nil { + ccmd.Println(err) + return + } + hideUnsupportedFeatures(ccmd, dockerCli.Client().ClientVersion(), dockerCli.HasExperimental()) if err := ccmd.Help(); err != nil { @@ -155,3 +164,17 @@ func hideUnsupportedFeatures(cmd *cobra.Command, clientVersion string, hasExperi } } } + +func isSupported(cmd *cobra.Command, clientVersion string, hasExperimental bool) error { + if !hasExperimental { + if _, ok := cmd.Tags["experimental"]; ok { + return errors.New("only supported with experimental daemon") + } + } + + if cmdVersion, ok := cmd.Tags["version"]; ok && versions.LessThan(clientVersion, cmdVersion) { + return fmt.Errorf("only supported with daemon version >= %s", cmdVersion) + } + + return nil +} From f186f103bcd2e6b07bba21cd458ad6d5fd0d3aa9 Mon Sep 17 00:00:00 2001 From: allencloud Date: Thu, 17 Nov 2016 10:51:16 +0800 Subject: [PATCH 385/978] update secret create url for consistency Signed-off-by: allencloud Upstream-commit: 42788cad9c1a56fe3eb613437d8a561b57032074 Component: cli --- components/cli/secret_create.go | 2 +- components/cli/secret_create_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/cli/secret_create.go b/components/cli/secret_create.go index f92a3d1510..de8b041567 100644 --- a/components/cli/secret_create.go +++ b/components/cli/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/components/cli/secret_create_test.go b/components/cli/secret_create_test.go index b7def89d0e..cb378c77ff 100644 --- a/components/cli/secret_create_test.go +++ b/components/cli/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 33b6dcd341db4ce47104a6beeeb1cbc33afe5388 Mon Sep 17 00:00:00 2001 From: lixiaobing10051267 Date: Thu, 17 Nov 2016 15:50:38 +0800 Subject: [PATCH 386/978] expected new_container_id while testing ContainerCommit Signed-off-by: lixiaobing10051267 Upstream-commit: e98be4c62f80ba0d240616ce1cdcd15ac3d30561 Component: cli --- components/cli/container_commit_test.go | 2 +- components/cli/container_copy_test.go | 2 +- components/cli/container_inspect_test.go | 8 ++++---- components/cli/image_import_test.go | 2 +- components/cli/network_inspect_test.go | 2 +- components/cli/plugin_remove_test.go | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/components/cli/container_commit_test.go b/components/cli/container_commit_test.go index a844675368..6947ed3861 100644 --- a/components/cli/container_commit_test.go +++ b/components/cli/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/components/cli/container_copy_test.go b/components/cli/container_copy_test.go index 7eded611fd..6863cfba20 100644 --- a/components/cli/container_copy_test.go +++ b/components/cli/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/components/cli/container_inspect_test.go b/components/cli/container_inspect_test.go index f1a6f4ac7d..98f83bd8db 100644 --- a/components/cli/container_inspect_test.go +++ b/components/cli/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/components/cli/image_import_test.go b/components/cli/image_import_test.go index e309be74e6..370ad5fbed 100644 --- a/components/cli/image_import_test.go +++ b/components/cli/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/components/cli/network_inspect_test.go b/components/cli/network_inspect_test.go index 1f926d66ba..55f04eca2c 100644 --- a/components/cli/network_inspect_test.go +++ b/components/cli/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/components/cli/plugin_remove_test.go b/components/cli/plugin_remove_test.go index a15f1661f6..b2d515793a 100644 --- a/components/cli/plugin_remove_test.go +++ b/components/cli/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 bdac0235214c52e8149b0ec9fec6b985c1bba449 Mon Sep 17 00:00:00 2001 From: Boaz Shuster Date: Sun, 13 Nov 2016 10:28:25 +0200 Subject: [PATCH 387/978] Change the docker-tag usage text to be clearer Signed-off-by: Boaz Shuster Upstream-commit: cc36bf62efbde2232d859e4ee08c78ae442fc0fc Component: cli --- components/cli/command/image/tag.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/command/image/tag.go b/components/cli/command/image/tag.go index b88789b0f8..fb2b703856 100644 --- a/components/cli/command/image/tag.go +++ b/components/cli/command/image/tag.go @@ -18,8 +18,8 @@ func NewTagCommand(dockerCli *command.DockerCli) *cobra.Command { var opts tagOptions cmd := &cobra.Command{ - Use: "tag IMAGE[:TAG] IMAGE[:TAG]", - Short: "Tag an image into a repository", + Use: "tag SOURCE_IMAGE[:TAG] TARGET_IMAGE[:TAG]", + Short: "Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE", Args: cli.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { opts.image = args[0] From 98e2097ade91784ab3fefba1aa0c87fa162ee202 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 18 Nov 2016 15:09:13 +0100 Subject: [PATCH 388/978] Add support for healthcheck in composefile v3 `docker stack deploy` now supports a composefile v3 format that have a healthcheck. Signed-off-by: Vincent Demeester Upstream-commit: e21f4f9996b332c3a723548ac8e246c6cb79c4a8 Component: cli --- components/cli/command/stack/deploy.go | 49 ++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index 683f0cad35..13b43a78bf 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -6,6 +6,7 @@ import ( "os" "sort" "strings" + "time" "github.com/spf13/cobra" "golang.org/x/net/context" @@ -13,6 +14,7 @@ import ( "github.com/aanand/compose-file/loader" composetypes "github.com/aanand/compose-file/types" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/mount" networktypes "github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/swarm" @@ -487,6 +489,11 @@ func convertService( return swarm.ServiceSpec{}, err } + healthcheck, err := convertHealthcheck(service.HealthCheck) + if err != nil { + return swarm.ServiceSpec{}, err + } + serviceSpec := swarm.ServiceSpec{ Annotations: swarm.Annotations{ Name: name, @@ -499,6 +506,7 @@ func convertService( Args: service.Command, Hostname: service.Hostname, Hosts: convertExtraHosts(service.ExtraHosts), + Healthcheck: healthcheck, Env: convertEnvironment(service.Environment), Labels: getStackLabels(namespace.name, service.Labels), Dir: service.WorkingDir, @@ -531,6 +539,47 @@ func convertExtraHosts(extraHosts map[string]string) []string { return hosts } +func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container.HealthConfig, error) { + if healthcheck == nil { + return nil, nil + } + var ( + err error + timeout, interval time.Duration + retries int + ) + if healthcheck.Disable { + if len(healthcheck.Test) != 0 { + return nil, fmt.Errorf("command and disable key can't be set at the same time") + } + return &container.HealthConfig{ + Test: []string{"NONE"}, + }, nil + + } + if healthcheck.Timeout != "" { + timeout, err = time.ParseDuration(healthcheck.Timeout) + if err != nil { + return nil, err + } + } + if healthcheck.Interval != "" { + interval, err = time.ParseDuration(healthcheck.Interval) + if err != nil { + return nil, err + } + } + if healthcheck.Retries != nil { + retries = int(*healthcheck.Retries) + } + return &container.HealthConfig{ + Test: healthcheck.Test, + Timeout: timeout, + Interval: interval, + Retries: retries, + }, nil +} + func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (*swarm.RestartPolicy, error) { // TODO: log if restart is being ignored if source == nil { From 21435bb7d508928c5d125e00620e0adff936d8e6 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 18 Nov 2016 10:15:29 -0500 Subject: [PATCH 389/978] Default parallelism to 1. Signed-off-by: Daniel Nephin Upstream-commit: c682f10a8fda1544001d424e34a3356741a1a45f Component: cli --- components/cli/command/stack/deploy.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index 13b43a78bf..808cc7f4e5 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -615,8 +615,12 @@ func convertUpdateConfig(source *composetypes.UpdateConfig) *swarm.UpdateConfig if source == nil { return nil } + parallel := uint64(1) + if source.Parallelism != nil { + parallel = *source.Parallelism + } return &swarm.UpdateConfig{ - Parallelism: source.Parallelism, + Parallelism: parallel, Delay: source.Delay, FailureAction: source.FailureAction, Monitor: source.Monitor, From dee52b4790841ebb7a46e3f470fb98ab8d199db0 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Mon, 14 Nov 2016 18:08:24 -0800 Subject: [PATCH 390/978] 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 Upstream-commit: b866fa77f46dbe70cc87115938d209c3e03bc0e0 Component: cli --- components/cli/command/service/create.go | 4 ++++ components/cli/command/service/scale.go | 6 +++++- components/cli/command/service/update.go | 6 +++++- components/cli/command/stack/deploy.go | 9 +++++++-- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/components/cli/command/service/create.go b/components/cli/command/service/create.go index 061a36f06c..96c9f36da1 100644 --- a/components/cli/command/service/create.go +++ b/components/cli/command/service/create.go @@ -90,6 +90,10 @@ func runCreate(dockerCli *command.DockerCli, opts *serviceOptions) error { return err } + for _, warning := range response.Warnings { + fmt.Fprintln(dockerCli.Err(), warning) + } + fmt.Fprintf(dockerCli.Out(), "%s\n", response.ID) return nil } diff --git a/components/cli/command/service/scale.go b/components/cli/command/service/scale.go index ea30265bd7..cf89e90273 100644 --- a/components/cli/command/service/scale.go +++ b/components/cli/command/service/scale.go @@ -82,11 +82,15 @@ func runServiceScale(dockerCli *command.DockerCli, serviceID string, scale uint6 serviceMode.Replicated.Replicas = &scale - err = client.ServiceUpdate(ctx, service.ID, service.Version, service.Spec, types.ServiceUpdateOptions{}) + response, err := client.ServiceUpdate(ctx, service.ID, service.Version, service.Spec, types.ServiceUpdateOptions{}) if err != nil { return err } + for _, warning := range response.Warnings { + fmt.Fprintln(dockerCli.Err(), warning) + } + fmt.Fprintf(dockerCli.Out(), "%s scaled to %d\n", serviceID, scale) return nil } diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index d2639a62db..20a4fc5708 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -133,11 +133,15 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID str updateOpts.RegistryAuthFrom = types.RegistryAuthFromSpec } - err = apiClient.ServiceUpdate(ctx, service.ID, service.Version, *spec, updateOpts) + response, err := apiClient.ServiceUpdate(ctx, service.ID, service.Version, *spec, updateOpts) if err != nil { return err } + for _, warning := range response.Warnings { + fmt.Fprintln(dockerCli.Err(), warning) + } + fmt.Fprintf(dockerCli.Out(), "%s\n", serviceID) return nil } diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index 683f0cad35..63adeacd62 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -408,15 +408,20 @@ func deployServices( if sendAuth { updateOpts.EncodedRegistryAuth = encodedAuth } - if err := apiClient.ServiceUpdate( + response, err := apiClient.ServiceUpdate( ctx, service.ID, service.Version, serviceSpec, updateOpts, - ); err != nil { + ) + if err != nil { return err } + + for _, warning := range response.Warnings { + fmt.Fprintln(dockerCli.Err(), warning) + } } else { fmt.Fprintf(out, "Creating service %s\n", name) From d5535f5557b50f1bb8c097dc6674f4275edd308d Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Mon, 14 Nov 2016 18:08:24 -0800 Subject: [PATCH 391/978] 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 Upstream-commit: b58a973b1820673b328e89514b26df2bb358f016 Component: cli --- components/cli/interface.go | 2 +- components/cli/service_update.go | 11 +++++++++-- components/cli/service_update_test.go | 6 +++--- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/components/cli/interface.go b/components/cli/interface.go index 7a3ebe8b4c..d46720e6c7 100644 --- a/components/cli/interface.go +++ b/components/cli/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/components/cli/service_update.go b/components/cli/service_update.go index 8e03f7f483..afa94d47e2 100644 --- a/components/cli/service_update.go +++ b/components/cli/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/components/cli/service_update_test.go b/components/cli/service_update_test.go index 081649f492..76bea176bf 100644 --- a/components/cli/service_update_test.go +++ b/components/cli/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 71aa7e39fafdeaaf0ef776a652ced53b22a6c193 Mon Sep 17 00:00:00 2001 From: Nishant Totla Date: Wed, 16 Nov 2016 22:21:18 -0800 Subject: [PATCH 392/978] Suppressing digest for docker service ls/ps Signed-off-by: Nishant Totla Upstream-commit: 5f1209bf4b2e2a3da74c5f86c742068aeb5983d6 Component: cli --- components/cli/command/service/list.go | 13 ++++++++++++- components/cli/command/task/print.go | 15 ++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/components/cli/command/service/list.go b/components/cli/command/service/list.go index f758808d1f..724126079c 100644 --- a/components/cli/command/service/list.go +++ b/components/cli/command/service/list.go @@ -5,6 +5,7 @@ import ( "io" "text/tabwriter" + distreference "github.com/docker/distribution/reference" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/swarm" @@ -127,6 +128,16 @@ func printTable(out io.Writer, services []swarm.Service, running, tasksNoShutdow mode = "global" replicas = fmt.Sprintf("%d/%d", running[service.ID], tasksNoShutdown[service.ID]) } + image := service.Spec.TaskTemplate.ContainerSpec.Image + ref, err := distreference.ParseNamed(image) + if err == nil { + // update image string for display + namedTagged, ok := ref.(distreference.NamedTagged) + if ok { + image = namedTagged.Name() + ":" + namedTagged.Tag() + } + } + fmt.Fprintf( writer, listItemFmt, @@ -134,7 +145,7 @@ func printTable(out io.Writer, services []swarm.Service, running, tasksNoShutdow service.Spec.Name, mode, replicas, - service.Spec.TaskTemplate.ContainerSpec.Image) + image) } } diff --git a/components/cli/command/task/print.go b/components/cli/command/task/print.go index 2c5b2eecdd..2995e9afb3 100644 --- a/components/cli/command/task/print.go +++ b/components/cli/command/task/print.go @@ -10,6 +10,7 @@ import ( "golang.org/x/net/context" + distreference "github.com/docker/distribution/reference" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/idresolver" @@ -118,11 +119,23 @@ func print(out io.Writer, ctx context.Context, tasks []swarm.Task, resolver *idr taskErr = fmt.Sprintf("\"%s\"", taskErr) } + image := task.Spec.ContainerSpec.Image + if !noTrunc { + ref, err := distreference.ParseNamed(image) + if err == nil { + // update image string for display + namedTagged, ok := ref.(distreference.NamedTagged) + if ok { + image = namedTagged.Name() + ":" + namedTagged.Tag() + } + } + } + fmt.Fprintf( out, psTaskItemFmt, indentedName, - task.Spec.ContainerSpec.Image, + image, nodeValue, command.PrettyPrint(task.DesiredState), command.PrettyPrint(task.Status.State), From 607719cae67432105b3944dd70ba317d0447618b Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Fri, 18 Nov 2016 22:04:27 +0100 Subject: [PATCH 393/978] swap position of "host" and "ip" the service definition uses the format as defined in http://man7.org/linux/man-pages/man5/hosts.5.html (IP_address canonical_hostname [aliases...]) This format is the _reverse_ of the format used in the container API. Commit f32869d956eb175f88fd0b16992d2377d8eae79c inadvertently used the incorrect order. This fixes the order, and correctly sets it to; IP-Address hostname Signed-off-by: Sebastiaan van Stijn Upstream-commit: 82804cc8e510dea746c9c11365ec90890a4d88fe Component: cli --- components/cli/command/stack/deploy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index 63adeacd62..3075477b83 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -531,7 +531,7 @@ func convertService( func convertExtraHosts(extraHosts map[string]string) []string { hosts := []string{} for host, ip := range extraHosts { - hosts = append(hosts, fmt.Sprintf("%s %s", host, ip)) + hosts = append(hosts, fmt.Sprintf("%s %s", ip, host)) } return hosts } From 08da0db0bff6c4e4c795f756d724578d33da4aae Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Fri, 18 Nov 2016 15:57:11 -0800 Subject: [PATCH 394/978] fix a few golint errors Signed-off-by: Victor Vieux Upstream-commit: 40acabdfe9c937e6afce929080cf5f010cd69e87 Component: cli --- components/cli/docker.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/docker.go b/components/cli/docker.go index 1e07cc8d7b..4245f8c400 100644 --- a/components/cli/docker.go +++ b/components/cli/docker.go @@ -81,7 +81,7 @@ func noArgs(cmd *cobra.Command, args []string) error { return nil } return fmt.Errorf( - "docker: '%s' is not a docker command.\nSee 'docker --help'.", args[0]) + "docker: '%s' is not a docker command.\nSee 'docker --help'", args[0]) } func main() { From 4efd139636a4806fcbe489547b9380357667f844 Mon Sep 17 00:00:00 2001 From: Antonio Murdaca Date: Wed, 16 Nov 2016 22:30:29 +0100 Subject: [PATCH 395/978] api: types: keep info.SecurityOptions a string slice Signed-off-by: Antonio Murdaca Upstream-commit: 123d33d81dc037eebdb67a89dbce887f77ad7705 Component: cli --- components/cli/command/system/info.go | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/components/cli/command/system/info.go b/components/cli/command/system/info.go index b751bbff13..e0b8767377 100644 --- a/components/cli/command/system/info.go +++ b/components/cli/command/system/info.go @@ -172,16 +172,21 @@ func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error { fmt.Fprintf(dockerCli.Out(), "\n") } if len(info.SecurityOptions) != 0 { + kvs, err := types.DecodeSecurityOptions(info.SecurityOptions) + if err != nil { + return err + } fmt.Fprintf(dockerCli.Out(), "Security Options:\n") - for _, o := range info.SecurityOptions { - switch o.Key { - case "Name": - fmt.Fprintf(dockerCli.Out(), " %s\n", o.Value) - case "Profile": - if o.Value != "default" { - fmt.Fprintf(dockerCli.Err(), " WARNING: You're not using the default seccomp profile\n") + for _, so := range kvs { + fmt.Fprintf(dockerCli.Out(), " %s\n", so.Name) + for _, o := range so.Options { + switch o.Key { + case "profile": + if o.Value != "default" { + fmt.Fprintf(dockerCli.Err(), " WARNING: You're not using the default seccomp profile\n") + } + fmt.Fprintf(dockerCli.Out(), " Profile: %s\n", o.Value) } - fmt.Fprintf(dockerCli.Out(), " %s: %s\n", o.Key, o.Value) } } } From 3c47b1cb864ef5fdd5e2fc3b3388194462732e38 Mon Sep 17 00:00:00 2001 From: Antonio Murdaca Date: Wed, 16 Nov 2016 22:30:29 +0100 Subject: [PATCH 396/978] api: types: keep info.SecurityOptions a string slice Signed-off-by: Antonio Murdaca Upstream-commit: b4fe4fb42b56ba0d985fbbf074f8a9127385f09e Component: cli --- components/cli/info_test.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/components/cli/info_test.go b/components/cli/info_test.go index 7af82a8a31..79f23c8af2 100644 --- a/components/cli/info_test.go +++ b/components/cli/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 cf98c0f30ee1be986206777cce2191718a5ef330 Mon Sep 17 00:00:00 2001 From: Zhang Wei Date: Mon, 21 Nov 2016 17:31:46 +0800 Subject: [PATCH 397/978] 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 Upstream-commit: 20ded0afd962de543f5ee8fd42c76fe3e49d4281 Component: cli --- components/cli/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/client.go b/components/cli/client.go index 814c537c65..a85b392674 100644 --- a/components/cli/client.go +++ b/components/cli/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 df30b2fb868d4f5b9c57f594922d685fea324f97 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 21 Nov 2016 17:59:29 +0100 Subject: [PATCH 398/978] Do not panic if network is nil network is `nil` if the following case: ``` services: foo: image: nginx networks: mynetwork: ``` It's a valid compose so we should not panic. Signed-off-by: Vincent Demeester Upstream-commit: 2638cd6f3d38e6c580a479dcc399a7c4591788af Component: cli --- components/cli/command/stack/deploy.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index b0aaa290b6..099f8c03ae 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -252,9 +252,13 @@ func convertServiceNetworks( nets := []swarm.NetworkAttachmentConfig{} for networkName, network := range networks { + var aliases []string + if network != nil { + aliases = network.Aliases + } nets = append(nets, swarm.NetworkAttachmentConfig{ Target: namespace.scope(networkName), - Aliases: append(network.Aliases, name), + Aliases: append(aliases, name), }) } return nets From bc199413c3850ade0dfbd5b1789e50cbeee43cfa Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 21 Nov 2016 15:30:25 -0500 Subject: [PATCH 399/978] Move docker stack out of experimental Signed-off-by: Daniel Nephin Upstream-commit: e1b5bdd768312acca85d014f2fff85c53bf16f08 Component: cli --- components/cli/command/stack/cmd.go | 3 ++- components/cli/command/stack/deploy.go | 1 - components/cli/command/stack/opts.go | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/components/cli/command/stack/cmd.go b/components/cli/command/stack/cmd.go index 8626dc7fe4..860bfedd1a 100644 --- a/components/cli/command/stack/cmd.go +++ b/components/cli/command/stack/cmd.go @@ -13,7 +13,7 @@ func NewStackCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Manage Docker stacks", Args: cli.NoArgs, RunE: dockerCli.ShowHelp, - Tags: map[string]string{"experimental": "", "version": "1.25"}, + Tags: map[string]string{"version": "1.25"}, } cmd.AddCommand( newDeployCommand(dockerCli), @@ -30,5 +30,6 @@ func NewTopLevelDeployCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := newDeployCommand(dockerCli) // Remove the aliases at the top level cmd.Aliases = []string{} + cmd.Tags = map[string]string{"experimental": "", "version": "1.25"} return cmd } diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index b0aaa290b6..b7e1fc4fc5 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -49,7 +49,6 @@ func newDeployCommand(dockerCli *command.DockerCli) *cobra.Command { opts.namespace = args[0] return runDeploy(dockerCli, opts) }, - Tags: map[string]string{"experimental": "", "version": "1.25"}, } flags := cmd.Flags() diff --git a/components/cli/command/stack/opts.go b/components/cli/command/stack/opts.go index c2cc0d1e70..440d6099e3 100644 --- a/components/cli/command/stack/opts.go +++ b/components/cli/command/stack/opts.go @@ -15,6 +15,7 @@ func addComposefileFlag(opt *string, flags *pflag.FlagSet) { func addBundlefileFlag(opt *string, flags *pflag.FlagSet) { flags.StringVar(opt, "bundle-file", "", "Path to a Distributed Application Bundle file") + flags.SetAnnotation("bundle-file", "experimental", nil) } func addRegistryAuthFlag(opt *bool, flags *pflag.FlagSet) { From d8c70a7d8da069a9133c62fc9bfcc69ad8650568 Mon Sep 17 00:00:00 2001 From: Anusha Ragunathan Date: Mon, 21 Nov 2016 09:24:01 -0800 Subject: [PATCH 400/978] Add HTTP client timeout. Signed-off-by: Anusha Ragunathan Upstream-commit: 43e89b53879f179427fa492193cb7a2a8f12867e Component: cli --- components/cli/interface.go | 2 +- components/cli/plugin_enable.go | 11 +++++++++-- components/cli/plugin_enable_test.go | 5 +++-- components/cli/plugin_install.go | 2 +- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/components/cli/interface.go b/components/cli/interface.go index d46720e6c7..0d722d9075 100644 --- a/components/cli/interface.go +++ b/components/cli/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/components/cli/plugin_enable.go b/components/cli/plugin_enable.go index 8109814ddb..95517c4b80 100644 --- a/components/cli/plugin_enable.go +++ b/components/cli/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/components/cli/plugin_enable_test.go b/components/cli/plugin_enable_test.go index d919914e75..b27681348f 100644 --- a/components/cli/plugin_enable_test.go +++ b/components/cli/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/components/cli/plugin_install.go b/components/cli/plugin_install.go index 407f1cddf2..f73362ccd3 100644 --- a/components/cli/plugin_install.go +++ b/components/cli/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 3374b2a4b4dae50fb4f301abf5ad1b38d182773c Mon Sep 17 00:00:00 2001 From: Anusha Ragunathan Date: Mon, 21 Nov 2016 09:24:01 -0800 Subject: [PATCH 401/978] Add HTTP client timeout. Signed-off-by: Anusha Ragunathan Upstream-commit: 752a9a7c56837d6e9669db0447c828d1d0f77824 Component: cli --- components/cli/command/plugin/enable.go | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/components/cli/command/plugin/enable.go b/components/cli/command/plugin/enable.go index 0fd8f469dd..d84da24668 100644 --- a/components/cli/command/plugin/enable.go +++ b/components/cli/command/plugin/enable.go @@ -3,6 +3,7 @@ package plugin import ( "fmt" + "github.com/docker/docker/api/types" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/reference" @@ -10,20 +11,32 @@ import ( "golang.org/x/net/context" ) +type enableOpts struct { + timeout int + name string +} + func newEnableCommand(dockerCli *command.DockerCli) *cobra.Command { + var opts enableOpts + cmd := &cobra.Command{ Use: "enable PLUGIN", Short: "Enable a plugin", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return runEnable(dockerCli, args[0]) + opts.name = args[0] + return runEnable(dockerCli, &opts) }, } + flags := cmd.Flags() + flags.IntVar(&opts.timeout, "timeout", 0, "HTTP client timeout (in seconds)") return cmd } -func runEnable(dockerCli *command.DockerCli, name string) error { +func runEnable(dockerCli *command.DockerCli, opts *enableOpts) error { + name := opts.name + named, err := reference.ParseNamed(name) // FIXME: validate if err != nil { return err @@ -35,7 +48,11 @@ func runEnable(dockerCli *command.DockerCli, name string) error { if !ok { return fmt.Errorf("invalid name: %s", named.String()) } - if err := dockerCli.Client().PluginEnable(context.Background(), ref.String()); err != nil { + if opts.timeout < 0 { + return fmt.Errorf("negative timeout %d is invalid", opts.timeout) + } + + if err := dockerCli.Client().PluginEnable(context.Background(), ref.String(), types.PluginEnableOptions{Timeout: opts.timeout}); err != nil { return err } fmt.Fprintln(dockerCli.Out(), name) From e3969c2cea2e0c19f20152a9d3f8f3186c7cd85d Mon Sep 17 00:00:00 2001 From: yupeng Date: Mon, 21 Nov 2016 17:08:28 +0800 Subject: [PATCH 402/978] First header should be a top level header Signed-off-by: yupeng Upstream-commit: 4e4541540ff600ae666c6633e3262033c156ad00 Component: cli --- components/cli/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/README.md b/components/cli/README.md index 2b7d81fada..161686c0a6 100644 --- a/components/cli/README.md +++ b/components/cli/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 e727185cfefb0b7fcf0afca78a36480a3fc893c4 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Tue, 15 Nov 2016 12:10:02 -0500 Subject: [PATCH 403/978] Handle `run --rm` against older daemons on the cli For previous versions of Docker, `--rm` was handled client side, as such there was no support in the daemon for it. Now it is handled daemon side, but we still need to handle the case of a newer client talking to an older daemon. Falls back to client-side removal when the daemon does not support it. Signed-off-by: Brian Goff Upstream-commit: 4632a029d929ebb250e148799682dedc50f7777a Component: cli --- components/cli/command/container/utils.go | 55 ++++++++++++++--------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/components/cli/command/container/utils.go b/components/cli/command/container/utils.go index 6161e07140..f4ad09b912 100644 --- a/components/cli/command/container/utils.go +++ b/components/cli/command/container/utils.go @@ -9,6 +9,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/events" "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/versions" "github.com/docker/docker/cli/command" clientapi "github.com/docker/docker/client" ) @@ -19,11 +20,21 @@ func waitExitOrRemoved(ctx context.Context, dockerCli *command.DockerCli, contai panic("Internal Error: waitExitOrRemoved needs a containerID as parameter") } + var removeErr error statusChan := make(chan int) exitCode := 125 - eventProcessor := func(e events.Message) bool { + // Get events via Events API + f := filters.NewArgs() + f.Add("type", "container") + f.Add("container", containerID) + options := types.EventsOptions{ + Filters: f, + } + eventCtx, cancel := context.WithCancel(ctx) + eventq, errq := dockerCli.Client().Events(eventCtx, options) + eventProcessor := func(e events.Message) bool { stopProcessing := false switch e.Status { case "die": @@ -37,6 +48,18 @@ func waitExitOrRemoved(ctx context.Context, dockerCli *command.DockerCli, contai } if !waitRemove { stopProcessing = true + } else { + // If we are talking to an older daemon, `AutoRemove` is not supported. + // We need to fall back to the old behavior, which is client-side removal + if versions.LessThan(dockerCli.Client().ClientVersion(), "1.25") { + go func() { + removeErr = dockerCli.Client().ContainerRemove(ctx, containerID, types.ContainerRemoveOptions{RemoveVolumes: true}) + if removeErr != nil { + logrus.Errorf("error removing container: %v", removeErr) + cancel() // cancel the event Q + } + }() + } } case "detach": exitCode = 0 @@ -44,39 +67,27 @@ func waitExitOrRemoved(ctx context.Context, dockerCli *command.DockerCli, contai case "destroy": stopProcessing = true } - - if stopProcessing { - statusChan <- exitCode - return true - } - - return false + return stopProcessing } - // Get events via Events API - f := filters.NewArgs() - f.Add("type", "container") - f.Add("container", containerID) - options := types.EventsOptions{ - Filters: f, - } - - eventCtx, cancel := context.WithCancel(ctx) - eventq, errq := dockerCli.Client().Events(eventCtx, options) - go func() { - defer cancel() + defer func() { + statusChan <- exitCode // must always send an exit code or the caller will block + cancel() + }() for { select { + case <-eventCtx.Done(): + if removeErr != nil { + return + } case evt := <-eventq: if eventProcessor(evt) { return } - case err := <-errq: logrus.Errorf("error getting events from daemon: %v", err) - statusChan <- exitCode return } } From d9eae8f9b0cd507e041f7065917156cfd6f5b193 Mon Sep 17 00:00:00 2001 From: Reficul Date: Tue, 22 Nov 2016 10:42:55 +0800 Subject: [PATCH 404/978] fix incorrect ErrConnectFailed comparison Signed-off-by: Reficul Upstream-commit: b35205ed1284dcc2237e48e4f2a4d915e779b848 Component: cli --- components/cli/errors.go | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/components/cli/errors.go b/components/cli/errors.go index 94c22a728a..854516669c 100644 --- a/components/cli/errors.go +++ b/components/cli/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 a08156a072e10fb0130eb70aad832f63cf3d99c9 Mon Sep 17 00:00:00 2001 From: Reficul Date: Tue, 22 Nov 2016 10:42:55 +0800 Subject: [PATCH 405/978] fix incorrect ErrConnectFailed comparison Signed-off-by: Reficul Upstream-commit: 14770269e8527dae0510dc0146e50a0a59355d90 Component: cli --- components/cli/command/container/exec.go | 2 +- components/cli/command/container/utils.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/command/container/exec.go b/components/cli/command/container/exec.go index 4bc8c58066..f0381494e2 100644 --- a/components/cli/command/container/exec.go +++ b/components/cli/command/container/exec.go @@ -170,7 +170,7 @@ func getExecExitCode(ctx context.Context, client apiclient.ContainerAPIClient, e resp, err := client.ContainerExecInspect(ctx, execID) if err != nil { // If we can't connect, then the daemon probably died. - if err != apiclient.ErrConnectionFailed { + if !apiclient.IsErrConnectionFailed(err) { return false, -1, err } return false, -1, nil diff --git a/components/cli/command/container/utils.go b/components/cli/command/container/utils.go index 6161e07140..f42a8def10 100644 --- a/components/cli/command/container/utils.go +++ b/components/cli/command/container/utils.go @@ -91,7 +91,7 @@ func getExitCode(ctx context.Context, dockerCli *command.DockerCli, containerID c, err := dockerCli.Client().ContainerInspect(ctx, containerID) if err != nil { // If we can't connect, then the daemon probably died. - if err != clientapi.ErrConnectionFailed { + if !clientapi.IsErrConnectionFailed(err) { return false, -1, err } return false, -1, nil From 8dd51982bdc423568dbd346c3e95f41ae9f9e181 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Tue, 15 Nov 2016 19:45:20 +0000 Subject: [PATCH 406/978] Rename Remote API to Engine API Implementation of https://github.com/docker/docker/issues/28319 Signed-off-by: Ben Firshman Upstream-commit: 9c9ae79e64a7b6c83c16cc89cc021cf19b41f7f7 Component: cli --- components/cli/README.md | 2 +- components/cli/client.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/components/cli/README.md b/components/cli/README.md index 161686c0a6..059dfb3ce7 100644 --- a/components/cli/README.md +++ b/components/cli/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/components/cli/client.go b/components/cli/client.go index a85b392674..31a311e7d5 100644 --- a/components/cli/client.go +++ b/components/cli/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 fa382861cb38b603ccbb3c59720292e18ac57836 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Tue, 22 Nov 2016 11:18:28 -0500 Subject: [PATCH 407/978] update secret inspect to support IDs This updates secret inspect to support inspect by ID in addition to name as well as inspecting multiple secrets. This also cleans up the help text for consistency. Signed-off-by: Evan Hazlett Upstream-commit: 46cd1fa87b2eecb9dba9141d5fe18215a7fb0fd3 Component: cli --- components/cli/command/secret/inspect.go | 24 ++++++-------------- components/cli/command/secret/remove.go | 28 +++++------------------- components/cli/command/secret/utils.go | 27 +++++++++++++++++++++++ 3 files changed, 39 insertions(+), 40 deletions(-) diff --git a/components/cli/command/secret/inspect.go b/components/cli/command/secret/inspect.go index 04a5bd8a88..a82a26e4a8 100644 --- a/components/cli/command/secret/inspect.go +++ b/components/cli/command/secret/inspect.go @@ -9,18 +9,18 @@ import ( ) type inspectOptions struct { - name string + names []string format string } func newSecretInspectCommand(dockerCli *command.DockerCli) *cobra.Command { opts := inspectOptions{} cmd := &cobra.Command{ - Use: "inspect [name]", + Use: "inspect SECRET [SECRET]", Short: "Inspect a secret", - Args: cli.ExactArgs(1), + Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - opts.name = args[0] + opts.names = args return runSecretInspect(dockerCli, opts) }, } @@ -33,23 +33,13 @@ func runSecretInspect(dockerCli *command.DockerCli, opts inspectOptions) error { client := dockerCli.Client() ctx := context.Background() - // attempt to lookup secret by name - secrets, err := getSecretsByName(ctx, client, []string{opts.name}) + ids, err := getCliRequestedSecretIDs(ctx, client, opts.names) if err != nil { return err } - - id := opts.name - for _, s := range secrets { - if s.Spec.Annotations.Name == opts.name { - id = s.ID - break - } - } - - getRef := func(name string) (interface{}, []byte, error) { + getRef := func(id string) (interface{}, []byte, error) { return client.SecretInspectWithRaw(ctx, id) } - return inspect.Inspect(dockerCli.Out(), []string{id}, opts.format, getRef) + return inspect.Inspect(dockerCli.Out(), ids, opts.format, getRef) } diff --git a/components/cli/command/secret/remove.go b/components/cli/command/secret/remove.go index 44a71ef013..75b4be622b 100644 --- a/components/cli/command/secret/remove.go +++ b/components/cli/command/secret/remove.go @@ -10,17 +10,17 @@ import ( ) type removeOptions struct { - ids []string + names []string } func newSecretRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { return &cobra.Command{ - Use: "rm [id]", + Use: "rm SECRET [SECRET]", Short: "Remove a secret", Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts := removeOptions{ - ids: args, + names: args, } return runSecretRemove(dockerCli, opts) }, @@ -31,32 +31,14 @@ func runSecretRemove(dockerCli *command.DockerCli, opts removeOptions) error { client := dockerCli.Client() ctx := context.Background() - // attempt to lookup secret by name - secrets, err := getSecretsByName(ctx, client, opts.ids) + ids, err := getCliRequestedSecretIDs(ctx, client, opts.names) if err != nil { return err } - ids := opts.ids - - names := make(map[string]int) - for _, id := range ids { - names[id] = 1 - } - - if len(secrets) > 0 { - ids = []string{} - - for _, s := range secrets { - if _, ok := names[s.Spec.Annotations.Name]; ok { - ids = append(ids, s.ID) - } - } - } - for _, id := range ids { if err := client.SecretRemove(ctx, id); err != nil { - return err + fmt.Fprintf(dockerCli.Out(), "WARN: %s\n", err) } fmt.Fprintln(dockerCli.Out(), id) diff --git a/components/cli/command/secret/utils.go b/components/cli/command/secret/utils.go index c6e3cb61a2..0134853e09 100644 --- a/components/cli/command/secret/utils.go +++ b/components/cli/command/secret/utils.go @@ -18,3 +18,30 @@ func getSecretsByName(ctx context.Context, client client.APIClient, names []stri Filters: args, }) } + +func getCliRequestedSecretIDs(ctx context.Context, client client.APIClient, names []string) ([]string, error) { + ids := names + + // attempt to lookup secret by name + secrets, err := getSecretsByName(ctx, client, ids) + if err != nil { + return nil, err + } + + lookup := make(map[string]struct{}) + for _, id := range ids { + lookup[id] = struct{}{} + } + + if len(secrets) > 0 { + ids = []string{} + + for _, s := range secrets { + if _, ok := lookup[s.Spec.Annotations.Name]; ok { + ids = append(ids, s.ID) + } + } + } + + return ids, nil +} From 0adbcb7fcf19e80abe72b5ce37da9262764cdbe4 Mon Sep 17 00:00:00 2001 From: cyli Date: Tue, 22 Nov 2016 16:37:02 -0500 Subject: [PATCH 408/978] Do not display the digest or size of swarm secrets Signed-off-by: cyli Upstream-commit: 357cabef2d1bbeae18ecd1b86cc1687d4484b420 Component: cli --- components/cli/command/secret/ls.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/components/cli/command/secret/ls.go b/components/cli/command/secret/ls.go index 7471f08b19..e99f99e3d2 100644 --- a/components/cli/command/secret/ls.go +++ b/components/cli/command/secret/ls.go @@ -50,15 +50,14 @@ func runSecretList(dockerCli *command.DockerCli, opts listOptions) error { fmt.Fprintf(w, "%s\n", s.ID) } } else { - fmt.Fprintf(w, "ID\tNAME\tCREATED\tUPDATED\tSIZE") + fmt.Fprintf(w, "ID\tNAME\tCREATED\tUPDATED") fmt.Fprintf(w, "\n") for _, s := range secrets { created := units.HumanDuration(time.Now().UTC().Sub(s.Meta.CreatedAt)) + " ago" updated := units.HumanDuration(time.Now().UTC().Sub(s.Meta.UpdatedAt)) + " ago" - size := units.HumanSizeWithPrecision(float64(s.SecretSize), 3) - fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", s.ID, s.Spec.Annotations.Name, created, updated, size) + fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", s.ID, s.Spec.Annotations.Name, created, updated) } } From 0b6a55655e030818e5cc46e4ed05f9bb2e2b7343 Mon Sep 17 00:00:00 2001 From: erxian Date: Fri, 18 Nov 2016 14:28:21 +0800 Subject: [PATCH 409/978] update secret command Signed-off-by: erxian Upstream-commit: 0171a79c56599d5b687dd046ac7295c5cacad779 Component: cli --- components/cli/command/secret/create.go | 2 +- components/cli/command/secret/inspect.go | 4 ++-- components/cli/command/secret/ls.go | 2 +- components/cli/command/secret/remove.go | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/components/cli/command/secret/create.go b/components/cli/command/secret/create.go index da1cb9275e..faef32ef89 100644 --- a/components/cli/command/secret/create.go +++ b/components/cli/command/secret/create.go @@ -25,7 +25,7 @@ func newSecretCreateCommand(dockerCli *command.DockerCli) *cobra.Command { } cmd := &cobra.Command{ - Use: "create [name]", + Use: "create [OPTIONS] SECRET [SECRET...]", Short: "Create a secret using stdin as content", Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { diff --git a/components/cli/command/secret/inspect.go b/components/cli/command/secret/inspect.go index a82a26e4a8..1dda6f7838 100644 --- a/components/cli/command/secret/inspect.go +++ b/components/cli/command/secret/inspect.go @@ -16,8 +16,8 @@ type inspectOptions struct { func newSecretInspectCommand(dockerCli *command.DockerCli) *cobra.Command { opts := inspectOptions{} cmd := &cobra.Command{ - Use: "inspect SECRET [SECRET]", - Short: "Inspect a secret", + Use: "inspect [OPTIONS] SECRET [SECRET...]", + Short: "Display detailed information on one or more secrets", Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.names = args diff --git a/components/cli/command/secret/ls.go b/components/cli/command/secret/ls.go index e99f99e3d2..d96b377867 100644 --- a/components/cli/command/secret/ls.go +++ b/components/cli/command/secret/ls.go @@ -21,7 +21,7 @@ func newSecretListCommand(dockerCli *command.DockerCli) *cobra.Command { opts := listOptions{} cmd := &cobra.Command{ - Use: "ls", + Use: "ls [OPTIONS]", Short: "List secrets", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { diff --git a/components/cli/command/secret/remove.go b/components/cli/command/secret/remove.go index 75b4be622b..5026a437f8 100644 --- a/components/cli/command/secret/remove.go +++ b/components/cli/command/secret/remove.go @@ -15,8 +15,8 @@ type removeOptions struct { func newSecretRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { return &cobra.Command{ - Use: "rm SECRET [SECRET]", - Short: "Remove a secret", + Use: "rm SECRET [SECRET...]", + Short: "Remove one or more secrets", Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts := removeOptions{ From 6e1cc0ff7fac2c1ea699aa11da46baa3ab459a0a Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 21 Nov 2016 15:03:43 -0500 Subject: [PATCH 410/978] Better error message on stack deploy against not a swarm. Signed-off-by: Daniel Nephin Upstream-commit: 5ead1cc4901027ced77944ff5eefe4b829ec1a14 Component: cli --- components/cli/command/stack/deploy.go | 29 ++++++++++++++++--- .../cli/command/stack/deploy_bundlefile.go | 8 +++-- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index 2cd4efebcc..d8e76106ac 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -1,6 +1,7 @@ package stack import ( + "errors" "fmt" "io/ioutil" "os" @@ -59,19 +60,36 @@ func newDeployCommand(dockerCli *command.DockerCli) *cobra.Command { } func runDeploy(dockerCli *command.DockerCli, opts deployOptions) error { + ctx := context.Background() + switch { case opts.bundlefile == "" && opts.composefile == "": return fmt.Errorf("Please specify either a bundle file (with --bundle-file) or a Compose file (with --compose-file).") case opts.bundlefile != "" && opts.composefile != "": return fmt.Errorf("You cannot specify both a bundle file and a Compose file.") case opts.bundlefile != "": - return deployBundle(dockerCli, opts) + return deployBundle(ctx, dockerCli, opts) default: - return deployCompose(dockerCli, opts) + return deployCompose(ctx, dockerCli, opts) } } -func deployCompose(dockerCli *command.DockerCli, opts deployOptions) error { +// checkDaemonIsSwarmManager does an Info API call to verify that the daemon is +// a swarm manager. This is necessary because we must create networks before we +// create services, but the API call for creating a network does not return a +// proper status code when it can't create a network in the "global" scope. +func checkDaemonIsSwarmManager(ctx context.Context, dockerCli *command.DockerCli) error { + info, err := dockerCli.Client().Info(ctx) + if err != nil { + return err + } + if !info.Swarm.ControlAvailable { + return errors.New("This node is not a swarm manager. Use \"docker swarm init\" or \"docker swarm join\" to connect this node to swarm and try again.") + } + return nil +} + +func deployCompose(ctx context.Context, dockerCli *command.DockerCli, opts deployOptions) error { configDetails, err := getConfigDetails(opts) if err != nil { return err @@ -99,7 +117,10 @@ func deployCompose(dockerCli *command.DockerCli, opts deployOptions) error { propertyWarnings(deprecatedProperties)) } - ctx := context.Background() + if err := checkDaemonIsSwarmManager(ctx, dockerCli); err != nil { + return err + } + namespace := namespace{name: opts.namespace} networks := convertNetworks(namespace, config.Networks) diff --git a/components/cli/command/stack/deploy_bundlefile.go b/components/cli/command/stack/deploy_bundlefile.go index 5ec8a2a05b..c82c46e424 100644 --- a/components/cli/command/stack/deploy_bundlefile.go +++ b/components/cli/command/stack/deploy_bundlefile.go @@ -8,12 +8,16 @@ import ( "github.com/docker/docker/cli/command" ) -func deployBundle(dockerCli *command.DockerCli, opts deployOptions) error { +func deployBundle(ctx context.Context, dockerCli *command.DockerCli, opts deployOptions) error { bundle, err := loadBundlefile(dockerCli.Err(), opts.namespace, opts.bundlefile) if err != nil { return err } + if err := checkDaemonIsSwarmManager(ctx, dockerCli); err != nil { + return err + } + namespace := namespace{name: opts.namespace} networks := make(map[string]types.NetworkCreate) @@ -71,8 +75,6 @@ func deployBundle(dockerCli *command.DockerCli, opts deployOptions) error { services[internalName] = serviceSpec } - ctx := context.Background() - if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil { return err } From d54f25b0fd3464ee3ffae97e698b2491151cdc07 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 15 Nov 2016 11:18:33 -0500 Subject: [PATCH 411/978] exit with status 1 if help is called on an invalid command. Signed-off-by: Daniel Nephin Upstream-commit: 7604609bed8696798e496adade06df8102b0780b Component: cli --- components/cli/cobra.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/components/cli/cobra.go b/components/cli/cobra.go index 324c0d7b2d..139845cb1b 100644 --- a/components/cli/cobra.go +++ b/components/cli/cobra.go @@ -2,6 +2,7 @@ package cli import ( "fmt" + "strings" "github.com/spf13/cobra" ) @@ -17,6 +18,7 @@ func SetupRootCommand(rootCmd *cobra.Command) { rootCmd.SetUsageTemplate(usageTemplate) rootCmd.SetHelpTemplate(helpTemplate) rootCmd.SetFlagErrorFunc(FlagErrorFunc) + rootCmd.SetHelpCommand(helpCommand) rootCmd.PersistentFlags().BoolP("help", "h", false, "Print usage") rootCmd.PersistentFlags().MarkShorthandDeprecated("help", "please use --help") @@ -39,6 +41,23 @@ func FlagErrorFunc(cmd *cobra.Command, err error) error { } } +var helpCommand = &cobra.Command{ + Use: "help [command]", + Short: "Help about the command", + PersistentPreRun: func(cmd *cobra.Command, args []string) {}, + PersistentPostRun: func(cmd *cobra.Command, args []string) {}, + RunE: func(c *cobra.Command, args []string) error { + cmd, args, e := c.Root().Find(args) + if cmd == nil || e != nil || len(args) > 0 { + return fmt.Errorf("unknown help topic: %v", strings.Join(args, " ")) + } + + helpFunc := cmd.HelpFunc() + helpFunc(cmd, args) + return nil + }, +} + func hasSubCommands(cmd *cobra.Command) bool { return len(operationSubCommands(cmd)) > 0 } From c34c8c73ffd5e28444f1542724e1c851eac7d03f Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 15 Nov 2016 11:18:33 -0500 Subject: [PATCH 412/978] exit with status 1 if help is called on an invalid command. Signed-off-by: Daniel Nephin Upstream-commit: 004fc6b9e4b5bd7cb000525aeb905f4fc8bd4aea Component: cli --- components/cli/docker_test.go | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/components/cli/docker_test.go b/components/cli/docker_test.go index 47e24eb0da..8738f6005d 100644 --- a/components/cli/docker_test.go +++ b/components/cli/docker_test.go @@ -1,13 +1,14 @@ package main import ( + "io/ioutil" "os" "testing" "github.com/Sirupsen/logrus" - "github.com/docker/docker/utils" - "github.com/docker/docker/cli/command" + "github.com/docker/docker/pkg/testutil/assert" + "github.com/docker/docker/utils" ) func TestClientDebugEnabled(t *testing.T) { @@ -16,14 +17,16 @@ func TestClientDebugEnabled(t *testing.T) { cmd := newDockerCommand(&command.DockerCli{}) cmd.Flags().Set("debug", "true") - if err := cmd.PersistentPreRunE(cmd, []string{}); err != nil { - t.Fatalf("Unexpected error: %s", err.Error()) - } - - 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()) - } + err := cmd.PersistentPreRunE(cmd, []string{}) + assert.NilError(t, err) + assert.Equal(t, os.Getenv("DEBUG"), "1") + assert.Equal(t, logrus.GetLevel(), logrus.DebugLevel) +} + +func TestExitStatusForInvalidSubcommandWithHelpFlag(t *testing.T) { + discard := ioutil.Discard + cmd := newDockerCommand(command.NewDockerCli(os.Stdin, discard, discard)) + cmd.SetArgs([]string{"help", "invalid"}) + err := cmd.Execute() + assert.Error(t, err, "unknown help topic: invalid") } From e2c24e804b08764228d40f9f32c447ee9821b688 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Wed, 23 Nov 2016 14:30:57 -0800 Subject: [PATCH 413/978] support src in --secret Signed-off-by: Victor Vieux Upstream-commit: dc5c8a7713e3a27c5104e780fd34ef689e87862e Component: cli --- components/cli/command/service/opts.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index 90d0f99249..92e00ce41c 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -182,7 +182,7 @@ func (o *SecretOpt) Set(value string) error { value := parts[1] switch key { - case "source": + case "source", "src": spec.source = value case "target": tDir, _ := filepath.Split(value) From 95a1a461c63333ec0782ddeb8d6491b00372f8a9 Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Fri, 25 Nov 2016 04:07:06 +0800 Subject: [PATCH 414/978] Add options for docker plugin enable and fix some typos Signed-off-by: yuexiao-wang Upstream-commit: 7a89624bd516e00845d31b997dcb1d992ea6d42e Component: cli --- components/cli/command/plugin/enable.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/plugin/enable.go b/components/cli/command/plugin/enable.go index d84da24668..9201e38e11 100644 --- a/components/cli/command/plugin/enable.go +++ b/components/cli/command/plugin/enable.go @@ -20,7 +20,7 @@ func newEnableCommand(dockerCli *command.DockerCli) *cobra.Command { var opts enableOpts cmd := &cobra.Command{ - Use: "enable PLUGIN", + Use: "enable [OPTIONS] PLUGIN", Short: "Enable a plugin", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { From 3a6ac9f77360b67d470fecfb0eaccd16e11fcfeb Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 23 Nov 2016 23:39:14 -0800 Subject: [PATCH 415/978] Update docs of `docker network ls --filter` Currently the help output of `docker network ls --filter` is: ``` Options: -f, --filter value Provide filter values (i.e. 'dangling=true') (default []) ... ``` This caused confusion as only the following filters are supported at the moment: - `driver` - `type` - `name` - `id` - `label` This fix update the help output of `docker network ls --filter` and `network_ls.md`. The `dangling=true` description has been replace to: ``` Options: -f, --filter filter Provide filter values (i.e. 'driver=bridge') ... ``` This fix fixes 28786. Signed-off-by: Yong Tang Upstream-commit: 961046c5a8e4917498e4c21ba84d5325faa20a45 Component: cli --- components/cli/command/network/list.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/network/list.go b/components/cli/command/network/list.go index 9ba803275b..1a5d285103 100644 --- a/components/cli/command/network/list.go +++ b/components/cli/command/network/list.go @@ -43,7 +43,7 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command { flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display network IDs") flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate the output") flags.StringVar(&opts.format, "format", "", "Pretty-print networks using a Go template") - flags.VarP(&opts.filter, "filter", "f", "Provide filter values (i.e. 'dangling=true')") + flags.VarP(&opts.filter, "filter", "f", "Provide filter values (e.g. 'driver=bridge')") return cmd } From 527b0ad3025bd9db6fe271a819e5872bd731f485 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 23 Nov 2016 14:42:56 -0500 Subject: [PATCH 416/978] Allow hostname to be updated on service. Signed-off-by: Daniel Nephin Upstream-commit: c40696023b0f439633379086340fdc751724d07a Component: cli --- components/cli/command/service/create.go | 1 - components/cli/command/service/opts.go | 1 + components/cli/command/service/update.go | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/components/cli/command/service/create.go b/components/cli/command/service/create.go index 96c9f36da1..ea078e43ad 100644 --- a/components/cli/command/service/create.go +++ b/components/cli/command/service/create.go @@ -33,7 +33,6 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.VarP(&opts.labels, flagLabel, "l", "Service labels") flags.Var(&opts.containerLabels, flagContainerLabel, "Container labels") - flags.StringVar(&opts.hostname, flagHostname, "", "Container hostname") flags.VarP(&opts.env, flagEnv, "e", "Set environment variables") flags.Var(&opts.envFile, flagEnvFile, "Read in a file of environment variables") flags.Var(&opts.mounts, flagMount, "Attach a filesystem mount to the service") diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index 90d0f99249..7e631743a9 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -570,6 +570,7 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { flags.StringVarP(&opts.workdir, flagWorkdir, "w", "", "Working directory inside the container") flags.StringVarP(&opts.user, flagUser, "u", "", "Username or UID (format: [:])") + flags.StringVar(&opts.hostname, flagHostname, "", "Container hostname") flags.Var(&opts.resources.limitCPU, flagLimitCPU, "Limit CPUs") flags.Var(&opts.resources.limitMemBytes, flagLimitMemory, "Limit Memory") diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index 20a4fc5708..92329d1439 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -208,6 +208,7 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error { updateEnvironment(flags, &cspec.Env) updateString(flagWorkdir, &cspec.Dir) updateString(flagUser, &cspec.User) + updateString(flagHostname, &cspec.Hostname) if err := updateMounts(flags, &cspec.Mounts); err != nil { return err } From eae5084f8a6cbc2f6eddba94709bd2f296fb4806 Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Fri, 25 Nov 2016 19:46:24 +0800 Subject: [PATCH 417/978] Modify reponame to PLUGIN and fix some typos Signed-off-by: yuexiao-wang Upstream-commit: 48537db8498635f891dcc825c998a237a85685b3 Component: cli --- components/cli/command/plugin/create.go | 2 +- components/cli/command/plugin/push.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/command/plugin/create.go b/components/cli/command/plugin/create.go index 94c0d2c367..e0041c1b88 100644 --- a/components/cli/command/plugin/create.go +++ b/components/cli/command/plugin/create.go @@ -64,7 +64,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { options := pluginCreateOptions{} cmd := &cobra.Command{ - Use: "create [OPTIONS] reponame[:tag] PATH-TO-ROOTFS (rootfs + config.json)", + Use: "create [OPTIONS] PLUGIN[:tag] PATH-TO-ROOTFS(rootfs + config.json)", Short: "Create a plugin from a rootfs and config", Args: cli.RequiresMinArgs(2), RunE: func(cmd *cobra.Command, args []string) error { diff --git a/components/cli/command/plugin/push.go b/components/cli/command/plugin/push.go index e37a0483a6..add4a2b0a6 100644 --- a/components/cli/command/plugin/push.go +++ b/components/cli/command/plugin/push.go @@ -14,7 +14,7 @@ import ( func newPushCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := &cobra.Command{ - Use: "push NAME[:TAG]", + Use: "push PLUGIN[:TAG]", Short: "Push a plugin to a registry", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { From 2e594e381b89d135eb257948a8ba442a1deb11af Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 24 Nov 2016 16:11:38 -0500 Subject: [PATCH 418/978] Add a short flag for docker stack deploy Signed-off-by: Daniel Nephin Upstream-commit: 7b35599e2d6695266e4c1aaf61aa6a70b6dc8201 Component: cli --- components/cli/command/stack/opts.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/stack/opts.go b/components/cli/command/stack/opts.go index 440d6099e3..74fe4f5343 100644 --- a/components/cli/command/stack/opts.go +++ b/components/cli/command/stack/opts.go @@ -10,7 +10,7 @@ import ( ) func addComposefileFlag(opt *string, flags *pflag.FlagSet) { - flags.StringVar(opt, "compose-file", "", "Path to a Compose file") + flags.StringVarP(opt, "compose-file", "c", "", "Path to a Compose file") } func addBundlefileFlag(opt *string, flags *pflag.FlagSet) { From 5c72df8bb560c630ae090e62953852156fbb4f34 Mon Sep 17 00:00:00 2001 From: Kei Ohmura Date: Mon, 28 Nov 2016 13:24:02 +0900 Subject: [PATCH 419/978] fix description of 'docker swarm init' Signed-off-by: Kei Ohmura Upstream-commit: 8feea86e0f09cb81261d40d0d2f6e27aaa8fe99c Component: cli --- components/cli/command/swarm/opts.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/command/swarm/opts.go b/components/cli/command/swarm/opts.go index 885a3cd04e..9db46dcf55 100644 --- a/components/cli/command/swarm/opts.go +++ b/components/cli/command/swarm/opts.go @@ -171,8 +171,8 @@ func parseExternalCA(caSpec string) (*swarm.ExternalCA, error) { func addSwarmFlags(flags *pflag.FlagSet, opts *swarmOptions) { flags.Int64Var(&opts.taskHistoryLimit, flagTaskHistoryLimit, 5, "Task history retention limit") - flags.DurationVar(&opts.dispatcherHeartbeat, flagDispatcherHeartbeat, time.Duration(5*time.Second), "Dispatcher heartbeat period (ns|us|ms|s|m|h) (default 5s)") - flags.DurationVar(&opts.nodeCertExpiry, flagCertExpiry, time.Duration(90*24*time.Hour), "Validity period for node certificates (ns|us|ms|s|m|h) (default 2160h0m0s)") + flags.DurationVar(&opts.dispatcherHeartbeat, flagDispatcherHeartbeat, time.Duration(5*time.Second), "Dispatcher heartbeat period (ns|us|ms|s|m|h)") + flags.DurationVar(&opts.nodeCertExpiry, flagCertExpiry, time.Duration(90*24*time.Hour), "Validity period for node certificates (ns|us|ms|s|m|h)") flags.Var(&opts.externalCA, flagExternalCA, "Specifications of one or more certificate signing endpoints") flags.Uint64Var(&opts.maxSnapshots, flagMaxSnapshots, 0, "Number of additional Raft snapshots to retain") flags.Uint64Var(&opts.snapshotInterval, flagSnapshotInterval, 10000, "Number of log entries between Raft snapshots") From 535a0c8419e1564b1755fba2e978597aed2fff3f Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 28 Nov 2016 17:38:41 +0100 Subject: [PATCH 420/978] stack deploy: handle external network when deploying If the network is marked as external, don't use the namespace on it. Otherwise, it's not found. Signed-off-by: Vincent Demeester Upstream-commit: 8e63000bf359322be9ebda10a6d1d5ecd80bd8eb Component: cli --- components/cli/command/stack/common.go | 2 +- components/cli/command/stack/deploy.go | 66 +++++++++++++++++++++----- components/cli/command/stack/remove.go | 2 +- 3 files changed, 57 insertions(+), 13 deletions(-) diff --git a/components/cli/command/stack/common.go b/components/cli/command/stack/common.go index b94c108667..920a1af0cc 100644 --- a/components/cli/command/stack/common.go +++ b/components/cli/command/stack/common.go @@ -37,7 +37,7 @@ func getServices( types.ServiceListOptions{Filters: getStackFilter(namespace)}) } -func getNetworks( +func getStackNetworks( ctx context.Context, apiclient client.APIClient, namespace string, diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index d8e76106ac..66195ffbbe 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -22,6 +22,7 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" servicecmd "github.com/docker/docker/cli/command/service" + dockerclient "github.com/docker/docker/client" "github.com/docker/docker/opts" runconfigopts "github.com/docker/docker/runconfig/opts" "github.com/docker/go-connections/nat" @@ -123,7 +124,10 @@ func deployCompose(ctx context.Context, dockerCli *command.DockerCli, opts deplo namespace := namespace{name: opts.namespace} - networks := convertNetworks(namespace, config.Networks) + networks, externalNetworks := convertNetworks(namespace, config.Networks) + if err := validateExternalNetworks(ctx, dockerCli, externalNetworks); err != nil { + return err + } if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil { return err } @@ -179,7 +183,7 @@ func getConfigFile(filename string) (*composetypes.ConfigFile, error) { func convertNetworks( namespace namespace, networks map[string]composetypes.NetworkConfig, -) map[string]types.NetworkCreate { +) (map[string]types.NetworkCreate, []string) { if networks == nil { networks = make(map[string]composetypes.NetworkConfig) } @@ -187,10 +191,12 @@ func convertNetworks( // TODO: only add default network if it's used networks["default"] = composetypes.NetworkConfig{} + externalNetworks := []string{} result := make(map[string]types.NetworkCreate) for internalName, network := range networks { - if network.External.Name != "" { + if network.External.External { + externalNetworks = append(externalNetworks, network.External.Name) continue } @@ -216,7 +222,29 @@ func convertNetworks( result[internalName] = createOpts } - return result + return result, externalNetworks +} + +func validateExternalNetworks( + ctx context.Context, + dockerCli *command.DockerCli, + externalNetworks []string) error { + client := dockerCli.Client() + + for _, networkName := range externalNetworks { + network, err := client.NetworkInspect(ctx, networkName) + if err != nil { + if dockerclient.IsErrNetworkNotFound(err) { + return fmt.Errorf("network %q is declared as external, but could not be found. You need to create the network before the stack is deployed (with overlay driver)", networkName) + } + return err + } + if network.Scope != "swarm" { + return fmt.Errorf("network %q is declared as external, but it is not in the right scope: %q instead of %q", networkName, network.Scope, "swarm") + } + } + + return nil } func createNetworks( @@ -227,7 +255,7 @@ func createNetworks( ) error { client := dockerCli.Client() - existingNetworks, err := getNetworks(ctx, client, namespace.name) + existingNetworks, err := getStackNetworks(ctx, client, namespace.name) if err != nil { return err } @@ -258,30 +286,39 @@ func createNetworks( func convertServiceNetworks( networks map[string]*composetypes.ServiceNetworkConfig, + networkConfigs map[string]composetypes.NetworkConfig, namespace namespace, name string, -) []swarm.NetworkAttachmentConfig { +) ([]swarm.NetworkAttachmentConfig, error) { if len(networks) == 0 { return []swarm.NetworkAttachmentConfig{ { Target: namespace.scope("default"), Aliases: []string{name}, }, - } + }, nil } nets := []swarm.NetworkAttachmentConfig{} for networkName, network := range networks { + networkConfig, ok := networkConfigs[networkName] + if !ok { + return []swarm.NetworkAttachmentConfig{}, fmt.Errorf("invalid network: %s", networkName) + } var aliases []string if network != nil { aliases = network.Aliases } + target := namespace.scope(networkName) + if networkConfig.External.External { + target = networkName + } nets = append(nets, swarm.NetworkAttachmentConfig{ - Target: namespace.scope(networkName), + Target: target, Aliases: append(aliases, name), }) } - return nets + return nets, nil } func convertVolumes( @@ -472,9 +509,10 @@ func convertServices( services := config.Services volumes := config.Volumes + networks := config.Networks for _, service := range services { - serviceSpec, err := convertService(namespace, service, volumes) + serviceSpec, err := convertService(namespace, service, networks, volumes) if err != nil { return nil, err } @@ -487,6 +525,7 @@ func convertServices( func convertService( namespace namespace, service composetypes.ServiceConfig, + networkConfigs map[string]composetypes.NetworkConfig, volumes map[string]composetypes.VolumeConfig, ) (swarm.ServiceSpec, error) { name := namespace.scope(service.Name) @@ -523,6 +562,11 @@ func convertService( return swarm.ServiceSpec{}, err } + networks, err := convertServiceNetworks(service.Networks, networkConfigs, namespace, service.Name) + if err != nil { + return swarm.ServiceSpec{}, err + } + serviceSpec := swarm.ServiceSpec{ Annotations: swarm.Annotations{ Name: name, @@ -553,7 +597,7 @@ func convertService( }, EndpointSpec: endpoint, Mode: mode, - Networks: convertServiceNetworks(service.Networks, namespace, service.Name), + Networks: networks, UpdateConfig: convertUpdateConfig(service.Deploy.UpdateConfig), } diff --git a/components/cli/command/stack/remove.go b/components/cli/command/stack/remove.go index 8137903d47..734ff92a53 100644 --- a/components/cli/command/stack/remove.go +++ b/components/cli/command/stack/remove.go @@ -49,7 +49,7 @@ func runRemove(dockerCli *command.DockerCli, opts removeOptions) error { } } - networks, err := getNetworks(ctx, client, namespace) + networks, err := getStackNetworks(ctx, client, namespace) if err != nil { return err } From 668bff0dea2620a08164e166b33972674b238a3e Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 28 Nov 2016 18:08:45 +0100 Subject: [PATCH 421/978] Revert "Add -a option to service/node ps" This reverts commit 139fff2bf0ebe12b61871ba8ec8be8d51c2338db. Signed-off-by: Vincent Demeester Upstream-commit: 6ffb62368a88d08f0721da4a511917ea2aab63fd Component: cli --- components/cli/command/node/ps.go | 7 ------- components/cli/command/service/ps.go | 8 -------- 2 files changed, 15 deletions(-) diff --git a/components/cli/command/node/ps.go b/components/cli/command/node/ps.go index 8591f04669..a034721d24 100644 --- a/components/cli/command/node/ps.go +++ b/components/cli/command/node/ps.go @@ -17,7 +17,6 @@ import ( type psOptions struct { nodeIDs []string - all bool noResolve bool noTrunc bool filter opts.FilterOpt @@ -44,7 +43,6 @@ func newPsCommand(dockerCli *command.DockerCli) *cobra.Command { flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output") flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names") flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") - flags.BoolVarP(&opts.all, "all", "a", false, "Show all tasks (default shows tasks that are or will be running)") return cmd } @@ -74,11 +72,6 @@ func runPs(dockerCli *command.DockerCli, opts psOptions) error { filter := opts.filter.Value() filter.Add("node", node.ID) - if !opts.all && !filter.Include("desired-state") { - filter.Add("desired-state", string(swarm.TaskStateRunning)) - filter.Add("desired-state", string(swarm.TaskStateAccepted)) - } - nodeTasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: filter}) if err != nil { errs = append(errs, err.Error()) diff --git a/components/cli/command/service/ps.go b/components/cli/command/service/ps.go index 0028507c22..cf94ad7374 100644 --- a/components/cli/command/service/ps.go +++ b/components/cli/command/service/ps.go @@ -2,7 +2,6 @@ package service import ( "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/idresolver" @@ -15,7 +14,6 @@ import ( type psOptions struct { serviceID string - all bool quiet bool noResolve bool noTrunc bool @@ -39,7 +37,6 @@ func newPsCommand(dockerCli *command.DockerCli) *cobra.Command { flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output") flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names") flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") - flags.BoolVarP(&opts.all, "all", "a", false, "Show all tasks (default shows tasks that are or will be running)") return cmd } @@ -67,11 +64,6 @@ func runPS(dockerCli *command.DockerCli, opts psOptions) error { } } - if !opts.all && !filter.Include("desired-state") { - filter.Add("desired-state", string(swarm.TaskStateRunning)) - filter.Add("desired-state", string(swarm.TaskStateAccepted)) - } - tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: filter}) if err != nil { return err From 6ff3c5f368f044ba9adf014871b2df26adab5a7a Mon Sep 17 00:00:00 2001 From: John Howard Date: Wed, 23 Nov 2016 13:42:50 -0800 Subject: [PATCH 422/978] Align output of docker version again Signed-off-by: John Howard Upstream-commit: a913891b7d2558c4b8612875f5e90731d16753c0 Component: cli --- components/cli/command/system/version.go | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/components/cli/command/system/version.go b/components/cli/command/system/version.go index 00a84a3cbc..ded4f4d118 100644 --- a/components/cli/command/system/version.go +++ b/components/cli/command/system/version.go @@ -24,14 +24,13 @@ var versionTemplate = `Client: OS/Arch: {{.Client.Os}}/{{.Client.Arch}}{{if .ServerOK}} Server: - Version: {{.Server.Version}} - API version: {{.Server.APIVersion}} - Minimum API version: {{.Server.MinAPIVersion}} - Go version: {{.Server.GoVersion}} - Git commit: {{.Server.GitCommit}} - Built: {{.Server.BuildTime}} - OS/Arch: {{.Server.Os}}/{{.Server.Arch}} - Experimental: {{.Server.Experimental}}{{end}}` + Version: {{.Server.Version}} + API version: {{.Server.APIVersion}} (minimum version {{.Server.MinAPIVersion}}) + Go version: {{.Server.GoVersion}} + Git commit: {{.Server.GitCommit}} + Built: {{.Server.BuildTime}} + OS/Arch: {{.Server.Os}}/{{.Server.Arch}} + Experimental: {{.Server.Experimental}}{{end}}` type versionOptions struct { format string From 90f36d598323c0c103b778c24aebc93a02afe747 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 28 Nov 2016 22:15:50 +0100 Subject: [PATCH 423/978] 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 Upstream-commit: 32f410cd353d50e1c0546e9f6255df8d0d52a078 Component: cli --- components/cli/image_list.go | 13 +++++++-- components/cli/image_list_test.go | 48 +++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/components/cli/image_list.go b/components/cli/image_list.go index 63c71b1dd1..f26464f67c 100644 --- a/components/cli/image_list.go +++ b/components/cli/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/components/cli/image_list_test.go b/components/cli/image_list_test.go index 1c9406ddda..7c4a46414d 100644 --- a/components/cli/image_list_test.go +++ b/components/cli/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 e387834219fff875d5d1773be94ff006c1a648b6 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 28 Nov 2016 18:02:39 -0500 Subject: [PATCH 424/978] Use namespace label on stack volumes. Signed-off-by: Daniel Nephin Upstream-commit: 798c4a614e2488b46058fe145281db9b149c2537 Component: cli --- components/cli/command/stack/deploy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index d8e76106ac..9161c0965a 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -347,7 +347,7 @@ func convertVolumeToMount( source = stackVolume.External.Name } else { volumeOptions = &mount.VolumeOptions{ - Labels: stackVolume.Labels, + Labels: getStackLabels(namespace.name, stackVolume.Labels), NoCopy: isNoCopy(mode), } From 9d0522f784decb60a56d069a1689f2938bea5acf Mon Sep 17 00:00:00 2001 From: allencloud Date: Tue, 29 Nov 2016 14:28:46 +0800 Subject: [PATCH 425/978] change secret remove logic in cli Signed-off-by: allencloud Upstream-commit: 0227275b7f8d09ed5c8787ca8ac534a95cbcac11 Component: cli --- components/cli/command/secret/remove.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/components/cli/command/secret/remove.go b/components/cli/command/secret/remove.go index 5026a437f8..97d1f445ca 100644 --- a/components/cli/command/secret/remove.go +++ b/components/cli/command/secret/remove.go @@ -2,6 +2,7 @@ package secret import ( "fmt" + "strings" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" @@ -36,13 +37,20 @@ func runSecretRemove(dockerCli *command.DockerCli, opts removeOptions) error { return err } + var errs []string + for _, id := range ids { if err := client.SecretRemove(ctx, id); err != nil { - fmt.Fprintf(dockerCli.Out(), "WARN: %s\n", err) + errs = append(errs, err.Error()) + continue } fmt.Fprintln(dockerCli.Out(), id) } + if len(errs) > 0 { + return fmt.Errorf("%s", strings.Join(errs, "\n")) + } + return nil } From 77042515db553768b6a3b107a1bee778cf7738a2 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 21 Oct 2016 05:41:54 +0000 Subject: [PATCH 426/978] 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 Upstream-commit: 232944cc1531c4a0377a960dccdf8a4b263589d3 Component: cli --- components/cli/client.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/components/cli/client.go b/components/cli/client.go index 31a311e7d5..4c0f097e53 100644 --- a/components/cli/client.go +++ b/components/cli/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 b3d1ee086f78b3540bff820c7cc8e2f0cef53c70 Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Tue, 29 Nov 2016 01:21:47 +0800 Subject: [PATCH 427/978] Fix some typos Signed-off-by: yuexiao-wang Upstream-commit: 5e2a13b97193bb1bd13c11cf8c118dde476c3814 Component: cli --- components/cli/command/registry/search.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/registry/search.go b/components/cli/command/registry/search.go index 124b4ae6cc..bbcedbdd99 100644 --- a/components/cli/command/registry/search.go +++ b/components/cli/command/registry/search.go @@ -118,7 +118,7 @@ func runSearch(dockerCli *command.DockerCli, opts searchOptions) error { return nil } -// SearchResultsByStars sorts search results in descending order by number of stars. +// searchResultsByStars sorts search results in descending order by number of stars. type searchResultsByStars []registrytypes.SearchResult func (r searchResultsByStars) Len() int { return len(r) } From 85a1bbf9ecc42812605e0626fc4712903d91a881 Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Tue, 29 Nov 2016 23:44:12 +0800 Subject: [PATCH 428/978] Fix the inconsistency for docker secret Signed-off-by: yuexiao-wang Upstream-commit: 7a9e414988cd16db520b083ddae728fd986d971e Component: cli --- components/cli/command/secret/inspect.go | 2 +- components/cli/command/secret/ls.go | 7 ++++--- components/cli/command/secret/remove.go | 7 ++++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/components/cli/command/secret/inspect.go b/components/cli/command/secret/inspect.go index 1dda6f7838..0a8bd4a23f 100644 --- a/components/cli/command/secret/inspect.go +++ b/components/cli/command/secret/inspect.go @@ -25,7 +25,7 @@ func newSecretInspectCommand(dockerCli *command.DockerCli) *cobra.Command { }, } - cmd.Flags().StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template") + cmd.Flags().StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") return cmd } diff --git a/components/cli/command/secret/ls.go b/components/cli/command/secret/ls.go index d96b377867..faeab314b7 100644 --- a/components/cli/command/secret/ls.go +++ b/components/cli/command/secret/ls.go @@ -21,9 +21,10 @@ func newSecretListCommand(dockerCli *command.DockerCli) *cobra.Command { opts := listOptions{} cmd := &cobra.Command{ - Use: "ls [OPTIONS]", - Short: "List secrets", - Args: cli.NoArgs, + Use: "ls [OPTIONS]", + Aliases: []string{"list"}, + Short: "List secrets", + Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { return runSecretList(dockerCli, opts) }, diff --git a/components/cli/command/secret/remove.go b/components/cli/command/secret/remove.go index 5026a437f8..41651a16b8 100644 --- a/components/cli/command/secret/remove.go +++ b/components/cli/command/secret/remove.go @@ -15,9 +15,10 @@ type removeOptions struct { func newSecretRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { return &cobra.Command{ - Use: "rm SECRET [SECRET...]", - Short: "Remove one or more secrets", - Args: cli.RequiresMinArgs(1), + Use: "rm SECRET [SECRET...]", + Aliases: []string{"remove"}, + Short: "Remove one or more secrets", + Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts := removeOptions{ names: args, From 4919e4031cc0da49c8212c51a3980e5ffceafe72 Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Thu, 1 Dec 2016 18:30:44 +0800 Subject: [PATCH 429/978] Fix the use for secret create Signed-off-by: yuexiao-wang Upstream-commit: cd79095c814a29dea0987aafc32257dfaeb6965d Component: cli --- components/cli/command/secret/create.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/command/secret/create.go b/components/cli/command/secret/create.go index faef32ef89..381a931415 100644 --- a/components/cli/command/secret/create.go +++ b/components/cli/command/secret/create.go @@ -25,9 +25,9 @@ func newSecretCreateCommand(dockerCli *command.DockerCli) *cobra.Command { } cmd := &cobra.Command{ - Use: "create [OPTIONS] SECRET [SECRET...]", + Use: "create [OPTIONS] SECRET", Short: "Create a secret using stdin as content", - Args: cli.RequiresMinArgs(1), + Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { createOpts.name = args[0] return runSecretCreate(dockerCli, createOpts) From d37ef3a01bc06c04c05bb24f5bcbbcdf6a091e83 Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Fri, 2 Dec 2016 03:32:04 +0800 Subject: [PATCH 430/978] Optimize the log info for client test Signed-off-by: yuexiao-wang Upstream-commit: 9a9c077e6340b65fc23406e1a71d916fe4876dbb Component: cli --- components/cli/container_copy_test.go | 8 ++++---- components/cli/image_search_test.go | 8 ++++---- components/cli/plugin_push_test.go | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/components/cli/container_copy_test.go b/components/cli/container_copy_test.go index 6863cfba20..c84f82e9fb 100644 --- a/components/cli/container_copy_test.go +++ b/components/cli/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/components/cli/image_search_test.go b/components/cli/image_search_test.go index e46d86437f..108bd96744 100644 --- a/components/cli/image_search_test.go +++ b/components/cli/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/components/cli/plugin_push_test.go b/components/cli/plugin_push_test.go index efdbdc6db1..7b8eb865d6 100644 --- a/components/cli/plugin_push_test.go +++ b/components/cli/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 1a100e23888297ec800d18d277a22186379e8a4b Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Fri, 2 Dec 2016 04:18:02 +0800 Subject: [PATCH 431/978] Fix the inconsistent function name for client Signed-off-by: yuexiao-wang Upstream-commit: 7673aad2234349cfc884390ed72881e644a9e0b3 Component: cli --- components/cli/login.go | 2 +- components/cli/request.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/components/cli/login.go b/components/cli/login.go index 600dc7196f..79219ff59c 100644 --- a/components/cli/login.go +++ b/components/cli/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/components/cli/request.go b/components/cli/request.go index f15e380339..6457b316a3 100644 --- a/components/cli/request.go +++ b/components/cli/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 e70cf0acf08138a872e838c63fec5b2b8b0e42f6 Mon Sep 17 00:00:00 2001 From: Jake Sanders Date: Thu, 18 Aug 2016 14:23:10 -0700 Subject: [PATCH 432/978] Add registry-specific credential helper support Signed-off-by: Jake Sanders Upstream-commit: c84b90291ca7b75099eaa6f52349cc073ee0f0fe Component: cli --- components/cli/command/cli.go | 49 +++++++++++++++++++++-- components/cli/command/image/build.go | 4 +- components/cli/command/registry.go | 4 +- components/cli/command/registry/login.go | 2 +- components/cli/command/registry/logout.go | 2 +- 5 files changed, 51 insertions(+), 10 deletions(-) diff --git a/components/cli/command/cli.go b/components/cli/command/cli.go index 99ea6331af..6d1dd7472e 100644 --- a/components/cli/command/cli.go +++ b/components/cli/command/cli.go @@ -10,6 +10,7 @@ import ( "runtime" "github.com/docker/docker/api" + "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/versions" cliflags "github.com/docker/docker/cli/flags" "github.com/docker/docker/cliconfig" @@ -86,15 +87,55 @@ func (cli *DockerCli) ConfigFile() *configfile.ConfigFile { return cli.configFile } +// GetAllCredentials returns all of the credentials stored in all of the +// configured credential stores. +func (cli *DockerCli) GetAllCredentials() (map[string]types.AuthConfig, error) { + auths := make(map[string]types.AuthConfig) + for registry := range cli.configFile.CredentialHelpers { + helper := cli.CredentialsStore(registry) + newAuths, err := helper.GetAll() + if err != nil { + return nil, err + } + addAll(auths, newAuths) + } + defaultStore := cli.CredentialsStore("") + newAuths, err := defaultStore.GetAll() + if err != nil { + return nil, err + } + addAll(auths, newAuths) + return auths, nil +} + +func addAll(to, from map[string]types.AuthConfig) { + for reg, ac := range from { + to[reg] = ac + } +} + // CredentialsStore returns a new credentials store based -// on the settings provided in the configuration file. -func (cli *DockerCli) CredentialsStore() credentials.Store { - if cli.configFile.CredentialsStore != "" { - return credentials.NewNativeStore(cli.configFile) +// on the settings provided in the configuration file. Empty string returns +// the default credential store. +func (cli *DockerCli) CredentialsStore(serverAddress string) credentials.Store { + if helper := getConfiguredCredentialStore(cli.configFile, serverAddress); helper != "" { + return credentials.NewNativeStore(cli.configFile, helper) } return credentials.NewFileStore(cli.configFile) } +// getConfiguredCredentialStore returns the credential helper configured for the +// given registry, the default credsStore, or the empty string if neither are +// configured. +func getConfiguredCredentialStore(c *configfile.ConfigFile, serverAddress string) string { + if c.CredentialHelpers != nil && serverAddress != "" { + if helper, exists := c.CredentialHelpers[serverAddress]; exists { + return helper + } + } + return c.CredentialsStore +} + // Initialize the dockerCli runs initialization that must happen after command // line flags are parsed. func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error { diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go index ebec87d641..78cc41494f 100644 --- a/components/cli/command/image/build.go +++ b/components/cli/command/image/build.go @@ -280,7 +280,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { } } - authConfig, _ := dockerCli.CredentialsStore().GetAll() + authConfigs, _ := dockerCli.GetAllCredentials() buildOptions := types.ImageBuildOptions{ Memory: memory, MemorySwap: memorySwap, @@ -301,7 +301,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { ShmSize: shmSize, Ulimits: options.ulimits.GetList(), BuildArgs: runconfigopts.ConvertKVStringsToMap(options.buildArgs.GetAll()), - AuthConfigs: authConfig, + AuthConfigs: authConfigs, Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()), CacheFrom: options.cacheFrom, SecurityOpt: options.securityOpt, diff --git a/components/cli/command/registry.go b/components/cli/command/registry.go index b70d6f444c..65f6b3309e 100644 --- a/components/cli/command/registry.go +++ b/components/cli/command/registry.go @@ -67,7 +67,7 @@ func ResolveAuthConfig(ctx context.Context, cli *DockerCli, index *registrytypes configKey = ElectAuthServer(ctx, cli) } - a, _ := cli.CredentialsStore().Get(configKey) + a, _ := cli.CredentialsStore(configKey).Get(configKey) return a } @@ -82,7 +82,7 @@ func ConfigureAuth(cli *DockerCli, flUser, flPassword, serverAddress string, isD serverAddress = registry.ConvertToHostname(serverAddress) } - authconfig, err := cli.CredentialsStore().Get(serverAddress) + authconfig, err := cli.CredentialsStore(serverAddress).Get(serverAddress) if err != nil { return authconfig, err } diff --git a/components/cli/command/registry/login.go b/components/cli/command/registry/login.go index f161f2d403..bdcc9a103b 100644 --- a/components/cli/command/registry/login.go +++ b/components/cli/command/registry/login.go @@ -69,7 +69,7 @@ func runLogin(dockerCli *command.DockerCli, opts loginOptions) error { authConfig.Password = "" authConfig.IdentityToken = response.IdentityToken } - if err := dockerCli.CredentialsStore().Store(authConfig); err != nil { + if err := dockerCli.CredentialsStore(serverAddress).Store(authConfig); err != nil { return fmt.Errorf("Error saving credentials: %v", err) } diff --git a/components/cli/command/registry/logout.go b/components/cli/command/registry/logout.go index 8e820dcc8c..877e60e8cc 100644 --- a/components/cli/command/registry/logout.go +++ b/components/cli/command/registry/logout.go @@ -68,7 +68,7 @@ func runLogout(dockerCli *command.DockerCli, serverAddress string) error { fmt.Fprintf(dockerCli.Out(), "Removing login credentials for %s\n", hostnameAddress) for _, r := range regsToLogout { - if err := dockerCli.CredentialsStore().Erase(r); err != nil { + if err := dockerCli.CredentialsStore(r).Erase(r); err != nil { fmt.Fprintf(dockerCli.Err(), "WARNING: could not erase credentials: %v\n", err) } } From c51ef613cbb777bf180c119d014d64777264e0df Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 23 Nov 2016 20:04:44 -0800 Subject: [PATCH 433/978] Allow `docker plugin inspect` to search based on ID or name This fix tries to address the issue raised in discussion of PR 28735 where it was not possible to manage plugin based on plugin ID. Previously it was not possible to invoke `docker plugin inspect` with a plugin ID (or ID prefix). This fix updates the implementation of `docker plugin inspect` so that it is possbile to search based on a plugin name, or a plugin ID. A short format of plugin ID (prefix) is also possible, as long as there is no ambiguity. Previously the check of `docker plugin inspect` was mostly done on the client side. This could potentially cause inconsistency between API and CMD. This fix move all the checks to daemon side so that API and CMD will be consistent. An integration test has been added to cover the changes. Signed-off-by: Yong Tang Upstream-commit: 312958f4db93d6675a39913cd2e1de8dbed3fc70 Component: cli --- components/cli/command/plugin/inspect.go | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/components/cli/command/plugin/inspect.go b/components/cli/command/plugin/inspect.go index 13c7fa72d8..46ec7b229b 100644 --- a/components/cli/command/plugin/inspect.go +++ b/components/cli/command/plugin/inspect.go @@ -1,12 +1,9 @@ package plugin import ( - "fmt" - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/inspect" - "github.com/docker/docker/reference" "github.com/spf13/cobra" "golang.org/x/net/context" ) @@ -20,7 +17,7 @@ func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { var opts inspectOptions cmd := &cobra.Command{ - Use: "inspect [OPTIONS] PLUGIN [PLUGIN...]", + Use: "inspect [OPTIONS] PLUGIN|ID [PLUGIN|ID...]", Short: "Display detailed information on one or more plugins", Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { @@ -37,20 +34,8 @@ func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { client := dockerCli.Client() ctx := context.Background() - getRef := func(name string) (interface{}, []byte, error) { - named, err := reference.ParseNamed(name) // FIXME: validate - if err != nil { - return nil, nil, err - } - if reference.IsNameOnly(named) { - named = reference.WithDefaultTag(named) - } - ref, ok := named.(reference.NamedTagged) - if !ok { - return nil, nil, fmt.Errorf("invalid name: %s", named.String()) - } - - return client.PluginInspectWithRaw(ctx, ref.String()) + getRef := func(ref string) (interface{}, []byte, error) { + return client.PluginInspectWithRaw(ctx, ref) } return inspect.Inspect(dockerCli.Out(), opts.pluginNames, opts.format, getRef) From 93e292438ceec18a8aba46bcd4cfa09b7cb000c2 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Thu, 1 Dec 2016 14:08:06 -0800 Subject: [PATCH 434/978] api: Hide UpdateStatus when it is not present When UpdateStatus was not present, the empty values of the timestamps would be present: "UpdateStatus": { "StartedAt": "0001-01-01T00:00:00Z", "CompletedAt": "0001-01-01T00:00:00Z" } To fix this, make the timestamps pointers, so they can be set to nil when they should not be shown. Also make UpdateStatus itself a pointer, so an empty object does not show up when there is no UpdateStatus. Signed-off-by: Aaron Lehmann Upstream-commit: 8a379d7bcebea5415e462f85ee2c1c0bf704178a Component: cli --- components/cli/command/formatter/service.go | 14 ++++++++++---- components/cli/command/service/inspect_test.go | 6 +++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/components/cli/command/formatter/service.go b/components/cli/command/formatter/service.go index aaa78386cb..2690029ce4 100644 --- a/components/cli/command/formatter/service.go +++ b/components/cli/command/formatter/service.go @@ -28,7 +28,9 @@ Service Mode: {{- if .HasUpdateStatus }} UpdateStatus: State: {{ .UpdateStatusState }} +{{- if .HasUpdateStatusStarted }} Started: {{ .UpdateStatusStarted }} +{{- end }} {{- if .UpdateIsCompleted }} Completed: {{ .UpdateStatusCompleted }} {{- end }} @@ -172,23 +174,27 @@ func (ctx *serviceInspectContext) ModeReplicatedReplicas() *uint64 { } func (ctx *serviceInspectContext) HasUpdateStatus() bool { - return ctx.Service.UpdateStatus.State != "" + return ctx.Service.UpdateStatus != nil && ctx.Service.UpdateStatus.State != "" } func (ctx *serviceInspectContext) UpdateStatusState() swarm.UpdateState { return ctx.Service.UpdateStatus.State } +func (ctx *serviceInspectContext) HasUpdateStatusStarted() bool { + return ctx.Service.UpdateStatus.StartedAt != nil +} + func (ctx *serviceInspectContext) UpdateStatusStarted() string { - return units.HumanDuration(time.Since(ctx.Service.UpdateStatus.StartedAt)) + return units.HumanDuration(time.Since(*ctx.Service.UpdateStatus.StartedAt)) } func (ctx *serviceInspectContext) UpdateIsCompleted() bool { - return ctx.Service.UpdateStatus.State == swarm.UpdateStateCompleted + return ctx.Service.UpdateStatus.State == swarm.UpdateStateCompleted && ctx.Service.UpdateStatus.CompletedAt != nil } func (ctx *serviceInspectContext) UpdateStatusCompleted() string { - return units.HumanDuration(time.Since(ctx.Service.UpdateStatus.CompletedAt)) + return units.HumanDuration(time.Since(*ctx.Service.UpdateStatus.CompletedAt)) } func (ctx *serviceInspectContext) UpdateStatusMessage() string { diff --git a/components/cli/command/service/inspect_test.go b/components/cli/command/service/inspect_test.go index 04a65080c7..34c41ee78a 100644 --- a/components/cli/command/service/inspect_test.go +++ b/components/cli/command/service/inspect_test.go @@ -74,9 +74,9 @@ func formatServiceInspect(t *testing.T, format formatter.Format, now time.Time) }, }, }, - UpdateStatus: swarm.UpdateStatus{ - StartedAt: now, - CompletedAt: now, + UpdateStatus: &swarm.UpdateStatus{ + StartedAt: &now, + CompletedAt: &now, }, } From 2c852cb8fd29dfd5e24dabb6bf256310cfac7e0c Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Thu, 18 Aug 2016 18:09:07 -0700 Subject: [PATCH 435/978] Return error for incorrect argument of `service update --publish-rm ` Currently `--publish-rm` only accepts `` or `[/Protocol]` though there are some confusions. Since `--publish-add` accepts `:[/Protocol]`, some user may provide `--publish-rm 80:80`. However, there is no error checking so the incorrect provided argument is ignored silently. This fix adds the check to make sure `--publish-rm` only accepts `[/Protocol]` and returns error if the format is invalid. The `--publish-rm` itself may needs to be revisited to have a better UI/UX experience, see discussions on: https://github.com/docker/swarmkit/issues/1396 https://github.com/docker/docker/issues/25200#issuecomment-236213242 https://github.com/docker/docker/issues/25338#issuecomment-240787002 This fix is short term measure so that end users are not misled by the silently ignored error of `--publish-rm`. This fix is related to (but is not a complete fix): https://github.com/docker/swarmkit/issues/1396 Signed-off-by: Yong Tang Upstream-commit: 8597e231a5caa23247934f7a89c1001d87e26192 Component: cli --- components/cli/command/service/update.go | 19 ++++++++- components/cli/command/service/update_test.go | 40 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index 92329d1439..200f58c3a6 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -47,7 +47,7 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Var(newListOptsVar(), flagLabelRemove, "Remove a label by its key") flags.Var(newListOptsVar(), flagContainerLabelRemove, "Remove a container label by its key") flags.Var(newListOptsVar(), flagMountRemove, "Remove a mount by its target path") - flags.Var(newListOptsVar(), flagPublishRemove, "Remove a published port by its target port") + flags.Var(newListOptsVar().WithValidator(validatePublishRemove), flagPublishRemove, "Remove a published port by its target port") flags.MarkHidden(flagPublishRemove) flags.Var(newListOptsVar(), flagPortRemove, "Remove a port(target-port mandatory)") flags.Var(newListOptsVar(), flagConstraintRemove, "Remove a constraint") @@ -645,6 +645,23 @@ func portConfigToString(portConfig *swarm.PortConfig) string { return fmt.Sprintf("%v:%v/%s/%s", portConfig.PublishedPort, portConfig.TargetPort, protocol, mode) } +// This validation is only used for `--publish-rm`. +// The `--publish-rm` takes: +// [/] (e.g., 80, 80/tcp, 53/udp) +func validatePublishRemove(val string) (string, error) { + proto, port := nat.SplitProtoPort(val) + if proto != "tcp" && proto != "udp" { + return "", fmt.Errorf("invalid protocol '%s' for %s", proto, val) + } + if strings.Contains(port, ":") { + return "", fmt.Errorf("invalid port format: '%s', should be [/] (e.g., 80, 80/tcp, 53/udp)", port) + } + if _, err := nat.ParsePort(port); err != nil { + return "", err + } + return val, nil +} + func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) error { // The key of the map is `port/protocol`, e.g., `80/tcp` portSet := map[string]swarm.PortConfig{} diff --git a/components/cli/command/service/update_test.go b/components/cli/command/service/update_test.go index 998d06d3bd..bb2e9c1073 100644 --- a/components/cli/command/service/update_test.go +++ b/components/cli/command/service/update_test.go @@ -345,3 +345,43 @@ func TestUpdateHosts(t *testing.T) { assert.Equal(t, hosts[1], "2001:db8:abc8::1 ipv6.net") assert.Equal(t, hosts[2], "4.3.2.1 example.org") } + +func TestUpdatePortsRmWithProtocol(t *testing.T) { + flags := newUpdateCommand(nil).Flags() + flags.Set("publish-add", "8081:81") + flags.Set("publish-add", "8082:82") + flags.Set("publish-rm", "80") + flags.Set("publish-rm", "81/tcp") + flags.Set("publish-rm", "82/udp") + + portConfigs := []swarm.PortConfig{ + {TargetPort: 80, PublishedPort: 8080, Protocol: swarm.PortConfigProtocolTCP}, + } + + err := updatePorts(flags, &portConfigs) + assert.Equal(t, err, nil) + assert.Equal(t, len(portConfigs), 1) + assert.Equal(t, portConfigs[0].TargetPort, uint32(82)) +} + +func TestValidatePort(t *testing.T) { + validPorts := []string{"80/tcp", "80", "80/udp"} + invalidPorts := map[string]string{ + "9999999": "out of range", + "80:80/tcp": "invalid port format", + "53:53/udp": "invalid port format", + "80:80": "invalid port format", + "80/xyz": "invalid protocol", + "tcp": "invalid syntax", + "udp": "invalid syntax", + "": "invalid protocol", + } + for _, port := range validPorts { + _, err := validatePublishRemove(port) + assert.Equal(t, err, nil) + } + for port, e := range invalidPorts { + _, err := validatePublishRemove(port) + assert.Error(t, err, e) + } +} From a25e20e8a1055f3efee9678eea317ac08287730d Mon Sep 17 00:00:00 2001 From: Arash Deshmeh Date: Thu, 1 Dec 2016 23:28:51 -0500 Subject: [PATCH 436/978] Print checkpoint id when creating a checkpoint Signed-off-by: Arash Deshmeh Upstream-commit: b68a6ad2ac8fdb513ff18d4c127848cd50b23525 Component: cli --- components/cli/command/checkpoint/create.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/components/cli/command/checkpoint/create.go b/components/cli/command/checkpoint/create.go index 2377b5e2e3..473a941733 100644 --- a/components/cli/command/checkpoint/create.go +++ b/components/cli/command/checkpoint/create.go @@ -1,6 +1,8 @@ package checkpoint import ( + "fmt" + "golang.org/x/net/context" "github.com/docker/docker/api/types" @@ -51,5 +53,6 @@ func runCreate(dockerCli *command.DockerCli, opts createOptions) error { return err } + fmt.Fprintf(dockerCli.Out(), "%s\n", opts.checkpoint) return nil } From a8d5c29439a18cb11ee1c186c0a3465da1c830f6 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Tue, 29 Nov 2016 17:31:29 -0800 Subject: [PATCH 437/978] 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 Upstream-commit: 492c2c8da878fecd9c2bc7a708af7963f56ece21 Component: cli --- components/cli/command/system/inspect.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/components/cli/command/system/inspect.go b/components/cli/command/system/inspect.go index a403685ee7..dee4efcfec 100644 --- a/components/cli/command/system/inspect.go +++ b/components/cli/command/system/inspect.go @@ -45,7 +45,7 @@ func NewInspectCommand(dockerCli *command.DockerCli) *cobra.Command { func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { var elementSearcher inspect.GetRefFunc switch opts.inspectType { - case "", "container", "image", "node", "network", "service", "volume", "task": + case "", "container", "image", "node", "network", "service", "volume", "task", "plugin": elementSearcher = inspectAll(context.Background(), dockerCli, opts.size, opts.inspectType) default: return fmt.Errorf("%q is not a valid value for --type", opts.inspectType) @@ -95,6 +95,12 @@ func inspectVolume(ctx context.Context, dockerCli *command.DockerCli) inspect.Ge } } +func inspectPlugin(ctx context.Context, dockerCli *command.DockerCli) inspect.GetRefFunc { + return func(ref string) (interface{}, []byte, error) { + return dockerCli.Client().PluginInspectWithRaw(ctx, ref) + } +} + func inspectAll(ctx context.Context, dockerCli *command.DockerCli, getSize bool, typeConstraint string) inspect.GetRefFunc { var inspectAutodetect = []struct { ObjectType string @@ -108,6 +114,7 @@ func inspectAll(ctx context.Context, dockerCli *command.DockerCli, getSize bool, {"service", false, inspectService(ctx, dockerCli)}, {"task", false, inspectTasks(ctx, dockerCli)}, {"node", false, inspectNode(ctx, dockerCli)}, + {"plugin", false, inspectPlugin(ctx, dockerCli)}, } isErrNotSwarmManager := func(err error) bool { From 78311eeeafa2825d535faa461983096aea5db84f Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Tue, 29 Nov 2016 17:31:29 -0800 Subject: [PATCH 438/978] 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 Upstream-commit: 47f0fde2cf0c09e571d476c680229086a9e994ef Component: cli --- components/cli/errors.go | 21 +++++++++++++++++++++ components/cli/plugin_inspect.go | 4 ++++ 2 files changed, 25 insertions(+) diff --git a/components/cli/errors.go b/components/cli/errors.go index 854516669c..bf6923f134 100644 --- a/components/cli/errors.go +++ b/components/cli/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/components/cli/plugin_inspect.go b/components/cli/plugin_inspect.go index e9474b5a98..72900a1310 100644 --- a/components/cli/plugin_inspect.go +++ b/components/cli/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 38bf746466109391fc7d89ea8fb1532caf93d4af Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Fri, 2 Dec 2016 13:42:50 -0800 Subject: [PATCH 439/978] Add `ID` field for `docker plugin ls` This fix tries to address the enhancement proposed in 28708 to display ID field for the output of `docker plugin ls`. This fix add `ID` field to the output of `docker plugin ls` Related docs has been updated. This fix fixes 28708. Signed-off-by: Yong Tang Upstream-commit: 0449997cf6f1db858fc8274ab653bc49b6b3e835 Component: cli --- components/cli/command/plugin/list.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/components/cli/command/plugin/list.go b/components/cli/command/plugin/list.go index e402d44b31..4f800d7ec1 100644 --- a/components/cli/command/plugin/list.go +++ b/components/cli/command/plugin/list.go @@ -7,6 +7,7 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/pkg/stringid" "github.com/docker/docker/pkg/stringutils" "github.com/spf13/cobra" "golang.org/x/net/context" @@ -43,17 +44,19 @@ func runList(dockerCli *command.DockerCli, opts listOptions) error { } w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0) - fmt.Fprintf(w, "NAME \tTAG \tDESCRIPTION\tENABLED") + fmt.Fprintf(w, "ID \tNAME \tTAG \tDESCRIPTION\tENABLED") fmt.Fprintf(w, "\n") for _, p := range plugins { + id := p.ID desc := strings.Replace(p.Config.Description, "\n", " ", -1) desc = strings.Replace(desc, "\r", " ", -1) if !opts.noTrunc { + id = stringid.TruncateID(p.ID) desc = stringutils.Ellipsis(desc, 45) } - fmt.Fprintf(w, "%s\t%s\t%s\t%v\n", p.Name, p.Tag, desc, p.Enabled) + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%v\n", id, p.Name, p.Tag, desc, p.Enabled) } w.Flush() return nil From cc2b3e61e83bf6ff8e2d68c5100da6d742f6e94b Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Wed, 23 Nov 2016 17:29:21 -0800 Subject: [PATCH 440/978] refactor plugin install Signed-off-by: Victor Vieux Upstream-commit: 7520858943638c53fb39ce7487651733654865e2 Component: cli --- components/cli/plugin_inspect.go | 2 +- components/cli/plugin_install.go | 33 +++++++++++++++++++++----------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/components/cli/plugin_inspect.go b/components/cli/plugin_inspect.go index e9474b5a98..1fb40624ca 100644 --- a/components/cli/plugin_inspect.go +++ b/components/cli/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/components/cli/plugin_install.go b/components/cli/plugin_install.go index f73362ccd3..e7b67f2051 100644 --- a/components/cli/plugin_install.go +++ b/components/cli/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 b4cfb614da28452f3177d59a6610322ebe15a686 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 16 Nov 2016 21:46:37 -0800 Subject: [PATCH 441/978] 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 Upstream-commit: dd39897fcaa611bdcd87cd7a560395dfb55ead80 Component: cli --- components/cli/command/container/prune.go | 4 ++-- components/cli/command/image/prune.go | 9 +++++---- components/cli/command/network/prune.go | 4 ++-- components/cli/command/volume/prune.go | 4 ++-- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/components/cli/command/container/prune.go b/components/cli/command/container/prune.go index ec6b0e3147..064f4c08e0 100644 --- a/components/cli/command/container/prune.go +++ b/components/cli/command/container/prune.go @@ -5,7 +5,7 @@ import ( "golang.org/x/net/context" - "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" units "github.com/docker/go-units" @@ -52,7 +52,7 @@ func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed u return } - report, err := dockerCli.Client().ContainersPrune(context.Background(), types.ContainersPruneConfig{}) + report, err := dockerCli.Client().ContainersPrune(context.Background(), filters.Args{}) if err != nil { return } diff --git a/components/cli/command/image/prune.go b/components/cli/command/image/prune.go index ea84cda877..82c28fcf49 100644 --- a/components/cli/command/image/prune.go +++ b/components/cli/command/image/prune.go @@ -5,7 +5,7 @@ import ( "golang.org/x/net/context" - "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" units "github.com/docker/go-units" @@ -54,6 +54,9 @@ Are you sure you want to continue?` ) func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed uint64, output string, err error) { + pruneFilters := filters.NewArgs() + pruneFilters.Add("dangling", fmt.Sprintf("%v", !opts.all)) + warning := danglingWarning if opts.all { warning = allImageWarning @@ -62,9 +65,7 @@ func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed u return } - report, err := dockerCli.Client().ImagesPrune(context.Background(), types.ImagesPruneConfig{ - DanglingOnly: !opts.all, - }) + report, err := dockerCli.Client().ImagesPrune(context.Background(), pruneFilters) if err != nil { return } diff --git a/components/cli/command/network/prune.go b/components/cli/command/network/prune.go index f2f8cc20c4..9f1979e6b5 100644 --- a/components/cli/command/network/prune.go +++ b/components/cli/command/network/prune.go @@ -5,7 +5,7 @@ import ( "golang.org/x/net/context" - "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/spf13/cobra" @@ -50,7 +50,7 @@ func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (output string, e return } - report, err := dockerCli.Client().NetworksPrune(context.Background(), types.NetworksPruneConfig{}) + report, err := dockerCli.Client().NetworksPrune(context.Background(), filters.Args{}) if err != nil { return } diff --git a/components/cli/command/volume/prune.go b/components/cli/command/volume/prune.go index ac9c94451a..405fbeb295 100644 --- a/components/cli/command/volume/prune.go +++ b/components/cli/command/volume/prune.go @@ -5,7 +5,7 @@ import ( "golang.org/x/net/context" - "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" units "github.com/docker/go-units" @@ -52,7 +52,7 @@ func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed u return } - report, err := dockerCli.Client().VolumesPrune(context.Background(), types.VolumesPruneConfig{}) + report, err := dockerCli.Client().VolumesPrune(context.Background(), filters.Args{}) if err != nil { return } From 11738d5e983947665f289ef2ef9858bd24327d9f Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 16 Nov 2016 21:46:37 -0800 Subject: [PATCH 442/978] 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 Upstream-commit: b3c4bacff29ac669fc38822562b740030bdb3b60 Component: cli --- components/cli/container_prune.go | 10 ++++++++-- components/cli/image_prune.go | 10 ++++++++-- components/cli/interface.go | 8 ++++---- components/cli/network_prune.go | 14 ++++++++++++-- components/cli/utils.go | 20 +++++++++++++++++++- components/cli/volume_prune.go | 10 ++++++++-- 6 files changed, 59 insertions(+), 13 deletions(-) diff --git a/components/cli/container_prune.go b/components/cli/container_prune.go index 3eabe71a7f..b582170867 100644 --- a/components/cli/container_prune.go +++ b/components/cli/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/components/cli/image_prune.go b/components/cli/image_prune.go index d5e69d5b19..5ef98b7f02 100644 --- a/components/cli/image_prune.go +++ b/components/cli/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/components/cli/interface.go b/components/cli/interface.go index 0d722d9075..6319f34f1e 100644 --- a/components/cli/interface.go +++ b/components/cli/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/components/cli/network_prune.go b/components/cli/network_prune.go index 01185f2e02..7352a7f0c5 100644 --- a/components/cli/network_prune.go +++ b/components/cli/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/components/cli/utils.go b/components/cli/utils.go index 03bf4c82fa..23d520ecb8 100644 --- a/components/cli/utils.go +++ b/components/cli/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/components/cli/volume_prune.go b/components/cli/volume_prune.go index ea4e234a30..a07e4ce637 100644 --- a/components/cli/volume_prune.go +++ b/components/cli/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 da195cdf4e366a7ed1212b5be271aec68078ecb7 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Mon, 28 Nov 2016 12:18:15 -0800 Subject: [PATCH 443/978] Fix issue where secret ID is masked by name This fix tries to address the issue in 28884 where it is possible to mask the secret ID by name. The reason was that searching a secret is based on name. However, searching a secret should be done based on: - Full ID - Full Name - Partial ID (prefix) This fix addresses the issue by changing related implementation in `getCliRequestedSecretIDs()` An integration test has been added to cover the changes. This fix fixes 28884 Signed-off-by: Yong Tang Upstream-commit: ff9ff6fdd29a8465f873773f39228bcaccbdc89d Component: cli --- components/cli/command/secret/utils.go | 64 ++++++++++++++++++-------- 1 file changed, 46 insertions(+), 18 deletions(-) diff --git a/components/cli/command/secret/utils.go b/components/cli/command/secret/utils.go index 0134853e09..42493896ca 100644 --- a/components/cli/command/secret/utils.go +++ b/components/cli/command/secret/utils.go @@ -1,6 +1,9 @@ package secret import ( + "fmt" + "strings" + "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/swarm" @@ -8,10 +11,11 @@ import ( "golang.org/x/net/context" ) -func getSecretsByName(ctx context.Context, client client.APIClient, names []string) ([]swarm.Secret, error) { +func getSecretsByNameOrIDPrefixes(ctx context.Context, client client.APIClient, terms []string) ([]swarm.Secret, error) { args := filters.NewArgs() - for _, n := range names { + for _, n := range terms { args.Add("names", n) + args.Add("id", n) } return client.SecretList(ctx, types.SecretListOptions{ @@ -19,29 +23,53 @@ func getSecretsByName(ctx context.Context, client client.APIClient, names []stri }) } -func getCliRequestedSecretIDs(ctx context.Context, client client.APIClient, names []string) ([]string, error) { - ids := names - - // attempt to lookup secret by name - secrets, err := getSecretsByName(ctx, client, ids) +func getCliRequestedSecretIDs(ctx context.Context, client client.APIClient, terms []string) ([]string, error) { + secrets, err := getSecretsByNameOrIDPrefixes(ctx, client, terms) if err != nil { return nil, err } - lookup := make(map[string]struct{}) - for _, id := range ids { - lookup[id] = struct{}{} - } - if len(secrets) > 0 { - ids = []string{} - - for _, s := range secrets { - if _, ok := lookup[s.Spec.Annotations.Name]; ok { - ids = append(ids, s.ID) + found := make(map[string]struct{}) + next: + for _, term := range terms { + // attempt to lookup secret by full ID + for _, s := range secrets { + if s.ID == term { + found[s.ID] = struct{}{} + continue next + } + } + // attempt to lookup secret by full name + for _, s := range secrets { + if s.Spec.Annotations.Name == term { + found[s.ID] = struct{}{} + continue next + } + } + // attempt to lookup secret by partial ID (prefix) + // return error if more than one matches found (ambiguous) + n := 0 + for _, s := range secrets { + if strings.HasPrefix(s.ID, term) { + found[s.ID] = struct{}{} + n++ + } + } + if n > 1 { + return nil, fmt.Errorf("secret %s is ambiguous (%d matches found)", term, n) } } + + // We already collected all the IDs found. + // Now we will remove duplicates by converting the map to slice + ids := []string{} + for id := range found { + ids = append(ids, id) + } + + return ids, nil } - return ids, nil + return terms, nil } From 8986c729f37a1584a3d7aaa523a5c66150c12d38 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 5 Dec 2016 15:18:36 +0100 Subject: [PATCH 444/978] Handle logging in compose to swarm Logging configuration was completely ignore when deploy a compose file to swarm. This fixes it. Signed-off-by: Vincent Demeester Upstream-commit: 68db0a20ddb44f9d3bd3220af7a1556b39baac9c Component: cli --- components/cli/command/stack/deploy.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index e7764f3b8d..1f41cb7d89 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -567,6 +567,14 @@ func convertService( return swarm.ServiceSpec{}, err } + var logDriver *swarm.Driver + if service.Logging != nil { + logDriver = &swarm.Driver{ + Name: service.Logging.Driver, + Options: service.Logging.Options, + } + } + serviceSpec := swarm.ServiceSpec{ Annotations: swarm.Annotations{ Name: name, @@ -589,6 +597,7 @@ func convertService( TTY: service.Tty, OpenStdin: service.StdinOpen, }, + LogDriver: logDriver, Resources: resources, RestartPolicy: restartPolicy, Placement: &swarm.Placement{ From c2a0302e053e262955135cd7bbd18e6ffac700e6 Mon Sep 17 00:00:00 2001 From: unclejack Date: Mon, 5 Dec 2016 17:00:36 +0200 Subject: [PATCH 445/978] api/types/container,client: gofmt Signed-off-by: Cristian Staretu Upstream-commit: ee4988f4b2ec8d5e630a08f2cdd6e1425e66c915 Component: cli --- components/cli/image_search_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/image_search_test.go b/components/cli/image_search_test.go index 108bd96744..b17bbd8343 100644 --- a/components/cli/image_search_test.go +++ b/components/cli/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 e380819b38647b14b496ca665f4fcb59940d0475 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Tue, 6 Dec 2016 11:27:27 -0800 Subject: [PATCH 446/978] registry: Remove reference.go This removes some very old vestigial code that really should have been removed during the content addressability transition. It implements something called "reference" but it behaves differently from the actual reference package. This was only used by client-side content trust code, and is relatively easy to extricate. Signed-off-by: Aaron Lehmann Upstream-commit: a5a246dbbc2a49b0a283409c0c5f879c38ca84db Component: cli --- components/cli/command/image/pull.go | 17 ++------- components/cli/command/image/trust.go | 55 ++++++++++++--------------- 2 files changed, 29 insertions(+), 43 deletions(-) diff --git a/components/cli/command/image/pull.go b/components/cli/command/image/pull.go index 9116d45840..13de492f92 100644 --- a/components/cli/command/image/pull.go +++ b/components/cli/command/image/pull.go @@ -55,16 +55,6 @@ func runPull(dockerCli *command.DockerCli, opts pullOptions) error { fmt.Fprintf(dockerCli.Out(), "Using default tag: %s\n", reference.DefaultTag) } - var tag string - switch x := distributionRef.(type) { - case reference.Canonical: - tag = x.Digest().String() - case reference.NamedTagged: - tag = x.Tag() - } - - registryRef := registry.ParseReference(tag) - // Resolve the Repository name from fqn to RepositoryInfo repoInfo, err := registry.ParseRepositoryInfo(distributionRef) if err != nil { @@ -76,9 +66,10 @@ func runPull(dockerCli *command.DockerCli, opts pullOptions) error { authConfig := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index) requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, "pull") - if command.IsTrusted() && !registryRef.HasDigest() { - // Check if tag is digest - err = trustedPull(ctx, dockerCli, repoInfo, registryRef, authConfig, requestPrivilege) + // Check if reference has a digest + _, isCanonical := distributionRef.(reference.Canonical) + if command.IsTrusted() && !isCanonical { + err = trustedPull(ctx, dockerCli, repoInfo, distributionRef, authConfig, requestPrivilege) } else { err = imagePullPrivileged(ctx, dockerCli, authConfig, distributionRef.String(), requestPrivilege, opts.all) } diff --git a/components/cli/command/image/trust.go b/components/cli/command/image/trust.go index d1106b532e..8f5c76d8ca 100644 --- a/components/cli/command/image/trust.go +++ b/components/cli/command/image/trust.go @@ -46,9 +46,9 @@ var ( ) type target struct { - reference registry.Reference - digest digest.Digest - size int64 + name string + digest digest.Digest + size int64 } // trustedPush handles content trust pushing of an image @@ -81,7 +81,7 @@ func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry target = nil return } - target.Name = registry.ParseReference(pushResult.Tag).String() + target.Name = pushResult.Tag target.Hashes = data.Hashes{string(pushResult.Digest.Algorithm()): h} target.Length = int64(pushResult.Size) } @@ -93,11 +93,9 @@ func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry return errors.New("cannot push a digest reference") case reference.NamedTagged: tag = x.Tag() - } - - // We want trust signatures to always take an explicit tag, - // otherwise it will act as an untrusted push. - if tag == "" { + default: + // We want trust signatures to always take an explicit tag, + // otherwise it will act as an untrusted push. if err = jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), nil); err != nil { return err } @@ -234,7 +232,7 @@ func imagePushPrivileged(ctx context.Context, cli *command.DockerCli, authConfig } // trustedPull handles content trust pulling of an image -func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry.RepositoryInfo, ref registry.Reference, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error { +func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error { var refs []target notaryRepo, err := GetNotaryRepository(cli, repoInfo, authConfig, "pull") @@ -243,7 +241,7 @@ func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry return err } - if ref.String() == "" { + if tagged, isTagged := ref.(reference.NamedTagged); !isTagged { // List all targets targets, err := notaryRepo.ListTargets(releasesRole, data.CanonicalTargetsRole) if err != nil { @@ -266,14 +264,14 @@ func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry return notaryError(repoInfo.FullName(), fmt.Errorf("No trusted tags for %s", repoInfo.FullName())) } } else { - t, err := notaryRepo.GetTargetByName(ref.String(), releasesRole, data.CanonicalTargetsRole) + t, err := notaryRepo.GetTargetByName(tagged.Tag(), releasesRole, data.CanonicalTargetsRole) if err != nil { return notaryError(repoInfo.FullName(), err) } // Only get the tag if it's in the top level targets role or the releases delegation role // ignore it if it's in any other delegation roles if t.Role != releasesRole && t.Role != data.CanonicalTargetsRole { - return notaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", ref.String())) + return notaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", tagged.Tag())) } logrus.Debugf("retrieving target for %s role\n", t.Role) @@ -286,7 +284,7 @@ func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry } for i, r := range refs { - displayTag := r.reference.String() + displayTag := r.name if displayTag != "" { displayTag = ":" + displayTag } @@ -300,19 +298,16 @@ func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry return err } - // If reference is not trusted, tag by trusted reference - if !r.reference.HasDigest() { - tagged, err := reference.WithTag(repoInfo, r.reference.String()) - if err != nil { - return err - } - trustedRef, err := reference.WithDigest(reference.TrimNamed(repoInfo), r.digest) - if err != nil { - return err - } - if err := TagTrusted(ctx, cli, trustedRef, tagged); err != nil { - return err - } + tagged, err := reference.WithTag(repoInfo, r.name) + if err != nil { + return err + } + trustedRef, err := reference.WithDigest(reference.TrimNamed(repoInfo), r.digest) + if err != nil { + return err + } + if err := TagTrusted(ctx, cli, trustedRef, tagged); err != nil { + return err } } return nil @@ -533,9 +528,9 @@ func convertTarget(t client.Target) (target, error) { return target{}, errors.New("no valid hash, expecting sha256") } return target{ - reference: registry.ParseReference(t.Name), - digest: digest.NewDigestFromHex("sha256", hex.EncodeToString(h)), - size: t.Length, + name: t.Name, + digest: digest.NewDigestFromHex("sha256", hex.EncodeToString(h)), + size: t.Length, }, nil } From fe8d13ef3eb3ef202acb62e34a3dfc8c1216aa93 Mon Sep 17 00:00:00 2001 From: Andrea Luzzardi Date: Tue, 6 Dec 2016 18:52:47 -0800 Subject: [PATCH 447/978] service ps: Revert output to 1.12 behavior. - Display the ID column - Do not append the task ID in the name column - (NEW): Truncate task IDs, unless --no-trunc is specified Signed-off-by: Andrea Luzzardi Upstream-commit: 43ed65ee28a64f9b490b9d2df5077a8ccf417f68 Component: cli --- components/cli/command/task/print.go | 33 +++++++++++++++++++--------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/components/cli/command/task/print.go b/components/cli/command/task/print.go index 2995e9afb3..0f1c2cf724 100644 --- a/components/cli/command/task/print.go +++ b/components/cli/command/task/print.go @@ -14,11 +14,12 @@ import ( "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/idresolver" + "github.com/docker/docker/pkg/stringid" "github.com/docker/go-units" ) const ( - psTaskItemFmt = "%s\t%s\t%s\t%s\t%s %s ago\t%s\t%s\n" + psTaskItemFmt = "%s\t%s\t%s\t%s\t%s\t%s %s ago\t%s\t%s\n" maxErrLength = 30 ) @@ -67,7 +68,7 @@ func Print(dockerCli *command.DockerCli, ctx context.Context, tasks []swarm.Task // Ignore flushing errors defer writer.Flush() - fmt.Fprintln(writer, strings.Join([]string{"NAME", "IMAGE", "NODE", "DESIRED STATE", "CURRENT STATE", "ERROR", "PORTS"}, "\t")) + fmt.Fprintln(writer, strings.Join([]string{"ID", "NAME", "IMAGE", "NODE", "DESIRED STATE", "CURRENT STATE", "ERROR", "PORTS"}, "\t")) if err := print(writer, ctx, tasks, resolver, noTrunc); err != nil { return err @@ -90,25 +91,36 @@ func PrintQuiet(dockerCli *command.DockerCli, tasks []swarm.Task) error { } func print(out io.Writer, ctx context.Context, tasks []swarm.Task, resolver *idresolver.IDResolver, noTrunc bool) error { - prevService := "" - prevSlot := 0 + prevName := "" for _, task := range tasks { - name, err := resolver.Resolve(ctx, task, task.ID) + id := task.ID + if !noTrunc { + id = stringid.TruncateID(id) + } + + serviceName, err := resolver.Resolve(ctx, swarm.Service{}, task.ServiceID) + if err != nil { + return err + } nodeValue, err := resolver.Resolve(ctx, swarm.Node{}, task.NodeID) if err != nil { return err } + name := "" + if task.Slot != 0 { + name = fmt.Sprintf("%v.%v", serviceName, task.Slot) + } else { + name = fmt.Sprintf("%v.%v", serviceName, task.NodeID) + } + // Indent the name if necessary indentedName := name - // Since the new format of the task name is .., we should only compare - // and here. - if prevService == task.ServiceID && prevSlot == task.Slot { + if name == prevName { indentedName = fmt.Sprintf(" \\_ %s", indentedName) } - prevService = task.ServiceID - prevSlot = task.Slot + prevName = name // Trim and quote the error message. taskErr := task.Status.Err @@ -134,6 +146,7 @@ func print(out io.Writer, ctx context.Context, tasks []swarm.Task, resolver *idr fmt.Fprintf( out, psTaskItemFmt, + id, indentedName, image, nodeValue, From 94a2133e92fc57e3f9bccfa1076524d7e940802f Mon Sep 17 00:00:00 2001 From: Doug Davis Date: Sat, 3 Dec 2016 05:46:04 -0800 Subject: [PATCH 448/978] 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 Upstream-commit: f1801ba475fe5a7c51b6303ad481d0dd52a56d10 Component: cli --- components/cli/command/image/build.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go index 78cc41494f..1699b2c45c 100644 --- a/components/cli/command/image/build.go +++ b/components/cli/command/image/build.go @@ -67,7 +67,7 @@ func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command { ulimits := make(map[string]*units.Ulimit) options := buildOptions{ tags: opts.NewListOpts(validateTag), - buildArgs: opts.NewListOpts(runconfigopts.ValidateArg), + buildArgs: opts.NewListOpts(runconfigopts.ValidateEnv), ulimits: runconfigopts.NewUlimitOpt(&ulimits), labels: opts.NewListOpts(runconfigopts.ValidateEnv), } @@ -300,7 +300,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { Dockerfile: relDockerfile, ShmSize: shmSize, Ulimits: options.ulimits.GetList(), - BuildArgs: runconfigopts.ConvertKVStringsToMap(options.buildArgs.GetAll()), + BuildArgs: runconfigopts.ConvertKVStringsToMapWithNil(options.buildArgs.GetAll()), AuthConfigs: authConfigs, Labels: runconfigopts.ConvertKVStringsToMap(options.labels.GetAll()), CacheFrom: options.cacheFrom, From f55c5f05981971489f90eac7ba90c3a976a3fc65 Mon Sep 17 00:00:00 2001 From: Doug Davis Date: Sat, 3 Dec 2016 05:46:04 -0800 Subject: [PATCH 449/978] 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 Upstream-commit: 259859289ba534e917993b1682d11703bf3403ad Component: cli --- components/cli/image_build_test.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/components/cli/image_build_test.go b/components/cli/image_build_test.go index ec0cbe2ee4..b9d04f817a 100644 --- a/components/cli/image_build_test.go +++ b/components/cli/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 e967701d4935d1972abaf706978b6d116cd538fe Mon Sep 17 00:00:00 2001 From: wefine Date: Fri, 2 Dec 2016 00:08:24 +0800 Subject: [PATCH 450/978] Give a order to AddCommands, for easy read and maintenance. Signed-off-by: wefine Upstream-commit: e41cf4a8600631b3fe90212e1c12ca4d5d4ad132 Component: cli --- components/cli/command/commands/commands.go | 54 ++++++++++++++++----- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/components/cli/command/commands/commands.go b/components/cli/command/commands/commands.go index d64d5680cc..0db7f3a409 100644 --- a/components/cli/command/commands/commands.go +++ b/components/cli/command/commands/commands.go @@ -23,23 +23,55 @@ import ( // AddCommands adds all the commands from cli/command to the root command func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) { cmd.AddCommand( - node.NewNodeCommand(dockerCli), - service.NewServiceCommand(dockerCli), - swarm.NewSwarmCommand(dockerCli), - secret.NewSecretCommand(dockerCli), + // checkpoint + checkpoint.NewCheckpointCommand(dockerCli), + + // container container.NewContainerCommand(dockerCli), - image.NewImageCommand(dockerCli), - system.NewSystemCommand(dockerCli), container.NewRunCommand(dockerCli), + + // image + image.NewImageCommand(dockerCli), image.NewBuildCommand(dockerCli), + + // node + node.NewNodeCommand(dockerCli), + + // network network.NewNetworkCommand(dockerCli), - hide(system.NewEventsCommand(dockerCli)), + + // plugin + plugin.NewPluginCommand(dockerCli), + + // registry registry.NewLoginCommand(dockerCli), registry.NewLogoutCommand(dockerCli), registry.NewSearchCommand(dockerCli), + + // secret + secret.NewSecretCommand(dockerCli), + + // service + service.NewServiceCommand(dockerCli), + + // system + system.NewSystemCommand(dockerCli), system.NewVersionCommand(dockerCli), + + // stack + stack.NewStackCommand(dockerCli), + stack.NewTopLevelDeployCommand(dockerCli), + + // swarm + swarm.NewSwarmCommand(dockerCli), + + // volume volume.NewVolumeCommand(dockerCli), + + // legacy commands may be hidden + hide(system.NewEventsCommand(dockerCli)), hide(system.NewInfoCommand(dockerCli)), + hide(system.NewInspectCommand(dockerCli)), hide(container.NewAttachCommand(dockerCli)), hide(container.NewCommitCommand(dockerCli)), hide(container.NewCopyCommand(dockerCli)), @@ -71,16 +103,14 @@ func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) { hide(image.NewRemoveCommand(dockerCli)), hide(image.NewSaveCommand(dockerCli)), hide(image.NewTagCommand(dockerCli)), - hide(system.NewInspectCommand(dockerCli)), - stack.NewStackCommand(dockerCli), - stack.NewTopLevelDeployCommand(dockerCli), - checkpoint.NewCheckpointCommand(dockerCli), - plugin.NewPluginCommand(dockerCli), ) } func hide(cmd *cobra.Command) *cobra.Command { + // If the environment variable with name "DOCKER_HIDE_LEGACY_COMMANDS" is not empty, + // these legacy commands (such as `docker ps`, `docker exec`, etc) + // will not be shown in output console. if os.Getenv("DOCKER_HIDE_LEGACY_COMMANDS") == "" { return cmd } From 4b41f0567126b9ce72d58c7510313ff733ad2913 Mon Sep 17 00:00:00 2001 From: John Howard Date: Fri, 9 Dec 2016 14:21:45 -0800 Subject: [PATCH 451/978] Windows: Prompt fix Signed-off-by: John Howard Upstream-commit: 1da163febe0109b6c719b218dfc070bd52b76280 Component: cli --- components/cli/command/utils.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/components/cli/command/utils.go b/components/cli/command/utils.go index 9f9a1ee80d..1837ca41f0 100644 --- a/components/cli/command/utils.go +++ b/components/cli/command/utils.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "os" "path/filepath" + "runtime" "strings" ) @@ -71,6 +72,11 @@ func PromptForConfirmation(ins *InStream, outs *OutStream, message string) bool fmt.Fprintf(outs, message) + // On Windows, force the use of the regular OS stdin stream. + if runtime.GOOS == "windows" { + ins = NewInStream(os.Stdin) + } + answer := "" n, _ := fmt.Fscan(ins, &answer) if n != 1 || (answer != "y" && answer != "Y") { From eef5b8fb30d34402d14c3b54f135fec2457e7eab Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Thu, 8 Dec 2016 22:32:10 +0100 Subject: [PATCH 452/978] Remove --port and update --publish for services to support syntaxes Add support for simple and complex syntax to `--publish` through the use of `PortOpt`. Signed-off-by: Vincent Demeester Upstream-commit: 7fbc616b4779d6290afcb7b97709eb784424df0b Component: cli --- components/cli/command/service/create.go | 2 - components/cli/command/service/opts.go | 54 +++---------- components/cli/command/service/update.go | 79 ++++++------------- components/cli/command/service/update_test.go | 1 + components/cli/command/stack/deploy.go | 3 +- 5 files changed, 37 insertions(+), 102 deletions(-) diff --git a/components/cli/command/service/create.go b/components/cli/command/service/create.go index ea078e43ad..a8382835a0 100644 --- a/components/cli/command/service/create.go +++ b/components/cli/command/service/create.go @@ -40,13 +40,11 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Var(&opts.networks, flagNetwork, "Network attachments") flags.Var(&opts.secrets, flagSecret, "Specify secrets to expose to the service") flags.VarP(&opts.endpoint.publishPorts, flagPublish, "p", "Publish a port as a node port") - flags.MarkHidden(flagPublish) flags.Var(&opts.groups, flagGroup, "Set one or more supplementary user groups for the container") flags.Var(&opts.dns, flagDNS, "Set custom DNS servers") flags.Var(&opts.dnsOption, flagDNSOption, "Set DNS options") flags.Var(&opts.dnsSearch, flagDNSSearch, "Set custom DNS search domains") flags.Var(&opts.hosts, flagHost, "Set one or more custom host-to-IP mappings (host:ip)") - flags.Var(&opts.endpoint.expandedPorts, flagPort, "Publish a port") flags.SetInterspersed(false) return cmd diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index 023b922a15..c7518e5976 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -287,45 +287,17 @@ func convertNetworks(networks []string) []swarm.NetworkAttachmentConfig { } type endpointOptions struct { - mode string - publishPorts opts.ListOpts - expandedPorts opts.PortOpt + mode string + publishPorts opts.PortOpt } func (e *endpointOptions) ToEndpointSpec() *swarm.EndpointSpec { - portConfigs := []swarm.PortConfig{} - // We can ignore errors because the format was already validated by ValidatePort - ports, portBindings, _ := nat.ParsePortSpecs(e.publishPorts.GetAll()) - - for port := range ports { - portConfigs = append(portConfigs, ConvertPortToPortConfig(port, portBindings)...) - } - return &swarm.EndpointSpec{ Mode: swarm.ResolutionMode(strings.ToLower(e.mode)), - Ports: append(portConfigs, e.expandedPorts.Value()...), + Ports: e.publishPorts.Value(), } } -// ConvertPortToPortConfig converts ports to the swarm type -func ConvertPortToPortConfig( - port nat.Port, - portBindings map[nat.Port][]nat.PortBinding, -) []swarm.PortConfig { - ports := []swarm.PortConfig{} - - for _, binding := range portBindings[port] { - hostPort, _ := strconv.ParseUint(binding.HostPort, 10, 16) - ports = append(ports, swarm.PortConfig{ - //TODO Name: ? - Protocol: swarm.PortConfigProtocol(strings.ToLower(port.Proto())), - TargetPort: uint32(port.Int()), - PublishedPort: uint32(hostPort), - }) - } - return ports -} - type logDriverOptions struct { name string opts opts.ListOpts @@ -459,16 +431,13 @@ func newServiceOptions() *serviceOptions { containerLabels: opts.NewListOpts(runconfigopts.ValidateEnv), env: opts.NewListOpts(runconfigopts.ValidateEnv), envFile: opts.NewListOpts(nil), - endpoint: endpointOptions{ - publishPorts: opts.NewListOpts(ValidatePort), - }, - groups: opts.NewListOpts(nil), - logDriver: newLogDriverOptions(), - dns: opts.NewListOpts(opts.ValidateIPAddress), - dnsOption: opts.NewListOpts(nil), - dnsSearch: opts.NewListOpts(opts.ValidateDNSSearch), - hosts: opts.NewListOpts(runconfigopts.ValidateExtraHost), - networks: opts.NewListOpts(nil), + groups: opts.NewListOpts(nil), + logDriver: newLogDriverOptions(), + dns: opts.NewListOpts(opts.ValidateIPAddress), + dnsOption: opts.NewListOpts(nil), + dnsSearch: opts.NewListOpts(opts.ValidateDNSSearch), + hosts: opts.NewListOpts(runconfigopts.ValidateExtraHost), + networks: opts.NewListOpts(nil), } } @@ -649,9 +618,6 @@ const ( flagPublish = "publish" flagPublishRemove = "publish-rm" flagPublishAdd = "publish-add" - flagPort = "port" - flagPortAdd = "port-add" - flagPortRemove = "port-rm" flagReplicas = "replicas" flagReserveCPU = "reserve-cpu" flagReserveMemory = "reserve-memory" diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index 200f58c3a6..2ceaf275ab 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -24,7 +24,7 @@ import ( ) func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := newServiceOptions() + serviceOpts := newServiceOptions() cmd := &cobra.Command{ Use: "update [OPTIONS] SERVICE", @@ -40,36 +40,33 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.String("args", "", "Service command args") flags.Bool("rollback", false, "Rollback to previous specification") flags.Bool("force", false, "Force update even if no changes require it") - addServiceFlags(cmd, opts) + addServiceFlags(cmd, serviceOpts) flags.Var(newListOptsVar(), flagEnvRemove, "Remove an environment variable") flags.Var(newListOptsVar(), flagGroupRemove, "Remove a previously added supplementary user group from the container") flags.Var(newListOptsVar(), flagLabelRemove, "Remove a label by its key") flags.Var(newListOptsVar(), flagContainerLabelRemove, "Remove a container label by its key") flags.Var(newListOptsVar(), flagMountRemove, "Remove a mount by its target path") - flags.Var(newListOptsVar().WithValidator(validatePublishRemove), flagPublishRemove, "Remove a published port by its target port") - flags.MarkHidden(flagPublishRemove) - flags.Var(newListOptsVar(), flagPortRemove, "Remove a port(target-port mandatory)") + // flags.Var(newListOptsVar().WithValidator(validatePublishRemove), flagPublishRemove, "Remove a published port by its target port") + flags.Var(&opts.PortOpt{}, flagPublishRemove, "Remove a published port by its target port") flags.Var(newListOptsVar(), flagConstraintRemove, "Remove a constraint") flags.Var(newListOptsVar(), flagDNSRemove, "Remove a custom DNS server") flags.Var(newListOptsVar(), flagDNSOptionRemove, "Remove a DNS option") flags.Var(newListOptsVar(), flagDNSSearchRemove, "Remove a DNS search domain") flags.Var(newListOptsVar(), flagHostRemove, "Remove a custom host-to-IP mapping (host:ip)") - flags.Var(&opts.labels, flagLabelAdd, "Add or update a service label") - flags.Var(&opts.containerLabels, flagContainerLabelAdd, "Add or update a container label") - flags.Var(&opts.env, flagEnvAdd, "Add or update an environment variable") + flags.Var(&serviceOpts.labels, flagLabelAdd, "Add or update a service label") + flags.Var(&serviceOpts.containerLabels, flagContainerLabelAdd, "Add or update a container label") + flags.Var(&serviceOpts.env, flagEnvAdd, "Add or update an environment variable") flags.Var(newListOptsVar(), flagSecretRemove, "Remove a secret") - flags.Var(&opts.secrets, flagSecretAdd, "Add or update a secret on a service") - flags.Var(&opts.mounts, flagMountAdd, "Add or update a mount on a service") - flags.Var(&opts.constraints, flagConstraintAdd, "Add or update a placement constraint") - flags.Var(&opts.endpoint.publishPorts, flagPublishAdd, "Add or update a published port") - flags.MarkHidden(flagPublishAdd) - flags.Var(&opts.endpoint.expandedPorts, flagPortAdd, "Add or update a port") - flags.Var(&opts.groups, flagGroupAdd, "Add an additional supplementary user group to the container") - flags.Var(&opts.dns, flagDNSAdd, "Add or update a custom DNS server") - flags.Var(&opts.dnsOption, flagDNSOptionAdd, "Add or update a DNS option") - flags.Var(&opts.dnsSearch, flagDNSSearchAdd, "Add or update a custom DNS search domain") - flags.Var(&opts.hosts, flagHostAdd, "Add or update a custom host-to-IP mapping (host:ip)") + flags.Var(&serviceOpts.secrets, flagSecretAdd, "Add or update a secret on a service") + flags.Var(&serviceOpts.mounts, flagMountAdd, "Add or update a mount on a service") + flags.Var(&serviceOpts.constraints, flagConstraintAdd, "Add or update a placement constraint") + flags.Var(&serviceOpts.endpoint.publishPorts, flagPublishAdd, "Add or update a published port") + flags.Var(&serviceOpts.groups, flagGroupAdd, "Add an additional supplementary user group to the container") + flags.Var(&serviceOpts.dns, flagDNSAdd, "Add or update a custom DNS server") + flags.Var(&serviceOpts.dnsOption, flagDNSOptionAdd, "Add or update a DNS option") + flags.Var(&serviceOpts.dnsSearch, flagDNSSearchAdd, "Add or update a custom DNS search domain") + flags.Var(&serviceOpts.hosts, flagHostAdd, "Add or update a custom host-to-IP mapping (host:ip)") return cmd } @@ -276,7 +273,7 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error { } } - if anyChanged(flags, flagPublishAdd, flagPublishRemove, flagPortAdd, flagPortRemove) { + if anyChanged(flags, flagPublishAdd, flagPublishRemove) { if spec.EndpointSpec == nil { spec.EndpointSpec = &swarm.EndpointSpec{} } @@ -645,6 +642,7 @@ func portConfigToString(portConfig *swarm.PortConfig) string { return fmt.Sprintf("%v:%v/%s/%s", portConfig.PublishedPort, portConfig.TargetPort, protocol, mode) } +// FIXME(vdemeester) port to opts.PortOpt // This validation is only used for `--publish-rm`. // The `--publish-rm` takes: // [/] (e.g., 80, 80/tcp, 53/udp) @@ -667,26 +665,13 @@ func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) error { portSet := map[string]swarm.PortConfig{} // Check to see if there are any conflict in flags. if flags.Changed(flagPublishAdd) { - values := flags.Lookup(flagPublishAdd).Value.(*opts.ListOpts).GetAll() - ports, portBindings, _ := nat.ParsePortSpecs(values) + ports := flags.Lookup(flagPublishAdd).Value.(*opts.PortOpt).Value() - for port := range ports { - newConfigs := ConvertPortToPortConfig(port, portBindings) - for _, entry := range newConfigs { - if v, ok := portSet[portConfigToString(&entry)]; ok && v != entry { - return fmt.Errorf("conflicting port mapping between %v:%v/%s and %v:%v/%s", entry.PublishedPort, entry.TargetPort, entry.Protocol, v.PublishedPort, v.TargetPort, v.Protocol) - } - portSet[portConfigToString(&entry)] = entry + for _, port := range ports { + if v, ok := portSet[portConfigToString(&port)]; ok && v != port { + return fmt.Errorf("conflicting port mapping between %v:%v/%s and %v:%v/%s", port.PublishedPort, port.TargetPort, port.Protocol, v.PublishedPort, v.TargetPort, v.Protocol) } - } - } - - if flags.Changed(flagPortAdd) { - for _, entry := range flags.Lookup(flagPortAdd).Value.(*opts.PortOpt).Value() { - if v, ok := portSet[portConfigToString(&entry)]; ok && v != entry { - return fmt.Errorf("conflicting port mapping between %v:%v/%s and %v:%v/%s", entry.PublishedPort, entry.TargetPort, entry.Protocol, v.PublishedPort, v.TargetPort, v.Protocol) - } - portSet[portConfigToString(&entry)] = entry + portSet[portConfigToString(&port)] = port } } @@ -697,26 +682,12 @@ func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) error { } } - toRemove := flags.Lookup(flagPublishRemove).Value.(*opts.ListOpts).GetAll() - removePortCSV := flags.Lookup(flagPortRemove).Value.(*opts.ListOpts).GetAll() - removePortOpts := &opts.PortOpt{} - for _, portCSV := range removePortCSV { - if err := removePortOpts.Set(portCSV); err != nil { - return err - } - } + toRemove := flags.Lookup(flagPublishRemove).Value.(*opts.PortOpt).Value() newPorts := []swarm.PortConfig{} portLoop: for _, port := range portSet { - for _, rawTargetPort := range toRemove { - targetPort := nat.Port(rawTargetPort) - if equalPort(targetPort, port) { - continue portLoop - } - } - - for _, pConfig := range removePortOpts.Value() { + for _, pConfig := range toRemove { if equalProtocol(port.Protocol, pConfig.Protocol) && port.TargetPort == pConfig.TargetPort && equalPublishMode(port.PublishMode, pConfig.PublishMode) { diff --git a/components/cli/command/service/update_test.go b/components/cli/command/service/update_test.go index bb2e9c1073..3cb7657996 100644 --- a/components/cli/command/service/update_test.go +++ b/components/cli/command/service/update_test.go @@ -364,6 +364,7 @@ func TestUpdatePortsRmWithProtocol(t *testing.T) { assert.Equal(t, portConfigs[0].TargetPort, uint32(82)) } +// FIXME(vdemeester) port to opts.PortOpt func TestValidatePort(t *testing.T) { validPorts := []string{"80/tcp", "80", "80/udp"} invalidPorts := map[string]string{ diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index 1f41cb7d89..00a7634a0a 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -21,7 +21,6 @@ import ( "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" - servicecmd "github.com/docker/docker/cli/command/service" dockerclient "github.com/docker/docker/client" "github.com/docker/docker/opts" runconfigopts "github.com/docker/docker/runconfig/opts" @@ -745,7 +744,7 @@ func convertEndpointSpec(source []string) (*swarm.EndpointSpec, error) { for port := range ports { portConfigs = append( portConfigs, - servicecmd.ConvertPortToPortConfig(port, portBindings)...) + opts.ConvertPortToPortConfig(port, portBindings)...) } return &swarm.EndpointSpec{Ports: portConfigs}, nil From d4d9c597561c09a4c5b35e78946343629b809d58 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 9 Dec 2016 21:17:57 +0100 Subject: [PATCH 453/978] Make --publish-rm precedes --publish-add, so that add wins `--publish-add 8081:81 --publish-add 8082:82 --publish-rm 80 --publish-rm 81/tcp --publish-rm 82/tcp` would thus result in 81 and 82 to be published. Signed-off-by: Vincent Demeester Upstream-commit: b4a6d83dc278b9a8cf0c83b4f3685320eed4deb4 Component: cli --- components/cli/command/service/update.go | 43 +++++++++---------- components/cli/command/service/update_test.go | 36 +++++++--------- 2 files changed, 37 insertions(+), 42 deletions(-) diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index 2ceaf275ab..4bbcf35a8d 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -630,15 +630,7 @@ func (r byPortConfig) Less(i, j int) bool { func portConfigToString(portConfig *swarm.PortConfig) string { protocol := portConfig.Protocol - if protocol == "" { - protocol = "tcp" - } - mode := portConfig.PublishMode - if mode == "" { - mode = "ingress" - } - return fmt.Sprintf("%v:%v/%s/%s", portConfig.PublishedPort, portConfig.TargetPort, protocol, mode) } @@ -663,28 +655,18 @@ func validatePublishRemove(val string) (string, error) { func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) error { // The key of the map is `port/protocol`, e.g., `80/tcp` portSet := map[string]swarm.PortConfig{} - // Check to see if there are any conflict in flags. - if flags.Changed(flagPublishAdd) { - ports := flags.Lookup(flagPublishAdd).Value.(*opts.PortOpt).Value() - for _, port := range ports { - if v, ok := portSet[portConfigToString(&port)]; ok && v != port { - return fmt.Errorf("conflicting port mapping between %v:%v/%s and %v:%v/%s", port.PublishedPort, port.TargetPort, port.Protocol, v.PublishedPort, v.TargetPort, v.Protocol) - } - portSet[portConfigToString(&port)] = port - } - } - - // Override previous PortConfig in service if there is any duplicate + // Build the current list of portConfig for _, entry := range *portConfig { if _, ok := portSet[portConfigToString(&entry)]; !ok { portSet[portConfigToString(&entry)] = entry } } - toRemove := flags.Lookup(flagPublishRemove).Value.(*opts.PortOpt).Value() - newPorts := []swarm.PortConfig{} + + // Clean current ports + toRemove := flags.Lookup(flagPublishRemove).Value.(*opts.PortOpt).Value() portLoop: for _, port := range portSet { for _, pConfig := range toRemove { @@ -698,6 +680,23 @@ portLoop: newPorts = append(newPorts, port) } + // Check to see if there are any conflict in flags. + if flags.Changed(flagPublishAdd) { + ports := flags.Lookup(flagPublishAdd).Value.(*opts.PortOpt).Value() + + for _, port := range ports { + if v, ok := portSet[portConfigToString(&port)]; ok { + if v != port { + fmt.Println("v", v) + return fmt.Errorf("conflicting port mapping between %v:%v/%s and %v:%v/%s", port.PublishedPort, port.TargetPort, port.Protocol, v.PublishedPort, v.TargetPort, v.Protocol) + } + continue + } + //portSet[portConfigToString(&port)] = port + newPorts = append(newPorts, port) + } + } + // Sort the PortConfig to avoid unnecessary updates sort.Sort(byPortConfig(newPorts)) *portConfig = newPorts diff --git a/components/cli/command/service/update_test.go b/components/cli/command/service/update_test.go index 3cb7657996..08fe248769 100644 --- a/components/cli/command/service/update_test.go +++ b/components/cli/command/service/update_test.go @@ -220,28 +220,18 @@ func TestUpdatePorts(t *testing.T) { assert.Equal(t, targetPorts[1], 1000) } -func TestUpdatePortsDuplicateEntries(t *testing.T) { +func TestUpdatePortsDuplicate(t *testing.T) { // Test case for #25375 flags := newUpdateCommand(nil).Flags() flags.Set("publish-add", "80:80") portConfigs := []swarm.PortConfig{ - {TargetPort: 80, PublishedPort: 80}, - } - - err := updatePorts(flags, &portConfigs) - assert.Equal(t, err, nil) - assert.Equal(t, len(portConfigs), 1) - assert.Equal(t, portConfigs[0].TargetPort, uint32(80)) -} - -func TestUpdatePortsDuplicateKeys(t *testing.T) { - // Test case for #25375 - flags := newUpdateCommand(nil).Flags() - flags.Set("publish-add", "80:80") - - portConfigs := []swarm.PortConfig{ - {TargetPort: 80, PublishedPort: 80}, + { + TargetPort: 80, + PublishedPort: 80, + Protocol: swarm.PortConfigProtocolTCP, + PublishMode: swarm.PortConfigPublishModeIngress, + }, } err := updatePorts(flags, &portConfigs) @@ -355,13 +345,19 @@ func TestUpdatePortsRmWithProtocol(t *testing.T) { flags.Set("publish-rm", "82/udp") portConfigs := []swarm.PortConfig{ - {TargetPort: 80, PublishedPort: 8080, Protocol: swarm.PortConfigProtocolTCP}, + { + TargetPort: 80, + PublishedPort: 8080, + Protocol: swarm.PortConfigProtocolTCP, + PublishMode: swarm.PortConfigPublishModeIngress, + }, } err := updatePorts(flags, &portConfigs) assert.Equal(t, err, nil) - assert.Equal(t, len(portConfigs), 1) - assert.Equal(t, portConfigs[0].TargetPort, uint32(82)) + assert.Equal(t, len(portConfigs), 2) + assert.Equal(t, portConfigs[0].TargetPort, uint32(81)) + assert.Equal(t, portConfigs[1].TargetPort, uint32(82)) } // FIXME(vdemeester) port to opts.PortOpt From 6f40e3addd149549eded48ac84079e76de189abf Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 12 Dec 2016 09:33:58 +0100 Subject: [PATCH 454/978] Move debug functions to cli/debug package Signed-off-by: Vincent Demeester Upstream-commit: a51750a650e073bdc88f084baee68f27297b153e Component: cli --- components/cli/command/system/info.go | 4 +-- components/cli/debug/debug.go | 26 ++++++++++++++++ components/cli/debug/debug_test.go | 43 +++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 components/cli/debug/debug.go create mode 100644 components/cli/debug/debug_test.go diff --git a/components/cli/command/system/info.go b/components/cli/command/system/info.go index e0b8767377..6c3487de8a 100644 --- a/components/cli/command/system/info.go +++ b/components/cli/command/system/info.go @@ -12,8 +12,8 @@ import ( "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/debug" "github.com/docker/docker/pkg/ioutils" - "github.com/docker/docker/utils" "github.com/docker/docker/utils/templates" "github.com/docker/go-units" "github.com/spf13/cobra" @@ -206,7 +206,7 @@ func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error { ioutils.FprintfIfNotEmpty(dockerCli.Out(), "Name: %s\n", info.Name) ioutils.FprintfIfNotEmpty(dockerCli.Out(), "ID: %s\n", info.ID) fmt.Fprintf(dockerCli.Out(), "Docker Root Dir: %s\n", info.DockerRootDir) - fmt.Fprintf(dockerCli.Out(), "Debug Mode (client): %v\n", utils.IsDebugEnabled()) + fmt.Fprintf(dockerCli.Out(), "Debug Mode (client): %v\n", debug.IsEnabled()) fmt.Fprintf(dockerCli.Out(), "Debug Mode (server): %v\n", info.Debug) if info.Debug { diff --git a/components/cli/debug/debug.go b/components/cli/debug/debug.go new file mode 100644 index 0000000000..51dfab2a97 --- /dev/null +++ b/components/cli/debug/debug.go @@ -0,0 +1,26 @@ +package debug + +import ( + "os" + + "github.com/Sirupsen/logrus" +) + +// Enable sets the DEBUG env var to true +// and makes the logger to log at debug level. +func Enable() { + os.Setenv("DEBUG", "1") + logrus.SetLevel(logrus.DebugLevel) +} + +// Disable sets the DEBUG env var to false +// and makes the logger to log at info level. +func Disable() { + os.Setenv("DEBUG", "") + logrus.SetLevel(logrus.InfoLevel) +} + +// IsEnabled checks whether the debug flag is set or not. +func IsEnabled() bool { + return os.Getenv("DEBUG") != "" +} diff --git a/components/cli/debug/debug_test.go b/components/cli/debug/debug_test.go new file mode 100644 index 0000000000..ad8412a944 --- /dev/null +++ b/components/cli/debug/debug_test.go @@ -0,0 +1,43 @@ +package debug + +import ( + "os" + "testing" + + "github.com/Sirupsen/logrus" +) + +func TestEnable(t *testing.T) { + defer func() { + os.Setenv("DEBUG", "") + logrus.SetLevel(logrus.InfoLevel) + }() + Enable() + if os.Getenv("DEBUG") != "1" { + t.Fatalf("expected DEBUG=1, got %s\n", os.Getenv("DEBUG")) + } + if logrus.GetLevel() != logrus.DebugLevel { + t.Fatalf("expected log level %v, got %v\n", logrus.DebugLevel, logrus.GetLevel()) + } +} + +func TestDisable(t *testing.T) { + Disable() + if os.Getenv("DEBUG") != "" { + t.Fatalf("expected DEBUG=\"\", got %s\n", os.Getenv("DEBUG")) + } + if logrus.GetLevel() != logrus.InfoLevel { + t.Fatalf("expected log level %v, got %v\n", logrus.InfoLevel, logrus.GetLevel()) + } +} + +func TestEnabled(t *testing.T) { + Enable() + if !IsEnabled() { + t.Fatal("expected debug enabled, got false") + } + Disable() + if IsEnabled() { + t.Fatal("expected debug disabled, got true") + } +} From e670b0366835e4dd2f2bae83c593f0dd65728c56 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 12 Dec 2016 09:33:58 +0100 Subject: [PATCH 455/978] Move debug functions to cli/debug package Signed-off-by: Vincent Demeester Upstream-commit: 26c5b4b7b8eca61a447ce18e928ec84d220eca51 Component: cli --- components/cli/docker.go | 4 ++-- components/cli/docker_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/components/cli/docker.go b/components/cli/docker.go index d4847a90ee..f4033738b7 100644 --- a/components/cli/docker.go +++ b/components/cli/docker.go @@ -10,11 +10,11 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/commands" + "github.com/docker/docker/cli/debug" cliflags "github.com/docker/docker/cli/flags" "github.com/docker/docker/cliconfig" "github.com/docker/docker/dockerversion" "github.com/docker/docker/pkg/term" - "github.com/docker/docker/utils" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -130,7 +130,7 @@ func dockerPreRun(opts *cliflags.ClientOptions) { } if opts.Common.Debug { - utils.EnableDebug() + debug.Enable() } } diff --git a/components/cli/docker_test.go b/components/cli/docker_test.go index 8738f6005d..f8a5297ed4 100644 --- a/components/cli/docker_test.go +++ b/components/cli/docker_test.go @@ -7,12 +7,12 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/debug" "github.com/docker/docker/pkg/testutil/assert" - "github.com/docker/docker/utils" ) func TestClientDebugEnabled(t *testing.T) { - defer utils.DisableDebug() + defer debug.Disable() cmd := newDockerCommand(&command.DockerCli{}) cmd.Flags().Set("debug", "true") From 8f86ac53c16e693c9f9b778f41aa1eccaf6f69ff Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 12 Dec 2016 09:34:03 +0100 Subject: [PATCH 456/978] Move templates to pkg/templates Signed-off-by: Vincent Demeester Upstream-commit: 26fca512dd6d99a5b8fa144676eb6a3cdaa4da90 Component: cli --- components/cli/command/container/list.go | 2 +- components/cli/command/formatter/formatter.go | 2 +- components/cli/command/inspect/inspector.go | 2 +- components/cli/command/inspect/inspector_test.go | 2 +- components/cli/command/system/events.go | 2 +- components/cli/command/system/info.go | 2 +- components/cli/command/system/version.go | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/components/cli/command/container/list.go b/components/cli/command/container/list.go index 60c2462986..5104e9b6c0 100644 --- a/components/cli/command/container/list.go +++ b/components/cli/command/container/list.go @@ -10,7 +10,7 @@ import ( "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/formatter" "github.com/docker/docker/opts" - "github.com/docker/docker/utils/templates" + "github.com/docker/docker/pkg/templates" "github.com/spf13/cobra" ) diff --git a/components/cli/command/formatter/formatter.go b/components/cli/command/formatter/formatter.go index e859a1ca26..4345f7c3bc 100644 --- a/components/cli/command/formatter/formatter.go +++ b/components/cli/command/formatter/formatter.go @@ -8,7 +8,7 @@ import ( "text/tabwriter" "text/template" - "github.com/docker/docker/utils/templates" + "github.com/docker/docker/pkg/templates" ) // Format keys used to specify certain kinds of output formats diff --git a/components/cli/command/inspect/inspector.go b/components/cli/command/inspect/inspector.go index 1d81643fb1..1e53671f84 100644 --- a/components/cli/command/inspect/inspector.go +++ b/components/cli/command/inspect/inspector.go @@ -9,7 +9,7 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/docker/cli" - "github.com/docker/docker/utils/templates" + "github.com/docker/docker/pkg/templates" ) // Inspector defines an interface to implement to process elements diff --git a/components/cli/command/inspect/inspector_test.go b/components/cli/command/inspect/inspector_test.go index 1ce1593ab7..9085230ac5 100644 --- a/components/cli/command/inspect/inspector_test.go +++ b/components/cli/command/inspect/inspector_test.go @@ -5,7 +5,7 @@ import ( "strings" "testing" - "github.com/docker/docker/utils/templates" + "github.com/docker/docker/pkg/templates" ) type testElement struct { diff --git a/components/cli/command/system/events.go b/components/cli/command/system/events.go index 087523051a..441ef91d33 100644 --- a/components/cli/command/system/events.go +++ b/components/cli/command/system/events.go @@ -17,7 +17,7 @@ import ( "github.com/docker/docker/cli/command" "github.com/docker/docker/opts" "github.com/docker/docker/pkg/jsonlog" - "github.com/docker/docker/utils/templates" + "github.com/docker/docker/pkg/templates" "github.com/spf13/cobra" ) diff --git a/components/cli/command/system/info.go b/components/cli/command/system/info.go index 6c3487de8a..e11aff77b4 100644 --- a/components/cli/command/system/info.go +++ b/components/cli/command/system/info.go @@ -14,7 +14,7 @@ import ( "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/debug" "github.com/docker/docker/pkg/ioutils" - "github.com/docker/docker/utils/templates" + "github.com/docker/docker/pkg/templates" "github.com/docker/go-units" "github.com/spf13/cobra" ) diff --git a/components/cli/command/system/version.go b/components/cli/command/system/version.go index ded4f4d118..569da21886 100644 --- a/components/cli/command/system/version.go +++ b/components/cli/command/system/version.go @@ -11,7 +11,7 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/dockerversion" - "github.com/docker/docker/utils/templates" + "github.com/docker/docker/pkg/templates" "github.com/spf13/cobra" ) From e51409f5124d1b101d195c2aa7d2d93199c7118e Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Fri, 9 Dec 2016 23:15:26 +0800 Subject: [PATCH 457/978] Update the option 'network' for docker build Signed-off-by: yuexiao-wang Upstream-commit: dd2b83e297e1a8b48668b968635d04bed7420597 Component: cli --- components/cli/command/image/build.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go index 1699b2c45c..e3e7ff2b02 100644 --- a/components/cli/command/image/build.go +++ b/components/cli/command/image/build.go @@ -107,7 +107,7 @@ func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command { flags.StringSliceVar(&options.cacheFrom, "cache-from", []string{}, "Images to consider as cache sources") flags.BoolVar(&options.compress, "compress", false, "Compress the build context using gzip") flags.StringSliceVar(&options.securityOpt, "security-opt", []string{}, "Security options") - flags.StringVar(&options.networkMode, "network", "default", "Connect a container to a network") + flags.StringVar(&options.networkMode, "network", "default", "Set the networking mode for the RUN instructions during build") command.AddTrustedFlags(flags, true) From 14f444d9328b2923bfd3d2d424b101148666a001 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 30 Nov 2016 13:23:18 -0800 Subject: [PATCH 458/978] Show usage when `docker swarm update` has no flags This fix tries to address the issue raised in 24352. Previously, when `docker swarm update` has no flags, the output is ``` Swarm updated. ``` even though nothing was updated. This could be misleading for users. This fix tries to address the issue by adding a `PreRunE` function in the command so that in case no flag is provided (`cmd.Flags().NFlag() == 0`), the usage will be outputed instead. An integration has been added to cover the changes. This fix fixes 24352. Signed-off-by: Yong Tang Upstream-commit: 249d4e5709da7e1438e9eeab1190fb9f097b4058 Component: cli --- components/cli/command/swarm/update.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/components/cli/command/swarm/update.go b/components/cli/command/swarm/update.go index cb0d83ef26..dbbd268725 100644 --- a/components/cli/command/swarm/update.go +++ b/components/cli/command/swarm/update.go @@ -23,6 +23,12 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { return runUpdate(dockerCli, cmd.Flags(), opts) }, + PreRunE: func(cmd *cobra.Command, args []string) error { + if cmd.Flags().NFlag() == 0 { + return pflag.ErrHelp + } + return nil + }, } cmd.Flags().BoolVar(&opts.autolock, flagAutolock, false, "Change manager autolocking setting (true|false)") From d0c6ebff61ee0a1658f36bed591e6bd746a6034c Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sat, 19 Nov 2016 17:41:11 -0800 Subject: [PATCH 459/978] Add `--file` flag for `docker secret create` command This fix tries to address the issue raised in 28581 and 28927 where it is not possible to create a secret from a file (only through STDIN). This fix add a flag `--file` to `docker secret create` so that it is possible to create a secret from a file with: ``` docker secret create --file secret.in secret.name ``` or ``` echo TEST | docker secret create --file - secret.name ``` Related docs has been updated. An integration test has been added to cover the changes. This fix fixes 28581. This fix is related to 28927. Signed-off-by: Yong Tang Upstream-commit: 8e680c48f1e112e2b477939bffb090c30b3b0f1b Component: cli --- components/cli/command/secret/create.go | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/components/cli/command/secret/create.go b/components/cli/command/secret/create.go index 381a931415..5d4dc34d12 100644 --- a/components/cli/command/secret/create.go +++ b/components/cli/command/secret/create.go @@ -2,13 +2,14 @@ package secret import ( "fmt" + "io" "io/ioutil" - "os" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/opts" + "github.com/docker/docker/pkg/system" runconfigopts "github.com/docker/docker/runconfig/opts" "github.com/spf13/cobra" "golang.org/x/net/context" @@ -16,6 +17,7 @@ import ( type createOptions struct { name string + file string labels opts.ListOpts } @@ -26,7 +28,7 @@ func newSecretCreateCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := &cobra.Command{ Use: "create [OPTIONS] SECRET", - Short: "Create a secret using stdin as content", + Short: "Create a secret from a file or STDIN as content", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { createOpts.name = args[0] @@ -35,6 +37,7 @@ func newSecretCreateCommand(dockerCli *command.DockerCli) *cobra.Command { } flags := cmd.Flags() flags.VarP(&createOpts.labels, "label", "l", "Secret labels") + flags.StringVarP(&createOpts.file, "file", "f", "", "Read from a file or STDIN ('-')") return cmd } @@ -43,9 +46,23 @@ func runSecretCreate(dockerCli *command.DockerCli, options createOptions) error client := dockerCli.Client() ctx := context.Background() - secretData, err := ioutil.ReadAll(os.Stdin) + if options.file == "" { + return fmt.Errorf("Please specify either a file name or STDIN ('-') with --file") + } + + var in io.Reader = dockerCli.In() + if options.file != "-" { + file, err := system.OpenSequential(options.file) + if err != nil { + return err + } + in = file + defer file.Close() + } + + secretData, err := ioutil.ReadAll(in) if err != nil { - return fmt.Errorf("Error reading content from STDIN: %v", err) + return fmt.Errorf("Error reading content from %q: %v", options.file, err) } spec := swarm.SecretSpec{ From db5fdebfa8c20cfedacf199693b5ccdc354bd579 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Thu, 8 Dec 2016 12:04:22 +0100 Subject: [PATCH 460/978] Ignore certificate expiry error for top-level inspect The top-level `docker inspect` command could return an error if the nodes Swarm certificates were expired. In situations where the user did not explicitly ask for an object-type (`--type=foo`), we should ignore these errors, and consider them equal to "node is not a swarm manager". This change makes `docker inspect` ignore these errors if no type was specified. As a further optimization, the "swarm status" result is now stored in a variable, so that other swarm-specific API calls can be skipped. Signed-off-by: Sebastiaan van Stijn Upstream-commit: 518b65c6f5f2de65932a715d161324305cbdc693 Component: cli --- components/cli/command/system/inspect.go | 93 +++++++++++++++++++----- 1 file changed, 74 insertions(+), 19 deletions(-) diff --git a/components/cli/command/system/inspect.go b/components/cli/command/system/inspect.go index dee4efcfec..cb5a1213af 100644 --- a/components/cli/command/system/inspect.go +++ b/components/cli/command/system/inspect.go @@ -2,7 +2,6 @@ package system import ( "fmt" - "strings" "golang.org/x/net/context" @@ -103,38 +102,94 @@ func inspectPlugin(ctx context.Context, dockerCli *command.DockerCli) inspect.Ge func inspectAll(ctx context.Context, dockerCli *command.DockerCli, getSize bool, typeConstraint string) inspect.GetRefFunc { var inspectAutodetect = []struct { - ObjectType string - IsSizeSupported bool - ObjectInspector func(string) (interface{}, []byte, error) + objectType string + isSizeSupported bool + isSwarmObject bool + objectInspector func(string) (interface{}, []byte, error) }{ - {"container", true, inspectContainers(ctx, dockerCli, getSize)}, - {"image", false, inspectImages(ctx, dockerCli)}, - {"network", false, inspectNetwork(ctx, dockerCli)}, - {"volume", false, inspectVolume(ctx, dockerCli)}, - {"service", false, inspectService(ctx, dockerCli)}, - {"task", false, inspectTasks(ctx, dockerCli)}, - {"node", false, inspectNode(ctx, dockerCli)}, - {"plugin", false, inspectPlugin(ctx, dockerCli)}, + { + objectType: "container", + isSizeSupported: true, + objectInspector: inspectContainers(ctx, dockerCli, getSize), + }, + { + objectType: "image", + objectInspector: inspectImages(ctx, dockerCli), + }, + { + objectType: "network", + objectInspector: inspectNetwork(ctx, dockerCli), + }, + { + objectType: "volume", + objectInspector: inspectVolume(ctx, dockerCli), + }, + { + objectType: "service", + isSwarmObject: true, + objectInspector: inspectService(ctx, dockerCli), + }, + { + objectType: "task", + isSwarmObject: true, + objectInspector: inspectTasks(ctx, dockerCli), + }, + { + objectType: "node", + isSwarmObject: true, + objectInspector: inspectNode(ctx, dockerCli), + }, + { + objectType: "plugin", + objectInspector: inspectPlugin(ctx, dockerCli), + }, } - isErrNotSwarmManager := func(err error) bool { - return strings.Contains(err.Error(), "This node is not a swarm manager") + // isSwarmManager does an Info API call to verify that the daemon is + // a swarm manager. + isSwarmManager := func() bool { + info, err := dockerCli.Client().Info(ctx) + if err != nil { + fmt.Fprintln(dockerCli.Err(), err) + return false + } + return info.Swarm.ControlAvailable } return func(ref string) (interface{}, []byte, error) { + const ( + swarmSupportUnknown = iota + swarmSupported + swarmUnsupported + ) + + isSwarmSupported := swarmSupportUnknown + for _, inspectData := range inspectAutodetect { - if typeConstraint != "" && inspectData.ObjectType != typeConstraint { + if typeConstraint != "" && inspectData.objectType != typeConstraint { continue } - v, raw, err := inspectData.ObjectInspector(ref) + if typeConstraint == "" && inspectData.isSwarmObject { + if isSwarmSupported == swarmSupportUnknown { + if isSwarmManager() { + isSwarmSupported = swarmSupported + } else { + isSwarmSupported = swarmUnsupported + } + } + if isSwarmSupported == swarmUnsupported { + continue + } + } + v, raw, err := inspectData.objectInspector(ref) if err != nil { - if typeConstraint == "" && (apiclient.IsErrNotFound(err) || isErrNotSwarmManager(err)) { + if typeConstraint == "" && apiclient.IsErrNotFound(err) { continue } return v, raw, err } - if getSize && !inspectData.IsSizeSupported { - fmt.Fprintf(dockerCli.Err(), "WARNING: --size ignored for %s\n", inspectData.ObjectType) + if getSize && !inspectData.isSizeSupported { + fmt.Fprintf(dockerCli.Err(), "WARNING: --size ignored for %s\n", inspectData.objectType) } return v, raw, err } From fe8fcf4846147f35d06fd9781a9e406b253669c7 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Mon, 5 Dec 2016 16:06:29 -0800 Subject: [PATCH 461/978] cli: Split out GetNotaryRepository and associated functions Split these into cli/trust so that other commands can make use of them. Signed-off-by: Aaron Lehmann Upstream-commit: 639f97daea2e286e7dbb811591b0b5d3909b6677 Component: cli --- components/cli/command/image/trust.go | 241 ++------------------- components/cli/command/image/trust_test.go | 9 +- components/cli/trust/trust.go | 221 +++++++++++++++++++ 3 files changed, 244 insertions(+), 227 deletions(-) create mode 100644 components/cli/trust/trust.go diff --git a/components/cli/command/image/trust.go b/components/cli/command/image/trust.go index 8f5c76d8ca..f32c301959 100644 --- a/components/cli/command/image/trust.go +++ b/components/cli/command/image/trust.go @@ -6,43 +6,22 @@ import ( "errors" "fmt" "io" - "net" - "net/http" - "net/url" - "os" "path" - "path/filepath" "sort" - "time" "golang.org/x/net/context" "github.com/Sirupsen/logrus" "github.com/docker/distribution/digest" - "github.com/docker/distribution/registry/client/auth" - "github.com/docker/distribution/registry/client/auth/challenge" - "github.com/docker/distribution/registry/client/transport" "github.com/docker/docker/api/types" - registrytypes "github.com/docker/docker/api/types/registry" "github.com/docker/docker/cli/command" - "github.com/docker/docker/cliconfig" + "github.com/docker/docker/cli/trust" "github.com/docker/docker/distribution" "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/reference" "github.com/docker/docker/registry" - "github.com/docker/go-connections/tlsconfig" - "github.com/docker/notary" "github.com/docker/notary/client" - "github.com/docker/notary/passphrase" - "github.com/docker/notary/storage" - "github.com/docker/notary/trustmanager" - "github.com/docker/notary/trustpinning" "github.com/docker/notary/tuf/data" - "github.com/docker/notary/tuf/signed" -) - -var ( - releasesRole = path.Join(data.CanonicalTargetsRole, "releases") ) type target struct { @@ -118,7 +97,7 @@ func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry fmt.Fprintln(cli.Out(), "Signing and pushing trust metadata") - repo, err := GetNotaryRepository(cli, repoInfo, authConfig, "push", "pull") + repo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "push", "pull") if err != nil { fmt.Fprintf(cli.Out(), "Error establishing connection to notary repository: %s\n", err) return err @@ -145,7 +124,7 @@ func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry // Initialize the notary repository with a remotely managed snapshot key if err := repo.Initialize([]string{rootKeyID}, data.CanonicalSnapshotRole); err != nil { - return notaryError(repoInfo.FullName(), err) + return trust.NotaryError(repoInfo.FullName(), err) } fmt.Fprintf(cli.Out(), "Finished initializing %q\n", repoInfo.FullName()) err = repo.AddTarget(target, data.CanonicalTargetsRole) @@ -153,7 +132,7 @@ func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry // already initialized and we have successfully downloaded the latest metadata err = addTargetToAllSignableRoles(repo, target) default: - return notaryError(repoInfo.FullName(), err) + return trust.NotaryError(repoInfo.FullName(), err) } if err == nil { @@ -162,7 +141,7 @@ func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry if err != nil { fmt.Fprintf(cli.Out(), "Failed to sign %q:%s - %s\n", repoInfo.FullName(), tag, err.Error()) - return notaryError(repoInfo.FullName(), err) + return trust.NotaryError(repoInfo.FullName(), err) } fmt.Fprintf(cli.Out(), "Successfully signed %q:%s\n", repoInfo.FullName(), tag) @@ -235,7 +214,7 @@ func imagePushPrivileged(ctx context.Context, cli *command.DockerCli, authConfig func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error { var refs []target - notaryRepo, err := GetNotaryRepository(cli, repoInfo, authConfig, "pull") + notaryRepo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "pull") if err != nil { fmt.Fprintf(cli.Out(), "Error establishing connection to trust repository: %s\n", err) return err @@ -243,9 +222,9 @@ func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry if tagged, isTagged := ref.(reference.NamedTagged); !isTagged { // List all targets - targets, err := notaryRepo.ListTargets(releasesRole, data.CanonicalTargetsRole) + targets, err := notaryRepo.ListTargets(trust.ReleasesRole, data.CanonicalTargetsRole) if err != nil { - return notaryError(repoInfo.FullName(), err) + return trust.NotaryError(repoInfo.FullName(), err) } for _, tgt := range targets { t, err := convertTarget(tgt.Target) @@ -255,23 +234,23 @@ func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry } // Only list tags in the top level targets role or the releases delegation role - ignore // all other delegation roles - if tgt.Role != releasesRole && tgt.Role != data.CanonicalTargetsRole { + if tgt.Role != trust.ReleasesRole && tgt.Role != data.CanonicalTargetsRole { continue } refs = append(refs, t) } if len(refs) == 0 { - return notaryError(repoInfo.FullName(), fmt.Errorf("No trusted tags for %s", repoInfo.FullName())) + return trust.NotaryError(repoInfo.FullName(), fmt.Errorf("No trusted tags for %s", repoInfo.FullName())) } } else { - t, err := notaryRepo.GetTargetByName(tagged.Tag(), releasesRole, data.CanonicalTargetsRole) + t, err := notaryRepo.GetTargetByName(tagged.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole) if err != nil { - return notaryError(repoInfo.FullName(), err) + return trust.NotaryError(repoInfo.FullName(), err) } // Only get the tag if it's in the top level targets role or the releases delegation role // ignore it if it's in any other delegation roles - if t.Role != releasesRole && t.Role != data.CanonicalTargetsRole { - return notaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", tagged.Tag())) + if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole { + return trust.NotaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", tagged.Tag())) } logrus.Debugf("retrieving target for %s role\n", t.Role) @@ -335,159 +314,6 @@ func imagePullPrivileged(ctx context.Context, cli *command.DockerCli, authConfig return jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), nil) } -func trustDirectory() string { - return filepath.Join(cliconfig.ConfigDir(), "trust") -} - -// certificateDirectory returns the directory containing -// TLS certificates for the given server. An error is -// returned if there was an error parsing the server string. -func certificateDirectory(server string) (string, error) { - u, err := url.Parse(server) - if err != nil { - return "", err - } - - return filepath.Join(cliconfig.ConfigDir(), "tls", u.Host), nil -} - -func trustServer(index *registrytypes.IndexInfo) (string, error) { - if s := os.Getenv("DOCKER_CONTENT_TRUST_SERVER"); s != "" { - urlObj, err := url.Parse(s) - if err != nil || urlObj.Scheme != "https" { - return "", fmt.Errorf("valid https URL required for trust server, got %s", s) - } - - return s, nil - } - if index.Official { - return registry.NotaryServer, nil - } - return "https://" + index.Name, nil -} - -type simpleCredentialStore struct { - auth types.AuthConfig -} - -func (scs simpleCredentialStore) Basic(u *url.URL) (string, string) { - return scs.auth.Username, scs.auth.Password -} - -func (scs simpleCredentialStore) RefreshToken(u *url.URL, service string) string { - return scs.auth.IdentityToken -} - -func (scs simpleCredentialStore) SetRefreshToken(*url.URL, string, string) { -} - -// GetNotaryRepository returns a NotaryRepository which stores all the -// information needed to operate on a notary repository. -// It creates an HTTP transport providing authentication support. -// TODO: move this too -func GetNotaryRepository(streams command.Streams, repoInfo *registry.RepositoryInfo, authConfig types.AuthConfig, actions ...string) (*client.NotaryRepository, error) { - server, err := trustServer(repoInfo.Index) - if err != nil { - return nil, err - } - - var cfg = tlsconfig.ClientDefault() - cfg.InsecureSkipVerify = !repoInfo.Index.Secure - - // Get certificate base directory - certDir, err := certificateDirectory(server) - if err != nil { - return nil, err - } - logrus.Debugf("reading certificate directory: %s", certDir) - - if err := registry.ReadCertsDirectory(cfg, certDir); err != nil { - return nil, err - } - - base := &http.Transport{ - Proxy: http.ProxyFromEnvironment, - Dial: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - DualStack: true, - }).Dial, - TLSHandshakeTimeout: 10 * time.Second, - TLSClientConfig: cfg, - DisableKeepAlives: true, - } - - // Skip configuration headers since request is not going to Docker daemon - modifiers := registry.DockerHeaders(command.UserAgent(), http.Header{}) - authTransport := transport.NewTransport(base, modifiers...) - pingClient := &http.Client{ - Transport: authTransport, - Timeout: 5 * time.Second, - } - endpointStr := server + "/v2/" - req, err := http.NewRequest("GET", endpointStr, nil) - if err != nil { - return nil, err - } - - challengeManager := challenge.NewSimpleManager() - - resp, err := pingClient.Do(req) - if err != nil { - // Ignore error on ping to operate in offline mode - logrus.Debugf("Error pinging notary server %q: %s", endpointStr, err) - } else { - defer resp.Body.Close() - - // Add response to the challenge manager to parse out - // authentication header and register authentication method - if err := challengeManager.AddResponse(resp); err != nil { - return nil, err - } - } - - creds := simpleCredentialStore{auth: authConfig} - tokenHandler := auth.NewTokenHandler(authTransport, creds, repoInfo.FullName(), actions...) - basicHandler := auth.NewBasicHandler(creds) - modifiers = append(modifiers, transport.RequestModifier(auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))) - tr := transport.NewTransport(base, modifiers...) - - return client.NewNotaryRepository( - trustDirectory(), - repoInfo.FullName(), - server, - tr, - getPassphraseRetriever(streams), - trustpinning.TrustPinConfig{}) -} - -func getPassphraseRetriever(streams command.Streams) notary.PassRetriever { - aliasMap := map[string]string{ - "root": "root", - "snapshot": "repository", - "targets": "repository", - "default": "repository", - } - baseRetriever := passphrase.PromptRetrieverWithInOut(streams.In(), streams.Out(), aliasMap) - env := map[string]string{ - "root": os.Getenv("DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE"), - "snapshot": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), - "targets": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), - "default": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), - } - - return func(keyName string, alias string, createNew bool, numAttempts int) (string, bool, error) { - if v := env[alias]; v != "" { - return v, numAttempts > 1, nil - } - // For non-root roles, we can also try the "default" alias if it is specified - if v := env["default"]; v != "" && alias != data.CanonicalRootRole { - return v, numAttempts > 1, nil - } - return baseRetriever(keyName, alias, createNew, numAttempts) - } -} - // TrustedReference returns the canonical trusted reference for an image reference func TrustedReference(ctx context.Context, cli *command.DockerCli, ref reference.NamedTagged) (reference.Canonical, error) { repoInfo, err := registry.ParseRepositoryInfo(ref) @@ -498,20 +324,20 @@ func TrustedReference(ctx context.Context, cli *command.DockerCli, ref reference // Resolve the Auth config relevant for this server authConfig := command.ResolveAuthConfig(ctx, cli, repoInfo.Index) - notaryRepo, err := GetNotaryRepository(cli, repoInfo, authConfig, "pull") + notaryRepo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "pull") if err != nil { fmt.Fprintf(cli.Out(), "Error establishing connection to trust repository: %s\n", err) return nil, err } - t, err := notaryRepo.GetTargetByName(ref.Tag(), releasesRole, data.CanonicalTargetsRole) + t, err := notaryRepo.GetTargetByName(ref.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole) if err != nil { return nil, err } // Only list tags in the top level targets role or the releases delegation role - ignore // all other delegation roles - if t.Role != releasesRole && t.Role != data.CanonicalTargetsRole { - return nil, notaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", ref.Tag())) + if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole { + return nil, trust.NotaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", ref.Tag())) } r, err := convertTarget(t.Target) if err != nil { @@ -540,34 +366,3 @@ func TagTrusted(ctx context.Context, cli *command.DockerCli, trustedRef referenc return cli.Client().ImageTag(ctx, trustedRef.String(), ref.String()) } - -// notaryError formats an error message received from the notary service -func notaryError(repoName string, err error) error { - switch err.(type) { - case *json.SyntaxError: - logrus.Debugf("Notary syntax error: %s", err) - return fmt.Errorf("Error: no trust data available for remote repository %s. Try running notary server and setting DOCKER_CONTENT_TRUST_SERVER to its HTTPS address?", repoName) - case signed.ErrExpired: - return fmt.Errorf("Error: remote repository %s out-of-date: %v", repoName, err) - case trustmanager.ErrKeyNotFound: - return fmt.Errorf("Error: signing keys for remote repository %s not found: %v", repoName, err) - case storage.NetworkError: - return fmt.Errorf("Error: error contacting notary server: %v", err) - case storage.ErrMetaNotFound: - return fmt.Errorf("Error: trust data missing for remote repository %s or remote repository not found: %v", repoName, err) - case trustpinning.ErrRootRotationFail, trustpinning.ErrValidationFail, signed.ErrInvalidKeyType: - return fmt.Errorf("Warning: potential malicious behavior - trust data mismatch for remote repository %s: %v", repoName, err) - case signed.ErrNoKeys: - return fmt.Errorf("Error: could not find signing keys for remote repository %s, or could not decrypt signing key: %v", repoName, err) - case signed.ErrLowVersion: - return fmt.Errorf("Warning: potential malicious behavior - trust data version is lower than expected for remote repository %s: %v", repoName, err) - case signed.ErrRoleThreshold: - return fmt.Errorf("Warning: potential malicious behavior - trust data has insufficient signatures for remote repository %s: %v", repoName, err) - case client.ErrRepositoryNotExist: - return fmt.Errorf("Error: remote trust data does not exist for %s: %v", repoName, err) - case signed.ErrInsufficientSignatures: - return fmt.Errorf("Error: could not produce valid signature for %s. If Yubikey was used, was touch input provided?: %v", repoName, err) - } - - return err -} diff --git a/components/cli/command/image/trust_test.go b/components/cli/command/image/trust_test.go index ba6373f2da..78146465e6 100644 --- a/components/cli/command/image/trust_test.go +++ b/components/cli/command/image/trust_test.go @@ -5,6 +5,7 @@ import ( "testing" registrytypes "github.com/docker/docker/api/types/registry" + "github.com/docker/docker/cli/trust" "github.com/docker/docker/registry" ) @@ -19,7 +20,7 @@ func TestENVTrustServer(t *testing.T) { if err := os.Setenv("DOCKER_CONTENT_TRUST_SERVER", "https://notary-test.com:5000"); err != nil { t.Fatal("Failed to set ENV variable") } - output, err := trustServer(indexInfo) + output, err := trust.Server(indexInfo) expectedStr := "https://notary-test.com:5000" if err != nil || output != expectedStr { t.Fatalf("Expected server to be %s, got %s", expectedStr, output) @@ -32,7 +33,7 @@ func TestHTTPENVTrustServer(t *testing.T) { if err := os.Setenv("DOCKER_CONTENT_TRUST_SERVER", "http://notary-test.com:5000"); err != nil { t.Fatal("Failed to set ENV variable") } - _, err := trustServer(indexInfo) + _, err := trust.Server(indexInfo) if err == nil { t.Fatal("Expected error with invalid scheme") } @@ -40,7 +41,7 @@ func TestHTTPENVTrustServer(t *testing.T) { func TestOfficialTrustServer(t *testing.T) { indexInfo := ®istrytypes.IndexInfo{Name: "testserver", Official: true} - output, err := trustServer(indexInfo) + output, err := trust.Server(indexInfo) if err != nil || output != registry.NotaryServer { t.Fatalf("Expected server to be %s, got %s", registry.NotaryServer, output) } @@ -48,7 +49,7 @@ func TestOfficialTrustServer(t *testing.T) { func TestNonOfficialTrustServer(t *testing.T) { indexInfo := ®istrytypes.IndexInfo{Name: "testserver", Official: false} - output, err := trustServer(indexInfo) + output, err := trust.Server(indexInfo) expectedStr := "https://" + indexInfo.Name if err != nil || output != expectedStr { t.Fatalf("Expected server to be %s, got %s", expectedStr, output) diff --git a/components/cli/trust/trust.go b/components/cli/trust/trust.go new file mode 100644 index 0000000000..0f3482f2d7 --- /dev/null +++ b/components/cli/trust/trust.go @@ -0,0 +1,221 @@ +package trust + +import ( + "encoding/json" + "fmt" + "net" + "net/http" + "net/url" + "os" + "path" + "path/filepath" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/distribution/registry/client/auth" + "github.com/docker/distribution/registry/client/auth/challenge" + "github.com/docker/distribution/registry/client/transport" + "github.com/docker/docker/api/types" + registrytypes "github.com/docker/docker/api/types/registry" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cliconfig" + "github.com/docker/docker/registry" + "github.com/docker/go-connections/tlsconfig" + "github.com/docker/notary" + "github.com/docker/notary/client" + "github.com/docker/notary/passphrase" + "github.com/docker/notary/storage" + "github.com/docker/notary/trustmanager" + "github.com/docker/notary/trustpinning" + "github.com/docker/notary/tuf/data" + "github.com/docker/notary/tuf/signed" +) + +var ( + // ReleasesRole is the role named "releases" + ReleasesRole = path.Join(data.CanonicalTargetsRole, "releases") +) + +func trustDirectory() string { + return filepath.Join(cliconfig.ConfigDir(), "trust") +} + +// certificateDirectory returns the directory containing +// TLS certificates for the given server. An error is +// returned if there was an error parsing the server string. +func certificateDirectory(server string) (string, error) { + u, err := url.Parse(server) + if err != nil { + return "", err + } + + return filepath.Join(cliconfig.ConfigDir(), "tls", u.Host), nil +} + +// Server returns the base URL for the trust server. +func Server(index *registrytypes.IndexInfo) (string, error) { + if s := os.Getenv("DOCKER_CONTENT_TRUST_SERVER"); s != "" { + urlObj, err := url.Parse(s) + if err != nil || urlObj.Scheme != "https" { + return "", fmt.Errorf("valid https URL required for trust server, got %s", s) + } + + return s, nil + } + if index.Official { + return registry.NotaryServer, nil + } + return "https://" + index.Name, nil +} + +type simpleCredentialStore struct { + auth types.AuthConfig +} + +func (scs simpleCredentialStore) Basic(u *url.URL) (string, string) { + return scs.auth.Username, scs.auth.Password +} + +func (scs simpleCredentialStore) RefreshToken(u *url.URL, service string) string { + return scs.auth.IdentityToken +} + +func (scs simpleCredentialStore) SetRefreshToken(*url.URL, string, string) { +} + +// GetNotaryRepository returns a NotaryRepository which stores all the +// information needed to operate on a notary repository. +// It creates an HTTP transport providing authentication support. +func GetNotaryRepository(streams command.Streams, repoInfo *registry.RepositoryInfo, authConfig types.AuthConfig, actions ...string) (*client.NotaryRepository, error) { + server, err := Server(repoInfo.Index) + if err != nil { + return nil, err + } + + var cfg = tlsconfig.ClientDefault() + cfg.InsecureSkipVerify = !repoInfo.Index.Secure + + // Get certificate base directory + certDir, err := certificateDirectory(server) + if err != nil { + return nil, err + } + logrus.Debugf("reading certificate directory: %s", certDir) + + if err := registry.ReadCertsDirectory(cfg, certDir); err != nil { + return nil, err + } + + base := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + Dial: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + }).Dial, + TLSHandshakeTimeout: 10 * time.Second, + TLSClientConfig: cfg, + DisableKeepAlives: true, + } + + // Skip configuration headers since request is not going to Docker daemon + modifiers := registry.DockerHeaders(command.UserAgent(), http.Header{}) + authTransport := transport.NewTransport(base, modifiers...) + pingClient := &http.Client{ + Transport: authTransport, + Timeout: 5 * time.Second, + } + endpointStr := server + "/v2/" + req, err := http.NewRequest("GET", endpointStr, nil) + if err != nil { + return nil, err + } + + challengeManager := challenge.NewSimpleManager() + + resp, err := pingClient.Do(req) + if err != nil { + // Ignore error on ping to operate in offline mode + logrus.Debugf("Error pinging notary server %q: %s", endpointStr, err) + } else { + defer resp.Body.Close() + + // Add response to the challenge manager to parse out + // authentication header and register authentication method + if err := challengeManager.AddResponse(resp); err != nil { + return nil, err + } + } + + creds := simpleCredentialStore{auth: authConfig} + tokenHandler := auth.NewTokenHandler(authTransport, creds, repoInfo.FullName(), actions...) + basicHandler := auth.NewBasicHandler(creds) + modifiers = append(modifiers, transport.RequestModifier(auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))) + tr := transport.NewTransport(base, modifiers...) + + return client.NewNotaryRepository( + trustDirectory(), + repoInfo.FullName(), + server, + tr, + getPassphraseRetriever(streams), + trustpinning.TrustPinConfig{}) +} + +func getPassphraseRetriever(streams command.Streams) notary.PassRetriever { + aliasMap := map[string]string{ + "root": "root", + "snapshot": "repository", + "targets": "repository", + "default": "repository", + } + baseRetriever := passphrase.PromptRetrieverWithInOut(streams.In(), streams.Out(), aliasMap) + env := map[string]string{ + "root": os.Getenv("DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE"), + "snapshot": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), + "targets": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), + "default": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), + } + + return func(keyName string, alias string, createNew bool, numAttempts int) (string, bool, error) { + if v := env[alias]; v != "" { + return v, numAttempts > 1, nil + } + // For non-root roles, we can also try the "default" alias if it is specified + if v := env["default"]; v != "" && alias != data.CanonicalRootRole { + return v, numAttempts > 1, nil + } + return baseRetriever(keyName, alias, createNew, numAttempts) + } +} + +// NotaryError formats an error message received from the notary service +func NotaryError(repoName string, err error) error { + switch err.(type) { + case *json.SyntaxError: + logrus.Debugf("Notary syntax error: %s", err) + return fmt.Errorf("Error: no trust data available for remote repository %s. Try running notary server and setting DOCKER_CONTENT_TRUST_SERVER to its HTTPS address?", repoName) + case signed.ErrExpired: + return fmt.Errorf("Error: remote repository %s out-of-date: %v", repoName, err) + case trustmanager.ErrKeyNotFound: + return fmt.Errorf("Error: signing keys for remote repository %s not found: %v", repoName, err) + case storage.NetworkError: + return fmt.Errorf("Error: error contacting notary server: %v", err) + case storage.ErrMetaNotFound: + return fmt.Errorf("Error: trust data missing for remote repository %s or remote repository not found: %v", repoName, err) + case trustpinning.ErrRootRotationFail, trustpinning.ErrValidationFail, signed.ErrInvalidKeyType: + return fmt.Errorf("Warning: potential malicious behavior - trust data mismatch for remote repository %s: %v", repoName, err) + case signed.ErrNoKeys: + return fmt.Errorf("Error: could not find signing keys for remote repository %s, or could not decrypt signing key: %v", repoName, err) + case signed.ErrLowVersion: + return fmt.Errorf("Warning: potential malicious behavior - trust data version is lower than expected for remote repository %s: %v", repoName, err) + case signed.ErrRoleThreshold: + return fmt.Errorf("Warning: potential malicious behavior - trust data has insufficient signatures for remote repository %s: %v", repoName, err) + case client.ErrRepositoryNotExist: + return fmt.Errorf("Error: remote trust data does not exist for %s: %v", repoName, err) + case signed.ErrInsufficientSignatures: + return fmt.Errorf("Error: could not produce valid signature for %s. If Yubikey was used, was touch input provided?: %v", repoName, err) + } + + return err +} From c953080e22b40633482cf00f2573d5ab04b41dbf Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Mon, 5 Dec 2016 17:02:26 -0800 Subject: [PATCH 462/978] cli: Pin image to digest using content trust Implement notary-based digest lookup in the client when DOCKER_CONTENT_TRUST=1. Signed-off-by: Aaron Lehmann Upstream-commit: fdb6a6ee1cf6f69a2dee00bce2d2d062d4cfa15e Component: cli --- components/cli/command/service/create.go | 4 + components/cli/command/service/trust.go | 96 ++++++++++++++++++++++++ components/cli/command/service/update.go | 6 ++ 3 files changed, 106 insertions(+) create mode 100644 components/cli/command/service/trust.go diff --git a/components/cli/command/service/create.go b/components/cli/command/service/create.go index a8382835a0..ca2bb089fd 100644 --- a/components/cli/command/service/create.go +++ b/components/cli/command/service/create.go @@ -72,6 +72,10 @@ func runCreate(dockerCli *command.DockerCli, opts *serviceOptions) error { ctx := context.Background() + if err := resolveServiceImageDigest(dockerCli, &service); err != nil { + return err + } + // only send auth if flag was set if opts.registryAuth { // Retrieve encoded auth token from the image reference diff --git a/components/cli/command/service/trust.go b/components/cli/command/service/trust.go new file mode 100644 index 0000000000..052d49c32a --- /dev/null +++ b/components/cli/command/service/trust.go @@ -0,0 +1,96 @@ +package service + +import ( + "encoding/hex" + "fmt" + + "github.com/Sirupsen/logrus" + "github.com/docker/distribution/digest" + distreference "github.com/docker/distribution/reference" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/trust" + "github.com/docker/docker/reference" + "github.com/docker/docker/registry" + "github.com/docker/notary/tuf/data" + "github.com/pkg/errors" + "golang.org/x/net/context" +) + +func resolveServiceImageDigest(dockerCli *command.DockerCli, service *swarm.ServiceSpec) error { + if !command.IsTrusted() { + // Digests are resolved by the daemon when not using content + // trust. + return nil + } + + image := service.TaskTemplate.ContainerSpec.Image + + // We only attempt to resolve the digest if the reference + // could be parsed as a digest reference. Specifying an image ID + // is valid but not resolvable. There is no warning message for + // an image ID because it's valid to use one. + if _, err := digest.ParseDigest(image); err == nil { + return nil + } + + ref, err := reference.ParseNamed(image) + if err != nil { + return fmt.Errorf("Could not parse image reference %s", service.TaskTemplate.ContainerSpec.Image) + } + if _, ok := ref.(reference.Canonical); !ok { + ref = reference.WithDefaultTag(ref) + + taggedRef, ok := ref.(reference.NamedTagged) + if !ok { + // This should never happen because a reference either + // has a digest, or WithDefaultTag would give it a tag. + return errors.New("Failed to resolve image digest using content trust: reference is missing a tag") + } + + resolvedImage, err := trustedResolveDigest(context.Background(), dockerCli, taggedRef) + if err != nil { + return fmt.Errorf("Failed to resolve image digest using content trust: %v", err) + } + logrus.Debugf("resolved image tag to %s using content trust", resolvedImage.String()) + service.TaskTemplate.ContainerSpec.Image = resolvedImage.String() + } + return nil +} + +func trustedResolveDigest(ctx context.Context, cli *command.DockerCli, ref reference.NamedTagged) (distreference.Canonical, error) { + repoInfo, err := registry.ParseRepositoryInfo(ref) + if err != nil { + return nil, err + } + + authConfig := command.ResolveAuthConfig(ctx, cli, repoInfo.Index) + + notaryRepo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "pull") + if err != nil { + return nil, errors.Wrap(err, "error establishing connection to trust repository") + } + + t, err := notaryRepo.GetTargetByName(ref.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole) + if err != nil { + return nil, trust.NotaryError(repoInfo.FullName(), err) + } + // Only get the tag if it's in the top level targets role or the releases delegation role + // ignore it if it's in any other delegation roles + if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole { + return nil, trust.NotaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", ref.String())) + } + + logrus.Debugf("retrieving target for %s role\n", t.Role) + h, ok := t.Hashes["sha256"] + if !ok { + return nil, errors.New("no valid hash, expecting sha256") + } + + dgst := digest.NewDigestFromHex("sha256", hex.EncodeToString(h)) + + // Using distribution reference package to make sure that adding a + // digest does not erase the tag. When the two reference packages + // are unified, this will no longer be an issue. + return distreference.WithDigest(ref, dgst) +} diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index 4bbcf35a8d..514b1bd510 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -103,6 +103,12 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID str return err } + if flags.Changed("image") { + if err := resolveServiceImageDigest(dockerCli, spec); err != nil { + return err + } + } + updatedSecrets, err := getUpdatedSecrets(apiClient, flags, spec.TaskTemplate.ContainerSpec.Secrets) if err != nil { return err From a92decfa5fe8c0ae4035cde5860e7538bbec1dce Mon Sep 17 00:00:00 2001 From: unclejack Date: Wed, 14 Dec 2016 23:16:12 +0200 Subject: [PATCH 463/978] return directly without ifs in remaining packages Signed-off-by: Cristian Staretu Upstream-commit: 9197940e4e21061febce4115cb79a9594160f0c8 Component: cli --- components/cli/command/task/print.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/components/cli/command/task/print.go b/components/cli/command/task/print.go index 0f1c2cf724..57c4e0c8c8 100644 --- a/components/cli/command/task/print.go +++ b/components/cli/command/task/print.go @@ -70,11 +70,7 @@ func Print(dockerCli *command.DockerCli, ctx context.Context, tasks []swarm.Task defer writer.Flush() fmt.Fprintln(writer, strings.Join([]string{"ID", "NAME", "IMAGE", "NODE", "DESIRED STATE", "CURRENT STATE", "ERROR", "PORTS"}, "\t")) - if err := print(writer, ctx, tasks, resolver, noTrunc); err != nil { - return err - } - - return nil + return print(writer, ctx, tasks, resolver, noTrunc) } // PrintQuiet shows task list in a quiet way. From 0f4c3f59f3f61051688efd60260aedbb27196e89 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Fri, 16 Dec 2016 15:10:20 +0100 Subject: [PATCH 464/978] swarm leave is not only for workers the "docker swarm leave" command description mentioned that the command can only be used for workers, however, the command can also be used for managers (using the `-f` / `--force` option). this patch removes the "(workers only)" part of the command description. Signed-off-by: Sebastiaan van Stijn Upstream-commit: 4cf95aeaa2e13e3d8e302eeffbdee3345aaae331 Component: cli --- components/cli/command/swarm/leave.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/swarm/leave.go b/components/cli/command/swarm/leave.go index 1ffaa3fcc9..e2cfa0a045 100644 --- a/components/cli/command/swarm/leave.go +++ b/components/cli/command/swarm/leave.go @@ -19,7 +19,7 @@ func newLeaveCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := &cobra.Command{ Use: "leave [OPTIONS]", - Short: "Leave the swarm (workers only)", + Short: "Leave the swarm", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { return runLeave(dockerCli, opts) From b2aa751ffb44378d0510626f8af6dfc73255b415 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Thu, 15 Dec 2016 13:07:27 -0500 Subject: [PATCH 465/978] 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 Upstream-commit: bcb7147ae5cff5f3a8d8186a46a6cec33dd49cd3 Component: cli --- components/cli/events.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/components/cli/events.go b/components/cli/events.go index c154f7dcf9..af47aefa74 100644 --- a/components/cli/events.go +++ b/components/cli/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 b77718801b640fd8d26d367fa77dae6e0e607dce Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 30 Nov 2016 15:33:54 -0500 Subject: [PATCH 466/978] Move ConvertNetworks to composetransform package. Signed-off-by: Daniel Nephin Upstream-commit: a28db56b0f02effe389287cc6a3e3d90bce82b3f Component: cli --- components/cli/command/stack/common.go | 20 ------------ components/cli/command/stack/deploy.go | 45 -------------------------- 2 files changed, 65 deletions(-) diff --git a/components/cli/command/stack/common.go b/components/cli/command/stack/common.go index 920a1af0cc..4ae8184933 100644 --- a/components/cli/command/stack/common.go +++ b/components/cli/command/stack/common.go @@ -9,18 +9,6 @@ import ( "github.com/docker/docker/client" ) -const ( - labelNamespace = "com.docker.stack.namespace" -) - -func getStackLabels(namespace string, labels map[string]string) map[string]string { - if labels == nil { - labels = make(map[string]string) - } - labels[labelNamespace] = namespace - return labels -} - func getStackFilter(namespace string) filters.Args { filter := filters.NewArgs() filter.Add("label", labelNamespace+"="+namespace) @@ -46,11 +34,3 @@ func getStackNetworks( ctx, types.NetworkListOptions{Filters: getStackFilter(namespace)}) } - -type namespace struct { - name string -} - -func (n namespace) scope(name string) string { - return n.name + "_" + name -} diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index 00a7634a0a..f1ab65ce95 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -179,51 +179,6 @@ func getConfigFile(filename string) (*composetypes.ConfigFile, error) { }, nil } -func convertNetworks( - namespace namespace, - networks map[string]composetypes.NetworkConfig, -) (map[string]types.NetworkCreate, []string) { - if networks == nil { - networks = make(map[string]composetypes.NetworkConfig) - } - - // TODO: only add default network if it's used - networks["default"] = composetypes.NetworkConfig{} - - externalNetworks := []string{} - result := make(map[string]types.NetworkCreate) - - for internalName, network := range networks { - if network.External.External { - externalNetworks = append(externalNetworks, network.External.Name) - continue - } - - createOpts := types.NetworkCreate{ - Labels: getStackLabels(namespace.name, network.Labels), - Driver: network.Driver, - Options: network.DriverOpts, - } - - if network.Ipam.Driver != "" || len(network.Ipam.Config) > 0 { - createOpts.IPAM = &networktypes.IPAM{} - } - - if network.Ipam.Driver != "" { - createOpts.IPAM.Driver = network.Ipam.Driver - } - for _, ipamConfig := range network.Ipam.Config { - config := networktypes.IPAMConfig{ - Subnet: ipamConfig.Subnet, - } - createOpts.IPAM.Config = append(createOpts.IPAM.Config, config) - } - result[internalName] = createOpts - } - - return result, externalNetworks -} - func validateExternalNetworks( ctx context.Context, dockerCli *command.DockerCli, From 4c4bfbf1f3e0a6fc020526f647a5dc4ae00d6180 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 30 Nov 2016 16:34:29 -0500 Subject: [PATCH 467/978] Move ConvertVolumes to composetransform package. Signed-off-by: Daniel Nephin Upstream-commit: af6a4113583ce2fb1c4e31aec5204dc11b454e54 Component: cli --- components/cli/command/stack/common.go | 3 +- components/cli/command/stack/deploy.go | 127 ++----------------------- 2 files changed, 10 insertions(+), 120 deletions(-) diff --git a/components/cli/command/stack/common.go b/components/cli/command/stack/common.go index 4ae8184933..050528de4e 100644 --- a/components/cli/command/stack/common.go +++ b/components/cli/command/stack/common.go @@ -7,11 +7,12 @@ import ( "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/client" + "github.com/docker/docker/pkg/composetransform" ) func getStackFilter(namespace string) filters.Args { filter := filters.NewArgs() - filter.Add("label", labelNamespace+"="+namespace) + filter.Add("label", composetransform.LabelNamespace+"="+namespace) return filter } diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index f1ab65ce95..e8238cde66 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -16,13 +16,12 @@ import ( composetypes "github.com/aanand/compose-file/types" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/mount" - networktypes "github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" dockerclient "github.com/docker/docker/client" "github.com/docker/docker/opts" + "github.com/docker/docker/pkg/composetransform" runconfigopts "github.com/docker/docker/runconfig/opts" "github.com/docker/go-connections/nat" ) @@ -121,9 +120,9 @@ func deployCompose(ctx context.Context, dockerCli *command.DockerCli, opts deplo return err } - namespace := namespace{name: opts.namespace} + namespace := composetransform.NewNamespace(opts.namespace) - networks, externalNetworks := convertNetworks(namespace, config.Networks) + networks, externalNetworks := composetransform.ConvertNetworks(namespace, config.Networks) if err := validateExternalNetworks(ctx, dockerCli, externalNetworks); err != nil { return err } @@ -204,12 +203,12 @@ func validateExternalNetworks( func createNetworks( ctx context.Context, dockerCli *command.DockerCli, - namespace namespace, + namespace composetransform.Namespace, networks map[string]types.NetworkCreate, ) error { client := dockerCli.Client() - existingNetworks, err := getStackNetworks(ctx, client, namespace.name) + existingNetworks, err := getStackNetworks(ctx, client, namespace.Name()) if err != nil { return err } @@ -220,7 +219,7 @@ func createNetworks( } for internalName, createOpts := range networks { - name := namespace.scope(internalName) + name := namespace.Scope(internalName) if _, exists := existingNetworkMap[name]; exists { continue } @@ -241,7 +240,7 @@ func createNetworks( func convertServiceNetworks( networks map[string]*composetypes.ServiceNetworkConfig, networkConfigs map[string]composetypes.NetworkConfig, - namespace namespace, + namespace composetransform.Namespace, name string, ) ([]swarm.NetworkAttachmentConfig, error) { if len(networks) == 0 { @@ -275,116 +274,6 @@ func convertServiceNetworks( return nets, nil } -func convertVolumes( - serviceVolumes []string, - stackVolumes map[string]composetypes.VolumeConfig, - namespace namespace, -) ([]mount.Mount, error) { - var mounts []mount.Mount - - for _, volumeSpec := range serviceVolumes { - mount, err := convertVolumeToMount(volumeSpec, stackVolumes, namespace) - if err != nil { - return nil, err - } - mounts = append(mounts, mount) - } - return mounts, nil -} - -func convertVolumeToMount( - volumeSpec string, - stackVolumes map[string]composetypes.VolumeConfig, - namespace namespace, -) (mount.Mount, error) { - var source, target string - var mode []string - - // TODO: split Windows path mappings properly - parts := strings.SplitN(volumeSpec, ":", 3) - - switch len(parts) { - case 3: - source = parts[0] - target = parts[1] - mode = strings.Split(parts[2], ",") - case 2: - source = parts[0] - target = parts[1] - case 1: - target = parts[0] - default: - return mount.Mount{}, fmt.Errorf("invald volume: %s", volumeSpec) - } - - // TODO: catch Windows paths here - if strings.HasPrefix(source, "/") { - return mount.Mount{ - Type: mount.TypeBind, - Source: source, - Target: target, - ReadOnly: isReadOnly(mode), - BindOptions: getBindOptions(mode), - }, nil - } - - stackVolume, exists := stackVolumes[source] - if !exists { - return mount.Mount{}, fmt.Errorf("undefined volume: %s", source) - } - - var volumeOptions *mount.VolumeOptions - if stackVolume.External.Name != "" { - source = stackVolume.External.Name - } else { - volumeOptions = &mount.VolumeOptions{ - Labels: getStackLabels(namespace.name, stackVolume.Labels), - NoCopy: isNoCopy(mode), - } - - if stackVolume.Driver != "" { - volumeOptions.DriverConfig = &mount.Driver{ - Name: stackVolume.Driver, - Options: stackVolume.DriverOpts, - } - } - source = namespace.scope(source) - } - return mount.Mount{ - Type: mount.TypeVolume, - Source: source, - Target: target, - ReadOnly: isReadOnly(mode), - VolumeOptions: volumeOptions, - }, nil -} - -func modeHas(mode []string, field string) bool { - for _, item := range mode { - if item == field { - return true - } - } - return false -} - -func isReadOnly(mode []string) bool { - return modeHas(mode, "ro") -} - -func isNoCopy(mode []string) bool { - return modeHas(mode, "nocopy") -} - -func getBindOptions(mode []string) *mount.BindOptions { - for _, item := range mode { - if strings.Contains(item, "private") || strings.Contains(item, "shared") || strings.Contains(item, "slave") { - return &mount.BindOptions{Propagation: mount.Propagation(item)} - } - } - return nil -} - func deployServices( ctx context.Context, dockerCli *command.DockerCli, @@ -494,7 +383,7 @@ func convertService( return swarm.ServiceSpec{}, err } - mounts, err := convertVolumes(service.Volumes, volumes, namespace) + mounts, err := composetransform.ConvertVolumes(service.Volumes, volumes, namespace) if err != nil { // TODO: better error message (include service name) return swarm.ServiceSpec{}, err From 415fa73d467181aa585a4a3959accd59c6adf1de Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 30 Nov 2016 17:38:40 -0500 Subject: [PATCH 468/978] Move ConvertService to composetransform package. Signed-off-by: Daniel Nephin Upstream-commit: 31355030b38d6e856098df8d790feebeb9edddc6 Component: cli --- components/cli/command/stack/common.go | 13 + components/cli/command/stack/deploy.go | 327 +----------------- .../cli/command/stack/deploy_bundlefile.go | 13 +- components/cli/command/stack/list.go | 12 +- components/cli/command/stack/ps.go | 3 +- components/cli/command/stack/services.go | 4 +- 6 files changed, 30 insertions(+), 342 deletions(-) diff --git a/components/cli/command/stack/common.go b/components/cli/command/stack/common.go index 050528de4e..c3a43f2cd8 100644 --- a/components/cli/command/stack/common.go +++ b/components/cli/command/stack/common.go @@ -7,6 +7,7 @@ import ( "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/client" + "github.com/docker/docker/opts" "github.com/docker/docker/pkg/composetransform" ) @@ -16,6 +17,18 @@ func getStackFilter(namespace string) filters.Args { return filter } +func getStackFilterFromOpt(namespace string, opt opts.FilterOpt) filters.Args { + filter := opt.Value() + filter.Add("label", composetransform.LabelNamespace+"="+namespace) + return filter +} + +func getAllStacksFilter() filters.Args { + filter := filters.NewArgs() + filter.Add("label", composetransform.LabelNamespace) + return filter +} + func getServices( ctx context.Context, apiclient client.APIClient, diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index e8238cde66..957f92f29e 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -7,7 +7,6 @@ import ( "os" "sort" "strings" - "time" "github.com/spf13/cobra" "golang.org/x/net/context" @@ -15,15 +14,11 @@ import ( "github.com/aanand/compose-file/loader" composetypes "github.com/aanand/compose-file/types" "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" dockerclient "github.com/docker/docker/client" - "github.com/docker/docker/opts" "github.com/docker/docker/pkg/composetransform" - runconfigopts "github.com/docker/docker/runconfig/opts" - "github.com/docker/go-connections/nat" ) const ( @@ -129,7 +124,7 @@ func deployCompose(ctx context.Context, dockerCli *command.DockerCli, opts deplo if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil { return err } - services, err := convertServices(namespace, config) + services, err := composetransform.ConvertServices(namespace, config) if err != nil { return err } @@ -237,54 +232,17 @@ func createNetworks( return nil } -func convertServiceNetworks( - networks map[string]*composetypes.ServiceNetworkConfig, - networkConfigs map[string]composetypes.NetworkConfig, - namespace composetransform.Namespace, - name string, -) ([]swarm.NetworkAttachmentConfig, error) { - if len(networks) == 0 { - return []swarm.NetworkAttachmentConfig{ - { - Target: namespace.scope("default"), - Aliases: []string{name}, - }, - }, nil - } - - nets := []swarm.NetworkAttachmentConfig{} - for networkName, network := range networks { - networkConfig, ok := networkConfigs[networkName] - if !ok { - return []swarm.NetworkAttachmentConfig{}, fmt.Errorf("invalid network: %s", networkName) - } - var aliases []string - if network != nil { - aliases = network.Aliases - } - target := namespace.scope(networkName) - if networkConfig.External.External { - target = networkName - } - nets = append(nets, swarm.NetworkAttachmentConfig{ - Target: target, - Aliases: append(aliases, name), - }) - } - return nets, nil -} - func deployServices( ctx context.Context, dockerCli *command.DockerCli, services map[string]swarm.ServiceSpec, - namespace namespace, + namespace composetransform.Namespace, sendAuth bool, ) error { apiClient := dockerCli.Client() out := dockerCli.Out() - existingServices, err := getServices(ctx, apiClient, namespace.name) + existingServices, err := getServices(ctx, apiClient, namespace.Name()) if err != nil { return err } @@ -295,7 +253,7 @@ func deployServices( } for internalName, serviceSpec := range services { - name := namespace.scope(internalName) + name := namespace.Scope(internalName) encodedAuth := "" if sendAuth { @@ -343,280 +301,3 @@ func deployServices( return nil } - -func convertServices( - namespace namespace, - config *composetypes.Config, -) (map[string]swarm.ServiceSpec, error) { - result := make(map[string]swarm.ServiceSpec) - - services := config.Services - volumes := config.Volumes - networks := config.Networks - - for _, service := range services { - serviceSpec, err := convertService(namespace, service, networks, volumes) - if err != nil { - return nil, err - } - result[service.Name] = serviceSpec - } - - return result, nil -} - -func convertService( - namespace namespace, - service composetypes.ServiceConfig, - networkConfigs map[string]composetypes.NetworkConfig, - volumes map[string]composetypes.VolumeConfig, -) (swarm.ServiceSpec, error) { - name := namespace.scope(service.Name) - - endpoint, err := convertEndpointSpec(service.Ports) - if err != nil { - return swarm.ServiceSpec{}, err - } - - mode, err := convertDeployMode(service.Deploy.Mode, service.Deploy.Replicas) - if err != nil { - return swarm.ServiceSpec{}, err - } - - mounts, err := composetransform.ConvertVolumes(service.Volumes, volumes, namespace) - if err != nil { - // TODO: better error message (include service name) - return swarm.ServiceSpec{}, err - } - - resources, err := convertResources(service.Deploy.Resources) - if err != nil { - return swarm.ServiceSpec{}, err - } - - restartPolicy, err := convertRestartPolicy( - service.Restart, service.Deploy.RestartPolicy) - if err != nil { - return swarm.ServiceSpec{}, err - } - - healthcheck, err := convertHealthcheck(service.HealthCheck) - if err != nil { - return swarm.ServiceSpec{}, err - } - - networks, err := convertServiceNetworks(service.Networks, networkConfigs, namespace, service.Name) - if err != nil { - return swarm.ServiceSpec{}, err - } - - var logDriver *swarm.Driver - if service.Logging != nil { - logDriver = &swarm.Driver{ - Name: service.Logging.Driver, - Options: service.Logging.Options, - } - } - - serviceSpec := swarm.ServiceSpec{ - Annotations: swarm.Annotations{ - Name: name, - Labels: getStackLabels(namespace.name, service.Deploy.Labels), - }, - TaskTemplate: swarm.TaskSpec{ - ContainerSpec: swarm.ContainerSpec{ - Image: service.Image, - Command: service.Entrypoint, - Args: service.Command, - Hostname: service.Hostname, - Hosts: convertExtraHosts(service.ExtraHosts), - Healthcheck: healthcheck, - Env: convertEnvironment(service.Environment), - Labels: getStackLabels(namespace.name, service.Labels), - Dir: service.WorkingDir, - User: service.User, - Mounts: mounts, - StopGracePeriod: service.StopGracePeriod, - TTY: service.Tty, - OpenStdin: service.StdinOpen, - }, - LogDriver: logDriver, - Resources: resources, - RestartPolicy: restartPolicy, - Placement: &swarm.Placement{ - Constraints: service.Deploy.Placement.Constraints, - }, - }, - EndpointSpec: endpoint, - Mode: mode, - Networks: networks, - UpdateConfig: convertUpdateConfig(service.Deploy.UpdateConfig), - } - - return serviceSpec, nil -} - -func convertExtraHosts(extraHosts map[string]string) []string { - hosts := []string{} - for host, ip := range extraHosts { - hosts = append(hosts, fmt.Sprintf("%s %s", ip, host)) - } - return hosts -} - -func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container.HealthConfig, error) { - if healthcheck == nil { - return nil, nil - } - var ( - err error - timeout, interval time.Duration - retries int - ) - if healthcheck.Disable { - if len(healthcheck.Test) != 0 { - return nil, fmt.Errorf("command and disable key can't be set at the same time") - } - return &container.HealthConfig{ - Test: []string{"NONE"}, - }, nil - - } - if healthcheck.Timeout != "" { - timeout, err = time.ParseDuration(healthcheck.Timeout) - if err != nil { - return nil, err - } - } - if healthcheck.Interval != "" { - interval, err = time.ParseDuration(healthcheck.Interval) - if err != nil { - return nil, err - } - } - if healthcheck.Retries != nil { - retries = int(*healthcheck.Retries) - } - return &container.HealthConfig{ - Test: healthcheck.Test, - Timeout: timeout, - Interval: interval, - Retries: retries, - }, nil -} - -func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (*swarm.RestartPolicy, error) { - // TODO: log if restart is being ignored - if source == nil { - policy, err := runconfigopts.ParseRestartPolicy(restart) - if err != nil { - return nil, err - } - // TODO: is this an accurate convertion? - switch { - case policy.IsNone(): - return nil, nil - case policy.IsAlways(), policy.IsUnlessStopped(): - return &swarm.RestartPolicy{ - Condition: swarm.RestartPolicyConditionAny, - }, nil - case policy.IsOnFailure(): - attempts := uint64(policy.MaximumRetryCount) - return &swarm.RestartPolicy{ - Condition: swarm.RestartPolicyConditionOnFailure, - MaxAttempts: &attempts, - }, nil - } - } - return &swarm.RestartPolicy{ - Condition: swarm.RestartPolicyCondition(source.Condition), - Delay: source.Delay, - MaxAttempts: source.MaxAttempts, - Window: source.Window, - }, nil -} - -func convertUpdateConfig(source *composetypes.UpdateConfig) *swarm.UpdateConfig { - if source == nil { - return nil - } - parallel := uint64(1) - if source.Parallelism != nil { - parallel = *source.Parallelism - } - return &swarm.UpdateConfig{ - Parallelism: parallel, - Delay: source.Delay, - FailureAction: source.FailureAction, - Monitor: source.Monitor, - MaxFailureRatio: source.MaxFailureRatio, - } -} - -func convertResources(source composetypes.Resources) (*swarm.ResourceRequirements, error) { - resources := &swarm.ResourceRequirements{} - if source.Limits != nil { - cpus, err := opts.ParseCPUs(source.Limits.NanoCPUs) - if err != nil { - return nil, err - } - resources.Limits = &swarm.Resources{ - NanoCPUs: cpus, - MemoryBytes: int64(source.Limits.MemoryBytes), - } - } - if source.Reservations != nil { - cpus, err := opts.ParseCPUs(source.Reservations.NanoCPUs) - if err != nil { - return nil, err - } - resources.Reservations = &swarm.Resources{ - NanoCPUs: cpus, - MemoryBytes: int64(source.Reservations.MemoryBytes), - } - } - return resources, nil -} - -func convertEndpointSpec(source []string) (*swarm.EndpointSpec, error) { - portConfigs := []swarm.PortConfig{} - ports, portBindings, err := nat.ParsePortSpecs(source) - if err != nil { - return nil, err - } - - for port := range ports { - portConfigs = append( - portConfigs, - opts.ConvertPortToPortConfig(port, portBindings)...) - } - - return &swarm.EndpointSpec{Ports: portConfigs}, nil -} - -func convertEnvironment(source map[string]string) []string { - var output []string - - for name, value := range source { - output = append(output, fmt.Sprintf("%s=%s", name, value)) - } - - return output -} - -func convertDeployMode(mode string, replicas *uint64) (swarm.ServiceMode, error) { - serviceMode := swarm.ServiceMode{} - - switch mode { - case "global": - if replicas != nil { - return serviceMode, fmt.Errorf("replicas can only be used with replicated mode") - } - serviceMode.Global = &swarm.GlobalService{} - case "replicated", "": - serviceMode.Replicated = &swarm.ReplicatedService{Replicas: replicas} - default: - return serviceMode, fmt.Errorf("Unknown mode: %s", mode) - } - return serviceMode, nil -} diff --git a/components/cli/command/stack/deploy_bundlefile.go b/components/cli/command/stack/deploy_bundlefile.go index c82c46e424..f9a4162389 100644 --- a/components/cli/command/stack/deploy_bundlefile.go +++ b/components/cli/command/stack/deploy_bundlefile.go @@ -6,6 +6,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli/command" + "github.com/docker/docker/pkg/composetransform" ) func deployBundle(ctx context.Context, dockerCli *command.DockerCli, opts deployOptions) error { @@ -18,20 +19,20 @@ func deployBundle(ctx context.Context, dockerCli *command.DockerCli, opts deploy return err } - namespace := namespace{name: opts.namespace} + namespace := composetransform.NewNamespace(opts.namespace) networks := make(map[string]types.NetworkCreate) for _, service := range bundle.Services { for _, networkName := range service.Networks { networks[networkName] = types.NetworkCreate{ - Labels: getStackLabels(namespace.name, nil), + Labels: composetransform.AddStackLabel(namespace, nil), } } } services := make(map[string]swarm.ServiceSpec) for internalName, service := range bundle.Services { - name := namespace.scope(internalName) + name := namespace.Scope(internalName) var ports []swarm.PortConfig for _, portSpec := range service.Ports { @@ -44,7 +45,7 @@ func deployBundle(ctx context.Context, dockerCli *command.DockerCli, opts deploy nets := []swarm.NetworkAttachmentConfig{} for _, networkName := range service.Networks { nets = append(nets, swarm.NetworkAttachmentConfig{ - Target: namespace.scope(networkName), + Target: namespace.Scope(networkName), Aliases: []string{networkName}, }) } @@ -52,7 +53,7 @@ func deployBundle(ctx context.Context, dockerCli *command.DockerCli, opts deploy serviceSpec := swarm.ServiceSpec{ Annotations: swarm.Annotations{ Name: name, - Labels: getStackLabels(namespace.name, service.Labels), + Labels: composetransform.AddStackLabel(namespace, service.Labels), }, TaskTemplate: swarm.TaskSpec{ ContainerSpec: swarm.ContainerSpec{ @@ -63,7 +64,7 @@ func deployBundle(ctx context.Context, dockerCli *command.DockerCli, opts deploy // Service Labels will not be copied to Containers // automatically during the deployment so we apply // it here. - Labels: getStackLabels(namespace.name, nil), + Labels: composetransform.AddStackLabel(namespace, nil), }, }, EndpointSpec: &swarm.EndpointSpec{ diff --git a/components/cli/command/stack/list.go b/components/cli/command/stack/list.go index f655b929ad..52e593316e 100644 --- a/components/cli/command/stack/list.go +++ b/components/cli/command/stack/list.go @@ -9,10 +9,10 @@ import ( "golang.org/x/net/context" "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/client" + "github.com/docker/docker/pkg/composetransform" "github.com/spf13/cobra" ) @@ -81,23 +81,19 @@ func getStacks( ctx context.Context, apiclient client.APIClient, ) ([]*stack, error) { - - filter := filters.NewArgs() - filter.Add("label", labelNamespace) - services, err := apiclient.ServiceList( ctx, - types.ServiceListOptions{Filters: filter}) + types.ServiceListOptions{Filters: getAllStacksFilter()}) if err != nil { return nil, err } m := make(map[string]*stack, 0) for _, service := range services { labels := service.Spec.Labels - name, ok := labels[labelNamespace] + name, ok := labels[composetransform.LabelNamespace] if !ok { return nil, fmt.Errorf("cannot get label %s for service %s", - labelNamespace, service.ID) + composetransform.LabelNamespace, service.ID) } ztack, ok := m[name] if !ok { diff --git a/components/cli/command/stack/ps.go b/components/cli/command/stack/ps.go index 7a5e069cbe..497fb97b5e 100644 --- a/components/cli/command/stack/ps.go +++ b/components/cli/command/stack/ps.go @@ -49,8 +49,7 @@ func runPS(dockerCli *command.DockerCli, opts psOptions) error { client := dockerCli.Client() ctx := context.Background() - filter := opts.filter.Value() - filter.Add("label", labelNamespace+"="+opts.namespace) + filter := getStackFilterFromOpt(opts.namespace, opts.filter) if !opts.all && !filter.Include("desired-state") { filter.Add("desired-state", string(swarm.TaskStateRunning)) filter.Add("desired-state", string(swarm.TaskStateAccepted)) diff --git a/components/cli/command/stack/services.go b/components/cli/command/stack/services.go index 1ca1c8c129..a46652df7c 100644 --- a/components/cli/command/stack/services.go +++ b/components/cli/command/stack/services.go @@ -43,9 +43,7 @@ func runServices(dockerCli *command.DockerCli, opts servicesOptions) error { ctx := context.Background() client := dockerCli.Client() - filter := opts.filter.Value() - filter.Add("label", labelNamespace+"="+opts.namespace) - + filter := getStackFilterFromOpt(opts.namespace, opts.filter) services, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: filter}) if err != nil { return err From 49a03faa441fb47cb3c243e2a6f4190a92b1720a Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 5 Dec 2016 16:14:08 -0500 Subject: [PATCH 469/978] Move pkg to cli/compose/convert Signed-off-by: Daniel Nephin Upstream-commit: c4ea22972f6e50789eb8472ffda9974cf20f5b61 Component: cli --- components/cli/command/stack/common.go | 8 +- components/cli/command/stack/deploy.go | 12 +- .../cli/command/stack/deploy_bundlefile.go | 10 +- components/cli/command/stack/list.go | 6 +- components/cli/compose/convert/compose.go | 88 +++++ .../cli/compose/convert/compose_test.go | 85 +++++ components/cli/compose/convert/service.go | 330 ++++++++++++++++++ .../cli/compose/convert/service_test.go | 193 ++++++++++ components/cli/compose/convert/volume.go | 116 ++++++ components/cli/compose/convert/volume_test.go | 112 ++++++ 10 files changed, 942 insertions(+), 18 deletions(-) create mode 100644 components/cli/compose/convert/compose.go create mode 100644 components/cli/compose/convert/compose_test.go create mode 100644 components/cli/compose/convert/service.go create mode 100644 components/cli/compose/convert/service_test.go create mode 100644 components/cli/compose/convert/volume.go create mode 100644 components/cli/compose/convert/volume_test.go diff --git a/components/cli/command/stack/common.go b/components/cli/command/stack/common.go index c3a43f2cd8..5c4996d666 100644 --- a/components/cli/command/stack/common.go +++ b/components/cli/command/stack/common.go @@ -6,26 +6,26 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/compose/convert" "github.com/docker/docker/client" "github.com/docker/docker/opts" - "github.com/docker/docker/pkg/composetransform" ) func getStackFilter(namespace string) filters.Args { filter := filters.NewArgs() - filter.Add("label", composetransform.LabelNamespace+"="+namespace) + filter.Add("label", convert.LabelNamespace+"="+namespace) return filter } func getStackFilterFromOpt(namespace string, opt opts.FilterOpt) filters.Args { filter := opt.Value() - filter.Add("label", composetransform.LabelNamespace+"="+namespace) + filter.Add("label", convert.LabelNamespace+"="+namespace) return filter } func getAllStacksFilter() filters.Args { filter := filters.NewArgs() - filter.Add("label", composetransform.LabelNamespace) + filter.Add("label", convert.LabelNamespace) return filter } diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index 957f92f29e..32ebd62d3f 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -17,8 +17,8 @@ import ( "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/compose/convert" dockerclient "github.com/docker/docker/client" - "github.com/docker/docker/pkg/composetransform" ) const ( @@ -115,16 +115,16 @@ func deployCompose(ctx context.Context, dockerCli *command.DockerCli, opts deplo return err } - namespace := composetransform.NewNamespace(opts.namespace) + namespace := convert.NewNamespace(opts.namespace) - networks, externalNetworks := composetransform.ConvertNetworks(namespace, config.Networks) + networks, externalNetworks := convert.Networks(namespace, config.Networks) if err := validateExternalNetworks(ctx, dockerCli, externalNetworks); err != nil { return err } if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil { return err } - services, err := composetransform.ConvertServices(namespace, config) + services, err := convert.Services(namespace, config) if err != nil { return err } @@ -198,7 +198,7 @@ func validateExternalNetworks( func createNetworks( ctx context.Context, dockerCli *command.DockerCli, - namespace composetransform.Namespace, + namespace convert.Namespace, networks map[string]types.NetworkCreate, ) error { client := dockerCli.Client() @@ -236,7 +236,7 @@ func deployServices( ctx context.Context, dockerCli *command.DockerCli, services map[string]swarm.ServiceSpec, - namespace composetransform.Namespace, + namespace convert.Namespace, sendAuth bool, ) error { apiClient := dockerCli.Client() diff --git a/components/cli/command/stack/deploy_bundlefile.go b/components/cli/command/stack/deploy_bundlefile.go index f9a4162389..5a178c4ab6 100644 --- a/components/cli/command/stack/deploy_bundlefile.go +++ b/components/cli/command/stack/deploy_bundlefile.go @@ -6,7 +6,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli/command" - "github.com/docker/docker/pkg/composetransform" + "github.com/docker/docker/cli/compose/convert" ) func deployBundle(ctx context.Context, dockerCli *command.DockerCli, opts deployOptions) error { @@ -19,13 +19,13 @@ func deployBundle(ctx context.Context, dockerCli *command.DockerCli, opts deploy return err } - namespace := composetransform.NewNamespace(opts.namespace) + namespace := convert.NewNamespace(opts.namespace) networks := make(map[string]types.NetworkCreate) for _, service := range bundle.Services { for _, networkName := range service.Networks { networks[networkName] = types.NetworkCreate{ - Labels: composetransform.AddStackLabel(namespace, nil), + Labels: convert.AddStackLabel(namespace, nil), } } } @@ -53,7 +53,7 @@ func deployBundle(ctx context.Context, dockerCli *command.DockerCli, opts deploy serviceSpec := swarm.ServiceSpec{ Annotations: swarm.Annotations{ Name: name, - Labels: composetransform.AddStackLabel(namespace, service.Labels), + Labels: convert.AddStackLabel(namespace, service.Labels), }, TaskTemplate: swarm.TaskSpec{ ContainerSpec: swarm.ContainerSpec{ @@ -64,7 +64,7 @@ func deployBundle(ctx context.Context, dockerCli *command.DockerCli, opts deploy // Service Labels will not be copied to Containers // automatically during the deployment so we apply // it here. - Labels: composetransform.AddStackLabel(namespace, nil), + Labels: convert.AddStackLabel(namespace, nil), }, }, EndpointSpec: &swarm.EndpointSpec{ diff --git a/components/cli/command/stack/list.go b/components/cli/command/stack/list.go index 52e593316e..9b6c645e29 100644 --- a/components/cli/command/stack/list.go +++ b/components/cli/command/stack/list.go @@ -11,8 +11,8 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/compose/convert" "github.com/docker/docker/client" - "github.com/docker/docker/pkg/composetransform" "github.com/spf13/cobra" ) @@ -90,10 +90,10 @@ func getStacks( m := make(map[string]*stack, 0) for _, service := range services { labels := service.Spec.Labels - name, ok := labels[composetransform.LabelNamespace] + name, ok := labels[convert.LabelNamespace] if !ok { return nil, fmt.Errorf("cannot get label %s for service %s", - composetransform.LabelNamespace, service.ID) + convert.LabelNamespace, service.ID) } ztack, ok := m[name] if !ok { diff --git a/components/cli/compose/convert/compose.go b/components/cli/compose/convert/compose.go new file mode 100644 index 0000000000..e0684482b8 --- /dev/null +++ b/components/cli/compose/convert/compose.go @@ -0,0 +1,88 @@ +package convert + +import ( + composetypes "github.com/aanand/compose-file/types" + "github.com/docker/docker/api/types" + networktypes "github.com/docker/docker/api/types/network" +) + +const ( + // LabelNamespace is the label used to track stack resources + LabelNamespace = "com.docker.stack.namespace" +) + +// Namespace mangles names by prepending the name +type Namespace struct { + name string +} + +// Scope prepends the namespace to a name +func (n Namespace) Scope(name string) string { + return n.name + "_" + name +} + +// Name returns the name of the namespace +func (n Namespace) Name() string { + return n.name +} + +// NewNamespace returns a new Namespace for scoping of names +func NewNamespace(name string) Namespace { + return Namespace{name: name} +} + +// AddStackLabel returns labels with the namespace label added +func AddStackLabel(namespace Namespace, labels map[string]string) map[string]string { + if labels == nil { + labels = make(map[string]string) + } + labels[LabelNamespace] = namespace.name + return labels +} + +type networkMap map[string]composetypes.NetworkConfig + +// Networks from the compose-file type to the engine API type +func Networks(namespace Namespace, networks networkMap) (map[string]types.NetworkCreate, []string) { + if networks == nil { + networks = make(map[string]composetypes.NetworkConfig) + } + + // TODO: only add default network if it's used + if _, ok := networks["default"]; !ok { + networks["default"] = composetypes.NetworkConfig{} + } + + externalNetworks := []string{} + result := make(map[string]types.NetworkCreate) + + for internalName, network := range networks { + if network.External.External { + externalNetworks = append(externalNetworks, network.External.Name) + continue + } + + createOpts := types.NetworkCreate{ + Labels: AddStackLabel(namespace, network.Labels), + Driver: network.Driver, + Options: network.DriverOpts, + } + + if network.Ipam.Driver != "" || len(network.Ipam.Config) > 0 { + createOpts.IPAM = &networktypes.IPAM{} + } + + if network.Ipam.Driver != "" { + createOpts.IPAM.Driver = network.Ipam.Driver + } + for _, ipamConfig := range network.Ipam.Config { + config := networktypes.IPAMConfig{ + Subnet: ipamConfig.Subnet, + } + createOpts.IPAM.Config = append(createOpts.IPAM.Config, config) + } + result[internalName] = createOpts + } + + return result, externalNetworks +} diff --git a/components/cli/compose/convert/compose_test.go b/components/cli/compose/convert/compose_test.go new file mode 100644 index 0000000000..8f8e8ea6d8 --- /dev/null +++ b/components/cli/compose/convert/compose_test.go @@ -0,0 +1,85 @@ +package convert + +import ( + "testing" + + composetypes "github.com/aanand/compose-file/types" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/network" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestNamespaceScope(t *testing.T) { + scoped := Namespace{name: "foo"}.Scope("bar") + assert.Equal(t, scoped, "foo_bar") +} + +func TestAddStackLabel(t *testing.T) { + labels := map[string]string{ + "something": "labeled", + } + actual := AddStackLabel(Namespace{name: "foo"}, labels) + expected := map[string]string{ + "something": "labeled", + LabelNamespace: "foo", + } + assert.DeepEqual(t, actual, expected) +} + +func TestNetworks(t *testing.T) { + namespace := Namespace{name: "foo"} + source := networkMap{ + "normal": composetypes.NetworkConfig{ + Driver: "overlay", + DriverOpts: map[string]string{ + "opt": "value", + }, + Ipam: composetypes.IPAMConfig{ + Driver: "driver", + Config: []*composetypes.IPAMPool{ + { + Subnet: "10.0.0.0", + }, + }, + }, + Labels: map[string]string{ + "something": "labeled", + }, + }, + "outside": composetypes.NetworkConfig{ + External: composetypes.External{ + External: true, + Name: "special", + }, + }, + } + expected := map[string]types.NetworkCreate{ + "default": { + Labels: map[string]string{ + LabelNamespace: "foo", + }, + }, + "normal": { + Driver: "overlay", + IPAM: &network.IPAM{ + Driver: "driver", + Config: []network.IPAMConfig{ + { + Subnet: "10.0.0.0", + }, + }, + }, + Options: map[string]string{ + "opt": "value", + }, + Labels: map[string]string{ + LabelNamespace: "foo", + "something": "labeled", + }, + }, + } + + networks, externals := Networks(namespace, source) + assert.DeepEqual(t, networks, expected) + assert.DeepEqual(t, externals, []string{"special"}) +} diff --git a/components/cli/compose/convert/service.go b/components/cli/compose/convert/service.go new file mode 100644 index 0000000000..458b518a46 --- /dev/null +++ b/components/cli/compose/convert/service.go @@ -0,0 +1,330 @@ +package convert + +import ( + "fmt" + "time" + + composetypes "github.com/aanand/compose-file/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/opts" + runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/docker/go-connections/nat" +) + +// Services from compose-file types to engine API types +func Services( + namespace Namespace, + config *composetypes.Config, +) (map[string]swarm.ServiceSpec, error) { + result := make(map[string]swarm.ServiceSpec) + + services := config.Services + volumes := config.Volumes + networks := config.Networks + + for _, service := range services { + serviceSpec, err := convertService(namespace, service, networks, volumes) + if err != nil { + return nil, err + } + result[service.Name] = serviceSpec + } + + return result, nil +} + +func convertService( + namespace Namespace, + service composetypes.ServiceConfig, + networkConfigs map[string]composetypes.NetworkConfig, + volumes map[string]composetypes.VolumeConfig, +) (swarm.ServiceSpec, error) { + name := namespace.Scope(service.Name) + + endpoint, err := convertEndpointSpec(service.Ports) + if err != nil { + return swarm.ServiceSpec{}, err + } + + mode, err := convertDeployMode(service.Deploy.Mode, service.Deploy.Replicas) + if err != nil { + return swarm.ServiceSpec{}, err + } + + mounts, err := Volumes(service.Volumes, volumes, namespace) + if err != nil { + // TODO: better error message (include service name) + return swarm.ServiceSpec{}, err + } + + resources, err := convertResources(service.Deploy.Resources) + if err != nil { + return swarm.ServiceSpec{}, err + } + + restartPolicy, err := convertRestartPolicy( + service.Restart, service.Deploy.RestartPolicy) + if err != nil { + return swarm.ServiceSpec{}, err + } + + healthcheck, err := convertHealthcheck(service.HealthCheck) + if err != nil { + return swarm.ServiceSpec{}, err + } + + networks, err := convertServiceNetworks(service.Networks, networkConfigs, namespace, service.Name) + if err != nil { + return swarm.ServiceSpec{}, err + } + + var logDriver *swarm.Driver + if service.Logging != nil { + logDriver = &swarm.Driver{ + Name: service.Logging.Driver, + Options: service.Logging.Options, + } + } + + serviceSpec := swarm.ServiceSpec{ + Annotations: swarm.Annotations{ + Name: name, + Labels: AddStackLabel(namespace, service.Deploy.Labels), + }, + TaskTemplate: swarm.TaskSpec{ + ContainerSpec: swarm.ContainerSpec{ + Image: service.Image, + Command: service.Entrypoint, + Args: service.Command, + Hostname: service.Hostname, + Hosts: convertExtraHosts(service.ExtraHosts), + Healthcheck: healthcheck, + Env: convertEnvironment(service.Environment), + Labels: AddStackLabel(namespace, service.Labels), + Dir: service.WorkingDir, + User: service.User, + Mounts: mounts, + StopGracePeriod: service.StopGracePeriod, + TTY: service.Tty, + OpenStdin: service.StdinOpen, + }, + LogDriver: logDriver, + Resources: resources, + RestartPolicy: restartPolicy, + Placement: &swarm.Placement{ + Constraints: service.Deploy.Placement.Constraints, + }, + }, + EndpointSpec: endpoint, + Mode: mode, + Networks: networks, + UpdateConfig: convertUpdateConfig(service.Deploy.UpdateConfig), + } + + return serviceSpec, nil +} + +func convertServiceNetworks( + networks map[string]*composetypes.ServiceNetworkConfig, + networkConfigs networkMap, + namespace Namespace, + name string, +) ([]swarm.NetworkAttachmentConfig, error) { + if len(networks) == 0 { + return []swarm.NetworkAttachmentConfig{ + { + Target: namespace.Scope("default"), + Aliases: []string{name}, + }, + }, nil + } + + nets := []swarm.NetworkAttachmentConfig{} + for networkName, network := range networks { + networkConfig, ok := networkConfigs[networkName] + if !ok { + return []swarm.NetworkAttachmentConfig{}, fmt.Errorf( + "service %q references network %q, which is not declared", name, networkName) + } + var aliases []string + if network != nil { + aliases = network.Aliases + } + target := namespace.Scope(networkName) + if networkConfig.External.External { + target = networkConfig.External.Name + } + nets = append(nets, swarm.NetworkAttachmentConfig{ + Target: target, + Aliases: append(aliases, name), + }) + } + return nets, nil +} + +func convertExtraHosts(extraHosts map[string]string) []string { + hosts := []string{} + for host, ip := range extraHosts { + hosts = append(hosts, fmt.Sprintf("%s %s", ip, host)) + } + return hosts +} + +func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container.HealthConfig, error) { + if healthcheck == nil { + return nil, nil + } + var ( + err error + timeout, interval time.Duration + retries int + ) + if healthcheck.Disable { + if len(healthcheck.Test) != 0 { + return nil, fmt.Errorf("test and disable can't be set at the same time") + } + return &container.HealthConfig{ + Test: []string{"NONE"}, + }, nil + + } + if healthcheck.Timeout != "" { + timeout, err = time.ParseDuration(healthcheck.Timeout) + if err != nil { + return nil, err + } + } + if healthcheck.Interval != "" { + interval, err = time.ParseDuration(healthcheck.Interval) + if err != nil { + return nil, err + } + } + if healthcheck.Retries != nil { + retries = int(*healthcheck.Retries) + } + return &container.HealthConfig{ + Test: healthcheck.Test, + Timeout: timeout, + Interval: interval, + Retries: retries, + }, nil +} + +func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (*swarm.RestartPolicy, error) { + // TODO: log if restart is being ignored + if source == nil { + policy, err := runconfigopts.ParseRestartPolicy(restart) + if err != nil { + return nil, err + } + switch { + case policy.IsNone(): + return nil, nil + case policy.IsAlways(), policy.IsUnlessStopped(): + return &swarm.RestartPolicy{ + Condition: swarm.RestartPolicyConditionAny, + }, nil + case policy.IsOnFailure(): + attempts := uint64(policy.MaximumRetryCount) + return &swarm.RestartPolicy{ + Condition: swarm.RestartPolicyConditionOnFailure, + MaxAttempts: &attempts, + }, nil + default: + return nil, fmt.Errorf("unknown restart policy: %s", restart) + } + } + return &swarm.RestartPolicy{ + Condition: swarm.RestartPolicyCondition(source.Condition), + Delay: source.Delay, + MaxAttempts: source.MaxAttempts, + Window: source.Window, + }, nil +} + +func convertUpdateConfig(source *composetypes.UpdateConfig) *swarm.UpdateConfig { + if source == nil { + return nil + } + parallel := uint64(1) + if source.Parallelism != nil { + parallel = *source.Parallelism + } + return &swarm.UpdateConfig{ + Parallelism: parallel, + Delay: source.Delay, + FailureAction: source.FailureAction, + Monitor: source.Monitor, + MaxFailureRatio: source.MaxFailureRatio, + } +} + +func convertResources(source composetypes.Resources) (*swarm.ResourceRequirements, error) { + resources := &swarm.ResourceRequirements{} + if source.Limits != nil { + cpus, err := opts.ParseCPUs(source.Limits.NanoCPUs) + if err != nil { + return nil, err + } + resources.Limits = &swarm.Resources{ + NanoCPUs: cpus, + MemoryBytes: int64(source.Limits.MemoryBytes), + } + } + if source.Reservations != nil { + cpus, err := opts.ParseCPUs(source.Reservations.NanoCPUs) + if err != nil { + return nil, err + } + resources.Reservations = &swarm.Resources{ + NanoCPUs: cpus, + MemoryBytes: int64(source.Reservations.MemoryBytes), + } + } + return resources, nil +} + +func convertEndpointSpec(source []string) (*swarm.EndpointSpec, error) { + portConfigs := []swarm.PortConfig{} + ports, portBindings, err := nat.ParsePortSpecs(source) + if err != nil { + return nil, err + } + + for port := range ports { + portConfigs = append( + portConfigs, + opts.ConvertPortToPortConfig(port, portBindings)...) + } + + return &swarm.EndpointSpec{Ports: portConfigs}, nil +} + +func convertEnvironment(source map[string]string) []string { + var output []string + + for name, value := range source { + output = append(output, fmt.Sprintf("%s=%s", name, value)) + } + + return output +} + +func convertDeployMode(mode string, replicas *uint64) (swarm.ServiceMode, error) { + serviceMode := swarm.ServiceMode{} + + switch mode { + case "global": + if replicas != nil { + return serviceMode, fmt.Errorf("replicas can only be used with replicated mode") + } + serviceMode.Global = &swarm.GlobalService{} + case "replicated", "": + serviceMode.Replicated = &swarm.ReplicatedService{Replicas: replicas} + default: + return serviceMode, fmt.Errorf("Unknown mode: %s", mode) + } + return serviceMode, nil +} diff --git a/components/cli/compose/convert/service_test.go b/components/cli/compose/convert/service_test.go new file mode 100644 index 0000000000..a6884917de --- /dev/null +++ b/components/cli/compose/convert/service_test.go @@ -0,0 +1,193 @@ +package convert + +import ( + "sort" + "strings" + "testing" + "time" + + composetypes "github.com/aanand/compose-file/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestConvertRestartPolicyFromNone(t *testing.T) { + policy, err := convertRestartPolicy("no", nil) + assert.NilError(t, err) + assert.Equal(t, policy, (*swarm.RestartPolicy)(nil)) +} + +func TestConvertRestartPolicyFromUnknown(t *testing.T) { + _, err := convertRestartPolicy("unknown", nil) + assert.Error(t, err, "unknown restart policy: unknown") +} + +func TestConvertRestartPolicyFromAlways(t *testing.T) { + policy, err := convertRestartPolicy("always", nil) + expected := &swarm.RestartPolicy{ + Condition: swarm.RestartPolicyConditionAny, + } + assert.NilError(t, err) + assert.DeepEqual(t, policy, expected) +} + +func TestConvertRestartPolicyFromFailure(t *testing.T) { + policy, err := convertRestartPolicy("on-failure:4", nil) + attempts := uint64(4) + expected := &swarm.RestartPolicy{ + Condition: swarm.RestartPolicyConditionOnFailure, + MaxAttempts: &attempts, + } + assert.NilError(t, err) + assert.DeepEqual(t, policy, expected) +} + +func TestConvertEnvironment(t *testing.T) { + source := map[string]string{ + "foo": "bar", + "key": "value", + } + env := convertEnvironment(source) + sort.Strings(env) + assert.DeepEqual(t, env, []string{"foo=bar", "key=value"}) +} + +func TestConvertResourcesFull(t *testing.T) { + source := composetypes.Resources{ + Limits: &composetypes.Resource{ + NanoCPUs: "0.003", + MemoryBytes: composetypes.UnitBytes(300000000), + }, + Reservations: &composetypes.Resource{ + NanoCPUs: "0.002", + MemoryBytes: composetypes.UnitBytes(200000000), + }, + } + resources, err := convertResources(source) + assert.NilError(t, err) + + expected := &swarm.ResourceRequirements{ + Limits: &swarm.Resources{ + NanoCPUs: 3000000, + MemoryBytes: 300000000, + }, + Reservations: &swarm.Resources{ + NanoCPUs: 2000000, + MemoryBytes: 200000000, + }, + } + assert.DeepEqual(t, resources, expected) +} + +func TestConvertHealthcheck(t *testing.T) { + retries := uint64(10) + source := &composetypes.HealthCheckConfig{ + Test: []string{"EXEC", "touch", "/foo"}, + Timeout: "30s", + Interval: "2ms", + Retries: &retries, + } + expected := &container.HealthConfig{ + Test: source.Test, + Timeout: 30 * time.Second, + Interval: 2 * time.Millisecond, + Retries: 10, + } + + healthcheck, err := convertHealthcheck(source) + assert.NilError(t, err) + assert.DeepEqual(t, healthcheck, expected) +} + +func TestConvertHealthcheckDisable(t *testing.T) { + source := &composetypes.HealthCheckConfig{Disable: true} + expected := &container.HealthConfig{ + Test: []string{"NONE"}, + } + + healthcheck, err := convertHealthcheck(source) + assert.NilError(t, err) + assert.DeepEqual(t, healthcheck, expected) +} + +func TestConvertHealthcheckDisableWithTest(t *testing.T) { + source := &composetypes.HealthCheckConfig{ + Disable: true, + Test: []string{"EXEC", "touch"}, + } + _, err := convertHealthcheck(source) + assert.Error(t, err, "test and disable can't be set") +} + +func TestConvertServiceNetworksOnlyDefault(t *testing.T) { + networkConfigs := networkMap{} + networks := map[string]*composetypes.ServiceNetworkConfig{} + + configs, err := convertServiceNetworks( + networks, networkConfigs, NewNamespace("foo"), "service") + + expected := []swarm.NetworkAttachmentConfig{ + { + Target: "foo_default", + Aliases: []string{"service"}, + }, + } + + assert.NilError(t, err) + assert.DeepEqual(t, configs, expected) +} + +func TestConvertServiceNetworks(t *testing.T) { + networkConfigs := networkMap{ + "front": composetypes.NetworkConfig{ + External: composetypes.External{ + External: true, + Name: "fronttier", + }, + }, + "back": composetypes.NetworkConfig{}, + } + networks := map[string]*composetypes.ServiceNetworkConfig{ + "front": { + Aliases: []string{"something"}, + }, + "back": { + Aliases: []string{"other"}, + }, + } + + configs, err := convertServiceNetworks( + networks, networkConfigs, NewNamespace("foo"), "service") + + expected := []swarm.NetworkAttachmentConfig{ + { + Target: "foo_back", + Aliases: []string{"other", "service"}, + }, + { + Target: "fronttier", + Aliases: []string{"something", "service"}, + }, + } + + sortedConfigs := byTargetSort(configs) + sort.Sort(&sortedConfigs) + + assert.NilError(t, err) + assert.DeepEqual(t, []swarm.NetworkAttachmentConfig(sortedConfigs), expected) +} + +type byTargetSort []swarm.NetworkAttachmentConfig + +func (s byTargetSort) Len() int { + return len(s) +} + +func (s byTargetSort) Less(i, j int) bool { + return strings.Compare(s[i].Target, s[j].Target) < 0 +} + +func (s byTargetSort) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} diff --git a/components/cli/compose/convert/volume.go b/components/cli/compose/convert/volume.go new file mode 100644 index 0000000000..4eb5788204 --- /dev/null +++ b/components/cli/compose/convert/volume.go @@ -0,0 +1,116 @@ +package convert + +import ( + "fmt" + "strings" + + composetypes "github.com/aanand/compose-file/types" + "github.com/docker/docker/api/types/mount" +) + +type volumes map[string]composetypes.VolumeConfig + +// Volumes from compose-file types to engine api types +func Volumes(serviceVolumes []string, stackVolumes volumes, namespace Namespace) ([]mount.Mount, error) { + var mounts []mount.Mount + + for _, volumeSpec := range serviceVolumes { + mount, err := convertVolumeToMount(volumeSpec, stackVolumes, namespace) + if err != nil { + return nil, err + } + mounts = append(mounts, mount) + } + return mounts, nil +} + +func convertVolumeToMount(volumeSpec string, stackVolumes volumes, namespace Namespace) (mount.Mount, error) { + var source, target string + var mode []string + + // TODO: split Windows path mappings properly + parts := strings.SplitN(volumeSpec, ":", 3) + + switch len(parts) { + case 3: + source = parts[0] + target = parts[1] + mode = strings.Split(parts[2], ",") + case 2: + source = parts[0] + target = parts[1] + case 1: + target = parts[0] + default: + return mount.Mount{}, fmt.Errorf("invald volume: %s", volumeSpec) + } + + // TODO: catch Windows paths here + if strings.HasPrefix(source, "/") { + return mount.Mount{ + Type: mount.TypeBind, + Source: source, + Target: target, + ReadOnly: isReadOnly(mode), + BindOptions: getBindOptions(mode), + }, nil + } + + stackVolume, exists := stackVolumes[source] + if !exists { + return mount.Mount{}, fmt.Errorf("undefined volume: %s", source) + } + + var volumeOptions *mount.VolumeOptions + if stackVolume.External.Name != "" { + source = stackVolume.External.Name + } else { + volumeOptions = &mount.VolumeOptions{ + Labels: AddStackLabel(namespace, stackVolume.Labels), + NoCopy: isNoCopy(mode), + } + + if stackVolume.Driver != "" { + volumeOptions.DriverConfig = &mount.Driver{ + Name: stackVolume.Driver, + Options: stackVolume.DriverOpts, + } + } + source = namespace.Scope(source) + } + return mount.Mount{ + Type: mount.TypeVolume, + Source: source, + Target: target, + ReadOnly: isReadOnly(mode), + VolumeOptions: volumeOptions, + }, nil +} + +func modeHas(mode []string, field string) bool { + for _, item := range mode { + if item == field { + return true + } + } + return false +} + +func isReadOnly(mode []string) bool { + return modeHas(mode, "ro") +} + +func isNoCopy(mode []string) bool { + return modeHas(mode, "nocopy") +} + +func getBindOptions(mode []string) *mount.BindOptions { + for _, item := range mode { + for _, propagation := range mount.Propagations { + if mount.Propagation(item) == propagation { + return &mount.BindOptions{Propagation: mount.Propagation(item)} + } + } + } + return nil +} diff --git a/components/cli/compose/convert/volume_test.go b/components/cli/compose/convert/volume_test.go new file mode 100644 index 0000000000..5e9c042b5f --- /dev/null +++ b/components/cli/compose/convert/volume_test.go @@ -0,0 +1,112 @@ +package convert + +import ( + "testing" + + composetypes "github.com/aanand/compose-file/types" + "github.com/docker/docker/api/types/mount" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestIsReadOnly(t *testing.T) { + assert.Equal(t, isReadOnly([]string{"foo", "bar", "ro"}), true) + assert.Equal(t, isReadOnly([]string{"ro"}), true) + assert.Equal(t, isReadOnly([]string{}), false) + assert.Equal(t, isReadOnly([]string{"foo", "rw"}), false) + assert.Equal(t, isReadOnly([]string{"foo"}), false) +} + +func TestIsNoCopy(t *testing.T) { + assert.Equal(t, isNoCopy([]string{"foo", "bar", "nocopy"}), true) + assert.Equal(t, isNoCopy([]string{"nocopy"}), true) + assert.Equal(t, isNoCopy([]string{}), false) + assert.Equal(t, isNoCopy([]string{"foo", "rw"}), false) +} + +func TestGetBindOptions(t *testing.T) { + opts := getBindOptions([]string{"slave"}) + expected := mount.BindOptions{Propagation: mount.PropagationSlave} + assert.Equal(t, *opts, expected) +} + +func TestGetBindOptionsNone(t *testing.T) { + opts := getBindOptions([]string{"ro"}) + assert.Equal(t, opts, (*mount.BindOptions)(nil)) +} + +func TestConvertVolumeToMountNamedVolume(t *testing.T) { + stackVolumes := volumes{ + "normal": composetypes.VolumeConfig{ + Driver: "glusterfs", + DriverOpts: map[string]string{ + "opt": "value", + }, + Labels: map[string]string{ + "something": "labeled", + }, + }, + } + namespace := NewNamespace("foo") + expected := mount.Mount{ + Type: mount.TypeVolume, + Source: "foo_normal", + Target: "/foo", + ReadOnly: true, + VolumeOptions: &mount.VolumeOptions{ + Labels: map[string]string{ + LabelNamespace: "foo", + "something": "labeled", + }, + DriverConfig: &mount.Driver{ + Name: "glusterfs", + Options: map[string]string{ + "opt": "value", + }, + }, + }, + } + mount, err := convertVolumeToMount("normal:/foo:ro", stackVolumes, namespace) + assert.NilError(t, err) + assert.DeepEqual(t, mount, expected) +} + +func TestConvertVolumeToMountNamedVolumeExternal(t *testing.T) { + stackVolumes := volumes{ + "outside": composetypes.VolumeConfig{ + External: composetypes.External{ + External: true, + Name: "special", + }, + }, + } + namespace := NewNamespace("foo") + expected := mount.Mount{ + Type: mount.TypeVolume, + Source: "special", + Target: "/foo", + } + mount, err := convertVolumeToMount("outside:/foo", stackVolumes, namespace) + assert.NilError(t, err) + assert.DeepEqual(t, mount, expected) +} + +func TestConvertVolumeToMountBind(t *testing.T) { + stackVolumes := volumes{} + namespace := NewNamespace("foo") + expected := mount.Mount{ + Type: mount.TypeBind, + Source: "/bar", + Target: "/foo", + ReadOnly: true, + BindOptions: &mount.BindOptions{Propagation: mount.PropagationShared}, + } + mount, err := convertVolumeToMount("/bar:/foo:ro,shared", stackVolumes, namespace) + assert.NilError(t, err) + assert.DeepEqual(t, mount, expected) +} + +func TestConvertVolumeToMountVolumeDoesNotExist(t *testing.T) { + namespace := NewNamespace("foo") + _, err := convertVolumeToMount("unknown:/foo:ro", volumes{}, namespace) + assert.Error(t, err, "undefined volume: unknown") +} From 7e0fb1585da92dc99109417937d8b9917b425ad6 Mon Sep 17 00:00:00 2001 From: Ying Li Date: Thu, 15 Dec 2016 18:36:37 -0800 Subject: [PATCH 470/978] Before asking a user for the unlock key when they run `docker swarm unlock`, actually check to see if the node is part of a swarm, and if so, if it is unlocked first. If neither of these are true, abort the command. Signed-off-by: Ying Li Upstream-commit: e4102ce61e8727296c0acc75531088064173de8c Component: cli --- components/cli/command/swarm/unlock.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/components/cli/command/swarm/unlock.go b/components/cli/command/swarm/unlock.go index 048fb56e3d..abb9e89fe7 100644 --- a/components/cli/command/swarm/unlock.go +++ b/components/cli/command/swarm/unlock.go @@ -2,6 +2,7 @@ package swarm import ( "bufio" + "errors" "fmt" "io" "strings" @@ -24,6 +25,22 @@ func newUnlockCommand(dockerCli *command.DockerCli) *cobra.Command { client := dockerCli.Client() ctx := context.Background() + // First see if the node is actually part of a swarm, and if it's is actually locked first. + // If it's in any other state than locked, don't ask for the key. + info, err := client.Info(ctx) + if err != nil { + return err + } + + switch info.Swarm.LocalNodeState { + case swarm.LocalNodeStateInactive: + return errors.New("Error: This node is not part of a swarm") + case swarm.LocalNodeStateLocked: + break + default: + return errors.New("Error: swarm is not locked") + } + key, err := readKey(dockerCli.In(), "Please enter unlock key: ") if err != nil { return err From 12d8535d84d18b8d8c57276bf3c5d050f23025df Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Fri, 16 Dec 2016 12:17:39 -0800 Subject: [PATCH 471/978] Improve error output for `docker stats ...` While looking into `docker stats ...` I noticed that the error output is quite long, especially if there are multiple errors: ```sh ubuntu@ubuntu:~/docker$ docker stats nofound : Error response from daemon: No such container: nofound ubuntu@ubuntu:~/docker$ docker stats nofound foo bar : Error response from daemon: No such container: nofound, : Error response from daemon: No such container: foo, : Error response from daemon: No such container: bar ``` There are several issues, 1. There is an extra `: ` at the beginning. That is because if container is not found, the name will not be available from the daemon. 2. Multiple errors are concatenated with `, ` which will be quite long. This fix: 1. Only prient out the error from daemon. 2. Multiple errors are printed out line by line. Below is the new output: ```sh ubuntu@ubuntu:~/docker$ docker stats nofound Error response from daemon: No such container: nofound ubuntu@ubuntu:~/docker$ docker stats nofound foo bar Error response from daemon: No such container: nofound Error response from daemon: No such container: foo Error response from daemon: No such container: bar ``` Signed-off-by: Yong Tang Upstream-commit: c6b3fcbe3290c79689a2c941273982d79b8fc5a1 Component: cli --- components/cli/command/container/stats.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/components/cli/command/container/stats.go b/components/cli/command/container/stats.go index 12d5c68522..ebbd36e7e4 100644 --- a/components/cli/command/container/stats.go +++ b/components/cli/command/container/stats.go @@ -173,14 +173,13 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error { var errs []string cStats.mu.Lock() for _, c := range cStats.cs { - cErr := c.GetError() - if cErr != nil { - errs = append(errs, fmt.Sprintf("%s: %v", c.Name, cErr)) + if err := c.GetError(); err != nil { + errs = append(errs, err.Error()) } } cStats.mu.Unlock() if len(errs) > 0 { - return fmt.Errorf("%s", strings.Join(errs, ", ")) + return fmt.Errorf("%s", strings.Join(errs, "\n")) } } From 58f845701ac743d027dfb78f7b667e04ad358ef5 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sun, 18 Dec 2016 16:50:32 +0100 Subject: [PATCH 472/978] fix conversion of anonymous volumes in compose-file the `convertVolumeToMount()` function did not take anonymous volumes into account when converting volume specifications to bind-mounts. this resulted in the conversion to try to look up an empty "source" volume, which lead to an error; undefined volume: this patch distinguishes "anonymous" volumes from bind-mounts and named-volumes, and skips further processing if no source is defined (i.e. the volume is "anonymous"). Signed-off-by: Sebastiaan van Stijn Upstream-commit: bc4590fd7d5cc7745e66def87895b2776ef4876e Component: cli --- components/cli/compose/convert/volume.go | 10 +++++++++- components/cli/compose/convert/volume_test.go | 12 ++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/components/cli/compose/convert/volume.go b/components/cli/compose/convert/volume.go index 4eb5788204..027774bcec 100644 --- a/components/cli/compose/convert/volume.go +++ b/components/cli/compose/convert/volume.go @@ -42,7 +42,15 @@ func convertVolumeToMount(volumeSpec string, stackVolumes volumes, namespace Nam case 1: target = parts[0] default: - return mount.Mount{}, fmt.Errorf("invald volume: %s", volumeSpec) + return mount.Mount{}, fmt.Errorf("invalid volume: %s", volumeSpec) + } + + if source == "" { + // Anonymous volume + return mount.Mount{ + Type: mount.TypeVolume, + Target: target, + }, nil } // TODO: catch Windows paths here diff --git a/components/cli/compose/convert/volume_test.go b/components/cli/compose/convert/volume_test.go index 5e9c042b5f..3ca6ab4a52 100644 --- a/components/cli/compose/convert/volume_test.go +++ b/components/cli/compose/convert/volume_test.go @@ -34,6 +34,18 @@ func TestGetBindOptionsNone(t *testing.T) { assert.Equal(t, opts, (*mount.BindOptions)(nil)) } +func TestConvertVolumeToMountAnonymousVolume(t *testing.T) { + stackVolumes := volumes{} + namespace := NewNamespace("foo") + expected := mount.Mount{ + Type: mount.TypeVolume, + Target: "/foo/bar", + } + mount, err := convertVolumeToMount("/foo/bar", stackVolumes, namespace) + assert.NilError(t, err) + assert.DeepEqual(t, mount, expected) +} + func TestConvertVolumeToMountNamedVolume(t *testing.T) { stackVolumes := volumes{ "normal": composetypes.VolumeConfig{ From 84caa8607177e3cf89442b72e2dd72f7c87c2c72 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 19 Dec 2016 01:50:08 +0100 Subject: [PATCH 473/978] Improve validation for volume specs The current validation only checked for the number of elements in the volume-spec, however, did not validate if the elements were empty. Because of this, an empty volume-spec (""), or volume spec only containing separators ("::") would not be invalidated. This adds a simple check for empty elements in the volume-spec, and returns an error if the spec is invalid. A unit-test is also added to verify the behavior. Signed-off-by: Sebastiaan van Stijn Upstream-commit: 1f57f0707041595f1eeec20ee5836d6873d6faae Component: cli --- components/cli/compose/convert/volume.go | 8 ++++++-- components/cli/compose/convert/volume_test.go | 9 +++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/components/cli/compose/convert/volume.go b/components/cli/compose/convert/volume.go index 027774bcec..3a7504106a 100644 --- a/components/cli/compose/convert/volume.go +++ b/components/cli/compose/convert/volume.go @@ -31,6 +31,12 @@ func convertVolumeToMount(volumeSpec string, stackVolumes volumes, namespace Nam // TODO: split Windows path mappings properly parts := strings.SplitN(volumeSpec, ":", 3) + for _, part := range parts { + if strings.TrimSpace(part) == "" { + return mount.Mount{}, fmt.Errorf("invalid volume: %s", volumeSpec) + } + } + switch len(parts) { case 3: source = parts[0] @@ -41,8 +47,6 @@ func convertVolumeToMount(volumeSpec string, stackVolumes volumes, namespace Nam target = parts[1] case 1: target = parts[0] - default: - return mount.Mount{}, fmt.Errorf("invalid volume: %s", volumeSpec) } if source == "" { diff --git a/components/cli/compose/convert/volume_test.go b/components/cli/compose/convert/volume_test.go index 3ca6ab4a52..bcbfb08b95 100644 --- a/components/cli/compose/convert/volume_test.go +++ b/components/cli/compose/convert/volume_test.go @@ -46,6 +46,15 @@ func TestConvertVolumeToMountAnonymousVolume(t *testing.T) { assert.DeepEqual(t, mount, expected) } +func TestConvertVolumeToMountInvalidFormat(t *testing.T) { + namespace := NewNamespace("foo") + invalids := []string{"::", "::cc", ":bb:", "aa::", "aa::cc", "aa:bb:", " : : ", " : :cc", " :bb: ", "aa: : ", "aa: :cc", "aa:bb: "} + for _, vol := range invalids { + _, err := convertVolumeToMount(vol, volumes{}, namespace) + assert.Error(t, err, "invalid volume: "+vol) + } +} + func TestConvertVolumeToMountNamedVolume(t *testing.T) { stackVolumes := volumes{ "normal": composetypes.VolumeConfig{ From 2858451cf07fc972c65185f93354340139389be7 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 22 Nov 2016 14:51:22 +0100 Subject: [PATCH 474/978] remove client-side for supported logging drivers The `docker logs` command performed a client-side check if the container's logging driver was supported. Now that we allow the client to connect to both "older" and "newer" daemon versions, this check is best done daemon-side. This patch remove the check on the client side, and leaves validation to the daemon, which should be the source of truth. Signed-off-by: Sebastiaan van Stijn Upstream-commit: 8246c4949806912961bd3b18a7d9a83ac7959175 Component: cli --- components/cli/command/container/logs.go | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/components/cli/command/container/logs.go b/components/cli/command/container/logs.go index 3a37cedf43..9f1d9f90dd 100644 --- a/components/cli/command/container/logs.go +++ b/components/cli/command/container/logs.go @@ -1,7 +1,6 @@ package container import ( - "fmt" "io" "golang.org/x/net/context" @@ -13,11 +12,6 @@ import ( "github.com/spf13/cobra" ) -var validDrivers = map[string]bool{ - "json-file": true, - "journald": true, -} - type logsOptions struct { follow bool since string @@ -54,15 +48,6 @@ func NewLogsCommand(dockerCli *command.DockerCli) *cobra.Command { func runLogs(dockerCli *command.DockerCli, opts *logsOptions) error { ctx := context.Background() - c, err := dockerCli.Client().ContainerInspect(ctx, opts.container) - if err != nil { - return err - } - - if !validDrivers[c.HostConfig.LogConfig.Type] { - return fmt.Errorf("\"logs\" command is supported only for \"json-file\" and \"journald\" logging drivers (got: %s)", c.HostConfig.LogConfig.Type) - } - options := types.ContainerLogsOptions{ ShowStdout: true, ShowStderr: true, @@ -78,6 +63,11 @@ func runLogs(dockerCli *command.DockerCli, opts *logsOptions) error { } defer responseBody.Close() + c, err := dockerCli.Client().ContainerInspect(ctx, opts.container) + if err != nil { + return err + } + if c.Config.Tty { _, err = io.Copy(dockerCli.Out(), responseBody) } else { From ed51ff0e2bcbece12e900c2a131698fe2c2d5280 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Fri, 16 Dec 2016 11:19:05 -0800 Subject: [PATCH 475/978] Abstract distribution interfaces from image specific types Move configurations into a single file. Abstract download manager in pull config. Add supports for schema2 only and schema2 type checking. Add interface for providing push layers. Abstract image store to generically handle configurations. Signed-off-by: Derek McGowan Upstream-commit: 476adcfd20c0dabfc85c6cbc3abc665d7e80427d Component: cli --- components/cli/command/image/pull.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/image/pull.go b/components/cli/command/image/pull.go index 13de492f92..24933fe846 100644 --- a/components/cli/command/image/pull.go +++ b/components/cli/command/image/pull.go @@ -74,7 +74,7 @@ func runPull(dockerCli *command.DockerCli, opts pullOptions) error { err = imagePullPrivileged(ctx, dockerCli, authConfig, distributionRef.String(), requestPrivilege, opts.all) } if err != nil { - if strings.Contains(err.Error(), "target is a plugin") { + if strings.Contains(err.Error(), "target is plugin") { return errors.New(err.Error() + " - Use `docker plugin install`") } return err From 375b7aafd2634854c59b21b12dbd75a11c473dad Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Thu, 15 Dec 2016 06:12:33 -0800 Subject: [PATCH 476/978] Fix misleading default for `--replicas` This fix tries to address the issue raised in 29291 where the output of `--replicas` in `service create/update`: ``` --replicas uint Number of tasks (default none) ``` is misleading. User might incorrectly assume the number of replicas would be `0` (`none`) by default, while the actual default is `1`. The issue comes from the fact that some of the default values are from daemon and it is not possible for client to find out the default value. In this case, it might be better to just simply not displaying `(default none)`. This fix returns "" for `Uint64Opt` so that `(default none)` is hidden. In addition to `--replicas`, this fix also changes `--restart-delay`, `--restart-max-attempts`, `--stop-grace-period`, `--health-interval`, `--health-timeout`, and `--restart-window` in a similiar fashion. New Output: ``` --health-interval duration Time between running the check (ns|us|ms|s|m|h) --health-timeout duration Maximum time to allow one check to run (ns|us|ms|s|m|h) ... --replicas uint Number of tasks ... --restart-delay duration Delay between restart attempts (ns|us|ms|s|m|h) --restart-max-attempts uint Maximum number of restarts before giving up --restart-window duration Window used to evaluate the restart policy (ns|us|ms|s|m|h) ... --stop-grace-period duration Time to wait before force killing a container (ns|us|ms|s|m|h) ``` The docs has been updated. Note the docs for help output of `service create/update` is out of sync with the current master. This fix replace with the update-to-date help output. This fix fixes 29291. Signed-off-by: Yong Tang Upstream-commit: 23ab849f06f07c7efae6dd8c169070a81b9e40f0 Component: cli --- components/cli/command/service/opts.go | 4 ++-- components/cli/command/service/opts_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index c7518e5976..cbe544aacc 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -84,7 +84,7 @@ func (d *DurationOpt) String() string { if d.value != nil { return d.value.String() } - return "none" + return "" } // Value returns the time.Duration @@ -114,7 +114,7 @@ func (i *Uint64Opt) String() string { if i.value != nil { return fmt.Sprintf("%v", *i.value) } - return "none" + return "" } // Value returns the uint64 diff --git a/components/cli/command/service/opts_test.go b/components/cli/command/service/opts_test.go index aa2d999dcf..78b956ad67 100644 --- a/components/cli/command/service/opts_test.go +++ b/components/cli/command/service/opts_test.go @@ -59,7 +59,7 @@ func TestUint64OptString(t *testing.T) { assert.Equal(t, opt.String(), "2345678") opt = Uint64Opt{} - assert.Equal(t, opt.String(), "none") + assert.Equal(t, opt.String(), "") } func TestUint64OptSetAndValue(t *testing.T) { From d49dbca38101acef706dbecf8fb7428fed0b84a8 Mon Sep 17 00:00:00 2001 From: allencloud Date: Mon, 19 Dec 2016 14:45:48 +0800 Subject: [PATCH 477/978] change minor mistake of spelling Signed-off-by: allencloud Upstream-commit: 3c8d009c7a8504ab552606cf2158e56fbaf7b27a Component: cli --- components/cli/command/registry/logout.go | 2 +- components/cli/command/system/info.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/command/registry/logout.go b/components/cli/command/registry/logout.go index 877e60e8cc..f1f397fa08 100644 --- a/components/cli/command/registry/logout.go +++ b/components/cli/command/registry/logout.go @@ -11,7 +11,7 @@ import ( "github.com/spf13/cobra" ) -// NewLogoutCommand creates a new `docker login` command +// NewLogoutCommand creates a new `docker logout` command func NewLogoutCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := &cobra.Command{ Use: "logout [SERVER]", diff --git a/components/cli/command/system/info.go b/components/cli/command/system/info.go index e0b8767377..973ee18241 100644 --- a/components/cli/command/system/info.go +++ b/components/cli/command/system/info.go @@ -277,7 +277,7 @@ func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error { for _, label := range info.Labels { stringSlice := strings.SplitN(label, "=", 2) if len(stringSlice) > 1 { - // If there is a conflict we will throw out an warning + // If there is a conflict we will throw out a warning if v, ok := labelMap[stringSlice[0]]; ok && v != stringSlice[1] { fmt.Fprintln(dockerCli.Err(), "WARNING: labels with duplicate keys and conflicting values have been deprecated") break From 5821bc8a550c4a59830f6d5c8a99a1cc8e415070 Mon Sep 17 00:00:00 2001 From: allencloud Date: Mon, 19 Dec 2016 14:45:48 +0800 Subject: [PATCH 478/978] change minor mistake of spelling Signed-off-by: allencloud Upstream-commit: 693328f346e094b972963736b2890afae5dc3a47 Component: cli --- components/cli/errors.go | 2 +- components/cli/node_inspect_test.go | 2 +- components/cli/ping.go | 2 +- components/cli/secret_inspect_test.go | 2 +- components/cli/service_inspect_test.go | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/components/cli/errors.go b/components/cli/errors.go index bf6923f134..2912692ec1 100644 --- a/components/cli/errors.go +++ b/components/cli/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/components/cli/node_inspect_test.go b/components/cli/node_inspect_test.go index fc13283084..dca16a8cdc 100644 --- a/components/cli/node_inspect_test.go +++ b/components/cli/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/components/cli/ping.go b/components/cli/ping.go index 22dcda24fd..150b1dc8d8 100644 --- a/components/cli/ping.go +++ b/components/cli/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/components/cli/secret_inspect_test.go b/components/cli/secret_inspect_test.go index 423d986968..0142a3ca9f 100644 --- a/components/cli/secret_inspect_test.go +++ b/components/cli/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/components/cli/service_inspect_test.go b/components/cli/service_inspect_test.go index e235cf0fef..0346847317 100644 --- a/components/cli/service_inspect_test.go +++ b/components/cli/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 dec54205a54d69cc5fbf315f98d0ace77df6e854 Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Tue, 20 Dec 2016 19:14:41 +0800 Subject: [PATCH 479/978] Change tls to TLS Signed-off-by: yuexiao-wang Upstream-commit: 1e7c22c80a75b735959c75e86e62bacb9e441a6e Component: cli --- components/cli/flags/common.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/flags/common.go b/components/cli/flags/common.go index 690e8da4b8..490c2922fc 100644 --- a/components/cli/flags/common.go +++ b/components/cli/flags/common.go @@ -21,7 +21,7 @@ const ( DefaultKeyFile = "key.pem" // DefaultCertFile is the default filename for the cert pem file DefaultCertFile = "cert.pem" - // FlagTLSVerify is the flag name for the tls verification option + // FlagTLSVerify is the flag name for the TLS verification option FlagTLSVerify = "tlsverify" ) @@ -73,7 +73,7 @@ func (commonOpts *CommonOptions) InstallFlags(flags *pflag.FlagSet) { // complete func (commonOpts *CommonOptions) SetDefaultOptions(flags *pflag.FlagSet) { // Regardless of whether the user sets it to true or false, if they - // specify --tlsverify at all then we need to turn on tls + // specify --tlsverify at all then we need to turn on TLS // TLSVerify can be true even if not set due to DOCKER_TLS_VERIFY env var, so we need // to check that here as well if flags.Changed(FlagTLSVerify) || commonOpts.TLSVerify { From 7d6defc8cf5a7592351e0027109c589ee57da1fa Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Tue, 20 Dec 2016 19:14:41 +0800 Subject: [PATCH 480/978] Change tls to TLS Signed-off-by: yuexiao-wang Upstream-commit: d044b55ee0551f20a12507713a1102385beb387d Component: cli --- components/cli/client.go | 2 +- components/cli/client_test.go | 4 ++-- components/cli/swarm_init.go | 2 +- components/cli/swarm_inspect.go | 2 +- components/cli/swarm_join.go | 2 +- components/cli/swarm_leave.go | 2 +- components/cli/swarm_update.go | 2 +- components/cli/transport.go | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/components/cli/client.go b/components/cli/client.go index 4c0f097e53..75cfc8698b 100644 --- a/components/cli/client.go +++ b/components/cli/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/components/cli/client_test.go b/components/cli/client_test.go index 3a6575c9cc..7c26403ebe 100644 --- a/components/cli/client_test.go +++ b/components/cli/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/components/cli/swarm_init.go b/components/cli/swarm_init.go index fd45d066e3..9e65e1cca4 100644 --- a/components/cli/swarm_init.go +++ b/components/cli/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/components/cli/swarm_inspect.go b/components/cli/swarm_inspect.go index 6d95cfc05e..77e72f8466 100644 --- a/components/cli/swarm_inspect.go +++ b/components/cli/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/components/cli/swarm_join.go b/components/cli/swarm_join.go index cda99930eb..19e5192b9e 100644 --- a/components/cli/swarm_join.go +++ b/components/cli/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/components/cli/swarm_leave.go b/components/cli/swarm_leave.go index a4df732174..3a205cf3b5 100644 --- a/components/cli/swarm_leave.go +++ b/components/cli/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/components/cli/swarm_update.go b/components/cli/swarm_update.go index cc8eeb6554..7245fd4e38 100644 --- a/components/cli/swarm_update.go +++ b/components/cli/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/components/cli/transport.go b/components/cli/transport.go index f04e601649..02ebadeac6 100644 --- a/components/cli/transport.go +++ b/components/cli/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 d11aa2f821072b0aa528f4d7534fd3544e0eab9e Mon Sep 17 00:00:00 2001 From: Misty Stanley-Jones Date: Tue, 20 Dec 2016 11:47:54 -0800 Subject: [PATCH 481/978] Clarify what docker diff shows Signed-off-by: Misty Stanley-Jones Upstream-commit: 606a16a07d6d3fec886253e24a17ed1e7de366c4 Component: cli --- components/cli/command/container/diff.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/container/diff.go b/components/cli/command/container/diff.go index 12d6591014..168af74172 100644 --- a/components/cli/command/container/diff.go +++ b/components/cli/command/container/diff.go @@ -21,7 +21,7 @@ func NewDiffCommand(dockerCli *command.DockerCli) *cobra.Command { return &cobra.Command{ Use: "diff CONTAINER", - Short: "Inspect changes on a container's filesystem", + Short: "Inspect changes to files or directories on a container's filesystem", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.container = args[0] From 8d700ff527fa7e76afb224b1d34d48a410bfc0da Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 7 Dec 2016 07:38:18 -0800 Subject: [PATCH 482/978] Fix `docker plugin inspect ` issue on Windows This fix is a follow up for comment: https://github.com/docker/docker/pull/29186/files#r91277345 While #29186 addresses the issue of `docker inspect ` on Windows, it actually makes `docker plugin inspect ` out `object not found` on Windows as well. This is actually misleading as plugin is not supported on Windows. This fix reverted the change in #29186 while at the same time, checks `not supported` in `docker inspect ` so that - `docker plugin inspect ` returns `not supported` on Windows - `docker inspect ` returns `not found` on Windows This fix is related to #29186 and #29185. Signed-off-by: Yong Tang Upstream-commit: 86a07d3fec4f8e39c50d97e3b89d05c5dcdb0a95 Component: cli --- components/cli/command/system/inspect.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/components/cli/command/system/inspect.go b/components/cli/command/system/inspect.go index cb5a1213af..c86e858a29 100644 --- a/components/cli/command/system/inspect.go +++ b/components/cli/command/system/inspect.go @@ -2,6 +2,7 @@ package system import ( "fmt" + "strings" "golang.org/x/net/context" @@ -156,6 +157,10 @@ func inspectAll(ctx context.Context, dockerCli *command.DockerCli, getSize bool, return info.Swarm.ControlAvailable } + isErrNotSupported := func(err error) bool { + return strings.Contains(err.Error(), "not supported") + } + return func(ref string) (interface{}, []byte, error) { const ( swarmSupportUnknown = iota @@ -183,7 +188,7 @@ func inspectAll(ctx context.Context, dockerCli *command.DockerCli, getSize bool, } v, raw, err := inspectData.objectInspector(ref) if err != nil { - if typeConstraint == "" && apiclient.IsErrNotFound(err) { + if typeConstraint == "" && (apiclient.IsErrNotFound(err) || isErrNotSupported(err)) { continue } return v, raw, err From 76e4b27bec8537d10d65e25b7c611751674d3877 Mon Sep 17 00:00:00 2001 From: WANG Yuexiao Date: Wed, 21 Dec 2016 19:41:14 +0800 Subject: [PATCH 483/978] Remove unused var 'errTLSConfigUnavailable' (#29626) Signed-off-by: yuexiao-wang Upstream-commit: fe5937d0a7b083986d07228dbc4f23d9050ec81b Component: cli --- components/cli/transport.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/components/cli/transport.go b/components/cli/transport.go index f04e601649..6cd47f2efd 100644 --- a/components/cli/transport.go +++ b/components/cli/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 46aec23f810916c6bd82f6b560a5c985b7ec5a1f Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Fri, 29 Jul 2016 08:20:03 -0700 Subject: [PATCH 484/978] Support multiple service IDs on "docker service ps" This fix tries to address issue raised in 25228 to support multiple service IDs on `docker service ps`. Multiple IDs are allowed with `docker service ps ...`, and related documentation has been updated. A test has been added to cover the changes. This fix fixes 25228. Signed-off-by: Yong Tang Upstream-commit: d04375bd4aa85794ef65737525da76ab84b8c8de Component: cli --- components/cli/command/service/ps.go | 56 +++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/components/cli/command/service/ps.go b/components/cli/command/service/ps.go index cf94ad7374..12b25bf4f6 100644 --- a/components/cli/command/service/ps.go +++ b/components/cli/command/service/ps.go @@ -1,7 +1,13 @@ package service import ( + "fmt" + "strings" + + "golang.org/x/net/context" + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/idresolver" @@ -9,11 +15,10 @@ import ( "github.com/docker/docker/cli/command/task" "github.com/docker/docker/opts" "github.com/spf13/cobra" - "golang.org/x/net/context" ) type psOptions struct { - serviceID string + services []string quiet bool noResolve bool noTrunc bool @@ -24,11 +29,11 @@ func newPsCommand(dockerCli *command.DockerCli) *cobra.Command { opts := psOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ - Use: "ps [OPTIONS] SERVICE", - Short: "List the tasks of a service", - Args: cli.ExactArgs(1), + Use: "ps [OPTIONS] SERVICE [SERVICE...]", + Short: "List the tasks of one or more services", + Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - opts.serviceID = args[0] + opts.services = args return runPS(dockerCli, opts) }, } @@ -45,13 +50,46 @@ func runPS(dockerCli *command.DockerCli, opts psOptions) error { client := dockerCli.Client() ctx := context.Background() - service, _, err := client.ServiceInspectWithRaw(ctx, opts.serviceID) + filter := opts.filter.Value() + + serviceIDFilter := filters.NewArgs() + serviceNameFilter := filters.NewArgs() + for _, service := range opts.services { + serviceIDFilter.Add("id", service) + serviceNameFilter.Add("name", service) + } + serviceByIDList, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: serviceIDFilter}) + if err != nil { + return err + } + serviceByNameList, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: serviceNameFilter}) if err != nil { return err } - filter := opts.filter.Value() - filter.Add("service", service.ID) + for _, service := range opts.services { + serviceCount := 0 + // Lookup by ID/Prefix + for _, serviceEntry := range serviceByIDList { + if strings.HasPrefix(serviceEntry.ID, service) { + filter.Add("service", serviceEntry.ID) + serviceCount++ + } + } + + // Lookup by Name/Prefix + for _, serviceEntry := range serviceByNameList { + if strings.HasPrefix(serviceEntry.Spec.Annotations.Name, service) { + filter.Add("service", serviceEntry.ID) + serviceCount++ + } + } + // If nothing has been found, return immediately. + if serviceCount == 0 { + return fmt.Errorf("no such services: %s", service) + } + } + if filter.Include("node") { nodeFilters := filter.Get("node") for _, nodeFilter := range nodeFilters { From dcbf69e68489263b312c514ff07a9cc1a8116e7d Mon Sep 17 00:00:00 2001 From: Anusha Ragunathan Date: Tue, 20 Dec 2016 08:26:58 -0800 Subject: [PATCH 485/978] 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 Upstream-commit: fa7cceeb4acdac250d0d0283f86f4e6b65fcacdb Component: cli --- components/cli/interface.go | 2 +- components/cli/plugin_disable.go | 11 +++++++++-- components/cli/plugin_disable_test.go | 5 +++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/components/cli/interface.go b/components/cli/interface.go index 6319f34f1e..96d65a428a 100644 --- a/components/cli/interface.go +++ b/components/cli/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/components/cli/plugin_disable.go b/components/cli/plugin_disable.go index 51e4565125..30467db742 100644 --- a/components/cli/plugin_disable.go +++ b/components/cli/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/components/cli/plugin_disable_test.go b/components/cli/plugin_disable_test.go index 2818008ab9..a4de45be2d 100644 --- a/components/cli/plugin_disable_test.go +++ b/components/cli/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 52276a061b1d0c0461397ce5ed9d1d6e647c9947 Mon Sep 17 00:00:00 2001 From: Anusha Ragunathan Date: Tue, 20 Dec 2016 08:26:58 -0800 Subject: [PATCH 486/978] 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 Upstream-commit: bf3250ae0ab6e54d998f94386c25e59a32ad87f1 Component: cli --- components/cli/command/plugin/disable.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/components/cli/command/plugin/disable.go b/components/cli/command/plugin/disable.go index 9089a3cf68..5399e61f1b 100644 --- a/components/cli/command/plugin/disable.go +++ b/components/cli/command/plugin/disable.go @@ -3,6 +3,7 @@ package plugin import ( "fmt" + "github.com/docker/docker/api/types" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/reference" @@ -11,19 +12,23 @@ import ( ) func newDisableCommand(dockerCli *command.DockerCli) *cobra.Command { + var force bool + cmd := &cobra.Command{ Use: "disable PLUGIN", Short: "Disable a plugin", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return runDisable(dockerCli, args[0]) + return runDisable(dockerCli, args[0], force) }, } + flags := cmd.Flags() + flags.BoolVarP(&force, "force", "f", false, "Force the disable of an active plugin") return cmd } -func runDisable(dockerCli *command.DockerCli, name string) error { +func runDisable(dockerCli *command.DockerCli, name string, force bool) error { named, err := reference.ParseNamed(name) // FIXME: validate if err != nil { return err @@ -35,7 +40,7 @@ func runDisable(dockerCli *command.DockerCli, name string) error { if !ok { return fmt.Errorf("invalid name: %s", named.String()) } - if err := dockerCli.Client().PluginDisable(context.Background(), ref.String()); err != nil { + if err := dockerCli.Client().PluginDisable(context.Background(), ref.String(), types.PluginDisableOptions{Force: force}); err != nil { return err } fmt.Fprintln(dockerCli.Out(), name) From b5d022c60890324edd3ad8b26d6f65bbc9f654a0 Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Mon, 12 Dec 2016 15:05:53 -0800 Subject: [PATCH 487/978] 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 Upstream-commit: 2825296deb853f09785952796a9e0edb5092089e Component: cli --- components/cli/command/plugin/create.go | 4 +- components/cli/command/plugin/disable.go | 14 +---- components/cli/command/plugin/enable.go | 15 +---- components/cli/command/plugin/install.go | 70 +++++++++++++++++++----- components/cli/command/plugin/list.go | 4 +- components/cli/command/plugin/push.go | 8 ++- components/cli/command/plugin/remove.go | 16 +----- components/cli/command/plugin/set.go | 20 +------ 8 files changed, 70 insertions(+), 81 deletions(-) diff --git a/components/cli/command/plugin/create.go b/components/cli/command/plugin/create.go index e0041c1b88..2aab1e9e4a 100644 --- a/components/cli/command/plugin/create.go +++ b/components/cli/command/plugin/create.go @@ -64,8 +64,8 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { options := pluginCreateOptions{} cmd := &cobra.Command{ - Use: "create [OPTIONS] PLUGIN[:tag] PATH-TO-ROOTFS(rootfs + config.json)", - Short: "Create a plugin from a rootfs and config", + Use: "create [OPTIONS] PLUGIN PLUGIN-DATA-DIR", + Short: "Create a plugin from a rootfs and configuration. Plugin data directory must contain config.json and rootfs directory.", Args: cli.RequiresMinArgs(2), RunE: func(cmd *cobra.Command, args []string) error { options.repoName = args[0] diff --git a/components/cli/command/plugin/disable.go b/components/cli/command/plugin/disable.go index 5399e61f1b..c3d36e20af 100644 --- a/components/cli/command/plugin/disable.go +++ b/components/cli/command/plugin/disable.go @@ -6,7 +6,6 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" - "github.com/docker/docker/reference" "github.com/spf13/cobra" "golang.org/x/net/context" ) @@ -29,18 +28,7 @@ func newDisableCommand(dockerCli *command.DockerCli) *cobra.Command { } func runDisable(dockerCli *command.DockerCli, name string, force bool) error { - named, err := reference.ParseNamed(name) // FIXME: validate - if err != nil { - return err - } - if reference.IsNameOnly(named) { - named = reference.WithDefaultTag(named) - } - ref, ok := named.(reference.NamedTagged) - if !ok { - return fmt.Errorf("invalid name: %s", named.String()) - } - if err := dockerCli.Client().PluginDisable(context.Background(), ref.String(), types.PluginDisableOptions{Force: force}); err != nil { + if err := dockerCli.Client().PluginDisable(context.Background(), name, types.PluginDisableOptions{Force: force}); err != nil { return err } fmt.Fprintln(dockerCli.Out(), name) diff --git a/components/cli/command/plugin/enable.go b/components/cli/command/plugin/enable.go index 9201e38e11..77762f4024 100644 --- a/components/cli/command/plugin/enable.go +++ b/components/cli/command/plugin/enable.go @@ -6,7 +6,6 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" - "github.com/docker/docker/reference" "github.com/spf13/cobra" "golang.org/x/net/context" ) @@ -36,23 +35,11 @@ func newEnableCommand(dockerCli *command.DockerCli) *cobra.Command { func runEnable(dockerCli *command.DockerCli, opts *enableOpts) error { name := opts.name - - named, err := reference.ParseNamed(name) // FIXME: validate - if err != nil { - return err - } - if reference.IsNameOnly(named) { - named = reference.WithDefaultTag(named) - } - ref, ok := named.(reference.NamedTagged) - if !ok { - return fmt.Errorf("invalid name: %s", named.String()) - } if opts.timeout < 0 { return fmt.Errorf("negative timeout %d is invalid", opts.timeout) } - if err := dockerCli.Client().PluginEnable(context.Background(), ref.String(), types.PluginEnableOptions{Timeout: opts.timeout}); err != nil { + if err := dockerCli.Client().PluginEnable(context.Background(), name, types.PluginEnableOptions{Timeout: opts.timeout}); err != nil { return err } fmt.Fprintln(dockerCli.Out(), name) diff --git a/components/cli/command/plugin/install.go b/components/cli/command/plugin/install.go index eae0183671..71bdeeff22 100644 --- a/components/cli/command/plugin/install.go +++ b/components/cli/command/plugin/install.go @@ -2,12 +2,16 @@ package plugin import ( "bufio" + "errors" "fmt" "strings" + distreference "github.com/docker/distribution/reference" "github.com/docker/docker/api/types" + registrytypes "github.com/docker/docker/api/types/registry" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/reference" "github.com/docker/docker/registry" "github.com/spf13/cobra" @@ -16,6 +20,7 @@ import ( type pluginOptions struct { name string + alias string grantPerms bool disable bool args []string @@ -39,41 +44,67 @@ func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() flags.BoolVar(&options.grantPerms, "grant-all-permissions", false, "Grant all permissions necessary to run the plugin") flags.BoolVar(&options.disable, "disable", false, "Do not enable the plugin on install") + flags.StringVar(&options.alias, "alias", "", "Local name for plugin") return cmd } +func getRepoIndexFromUnnormalizedRef(ref distreference.Named) (*registrytypes.IndexInfo, error) { + named, err := reference.ParseNamed(ref.Name()) + if err != nil { + return nil, err + } + + repoInfo, err := registry.ParseRepositoryInfo(named) + if err != nil { + return nil, err + } + + return repoInfo.Index, nil +} + func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error { - named, err := reference.ParseNamed(opts.name) // FIXME: validate + // Parse name using distribution reference package to support name + // containing both tag and digest. Names with both tag and digest + // will be treated by the daemon as a pull by digest with + // an alias for the tag (if no alias is provided). + ref, err := distreference.ParseNamed(opts.name) if err != nil { return err } - if reference.IsNameOnly(named) { - named = reference.WithDefaultTag(named) + + alias := "" + if opts.alias != "" { + aref, err := reference.ParseNamed(opts.alias) + if err != nil { + return err + } + aref = reference.WithDefaultTag(aref) + if _, ok := aref.(reference.NamedTagged); !ok { + return fmt.Errorf("invalid name: %s", opts.alias) + } + alias = aref.String() } - ref, ok := named.(reference.NamedTagged) - if !ok { - return fmt.Errorf("invalid name: %s", named.String()) + + index, err := getRepoIndexFromUnnormalizedRef(ref) + if err != nil { + return err } ctx := context.Background() - repoInfo, err := registry.ParseRepositoryInfo(named) - if err != nil { - return err - } - - authConfig := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index) + authConfig := command.ResolveAuthConfig(ctx, dockerCli, index) encodedAuth, err := command.EncodeAuthToBase64(authConfig) if err != nil { return err } - registryAuthFunc := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, "plugin install") + registryAuthFunc := command.RegistryAuthenticationPrivilegedFunc(dockerCli, index, "plugin install") options := types.PluginInstallOptions{ RegistryAuth: encodedAuth, + RemoteRef: ref.String(), Disabled: opts.disable, AcceptAllPermissions: opts.grantPerms, AcceptPermissionsFunc: acceptPrivileges(dockerCli, opts.name), @@ -81,10 +112,19 @@ func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error { PrivilegeFunc: registryAuthFunc, Args: opts.args, } - if err := dockerCli.Client().PluginInstall(ctx, ref.String(), options); err != nil { + + responseBody, err := dockerCli.Client().PluginInstall(ctx, alias, options) + if err != nil { + if strings.Contains(err.Error(), "target is image") { + return errors.New(err.Error() + " - Use `docker image pull`") + } return err } - fmt.Fprintln(dockerCli.Out(), opts.name) + defer responseBody.Close() + if err := jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil); err != nil { + return err + } + fmt.Fprintf(dockerCli.Out(), "Installed plugin %s\n", opts.name) // todo: return proper values from the API for this result return nil } diff --git a/components/cli/command/plugin/list.go b/components/cli/command/plugin/list.go index 4f800d7ec1..8fd16dae3f 100644 --- a/components/cli/command/plugin/list.go +++ b/components/cli/command/plugin/list.go @@ -44,7 +44,7 @@ func runList(dockerCli *command.DockerCli, opts listOptions) error { } w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0) - fmt.Fprintf(w, "ID \tNAME \tTAG \tDESCRIPTION\tENABLED") + fmt.Fprintf(w, "ID \tNAME \tDESCRIPTION\tENABLED") fmt.Fprintf(w, "\n") for _, p := range plugins { @@ -56,7 +56,7 @@ func runList(dockerCli *command.DockerCli, opts listOptions) error { desc = stringutils.Ellipsis(desc, 45) } - fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%v\n", id, p.Name, p.Tag, desc, p.Enabled) + fmt.Fprintf(w, "%s\t%s\t%s\t%v\n", id, p.Name, desc, p.Enabled) } w.Flush() return nil diff --git a/components/cli/command/plugin/push.go b/components/cli/command/plugin/push.go index add4a2b0a6..667379cdd2 100644 --- a/components/cli/command/plugin/push.go +++ b/components/cli/command/plugin/push.go @@ -7,6 +7,7 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/reference" "github.com/docker/docker/registry" "github.com/spf13/cobra" @@ -49,5 +50,10 @@ func runPush(dockerCli *command.DockerCli, name string) error { if err != nil { return err } - return dockerCli.Client().PluginPush(ctx, ref.String(), encodedAuth) + responseBody, err := dockerCli.Client().PluginPush(ctx, ref.String(), encodedAuth) + if err != nil { + return err + } + defer responseBody.Close() + return jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil) } diff --git a/components/cli/command/plugin/remove.go b/components/cli/command/plugin/remove.go index 7a51dce06d..9f3aba9a01 100644 --- a/components/cli/command/plugin/remove.go +++ b/components/cli/command/plugin/remove.go @@ -6,7 +6,6 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" - "github.com/docker/docker/reference" "github.com/spf13/cobra" "golang.org/x/net/context" ) @@ -41,21 +40,8 @@ func runRemove(dockerCli *command.DockerCli, opts *rmOptions) error { var errs cli.Errors for _, name := range opts.plugins { - named, err := reference.ParseNamed(name) // FIXME: validate - if err != nil { - errs = append(errs, err) - continue - } - if reference.IsNameOnly(named) { - named = reference.WithDefaultTag(named) - } - ref, ok := named.(reference.NamedTagged) - if !ok { - errs = append(errs, fmt.Errorf("invalid name: %s", named.String())) - continue - } // TODO: pass names to api instead of making multiple api calls - if err := dockerCli.Client().PluginRemove(ctx, ref.String(), types.PluginRemoveOptions{Force: opts.force}); err != nil { + if err := dockerCli.Client().PluginRemove(ctx, name, types.PluginRemoveOptions{Force: opts.force}); err != nil { errs = append(errs, err) continue } diff --git a/components/cli/command/plugin/set.go b/components/cli/command/plugin/set.go index 5660523ed9..52b09fb500 100644 --- a/components/cli/command/plugin/set.go +++ b/components/cli/command/plugin/set.go @@ -1,13 +1,10 @@ package plugin import ( - "fmt" - "golang.org/x/net/context" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" - "github.com/docker/docker/reference" "github.com/spf13/cobra" ) @@ -17,24 +14,9 @@ func newSetCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Change settings for a plugin", Args: cli.RequiresMinArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - return runSet(dockerCli, args[0], args[1:]) + return dockerCli.Client().PluginSet(context.Background(), args[0], args[1:]) }, } return cmd } - -func runSet(dockerCli *command.DockerCli, name string, args []string) error { - named, err := reference.ParseNamed(name) // FIXME: validate - if err != nil { - return err - } - if reference.IsNameOnly(named) { - named = reference.WithDefaultTag(named) - } - ref, ok := named.(reference.NamedTagged) - if !ok { - return fmt.Errorf("invalid name: %s", named.String()) - } - return dockerCli.Client().PluginSet(context.Background(), ref.String(), args) -} From b70a86e634dd80b21b0c009772a49eca4e39feda Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Mon, 12 Dec 2016 15:05:53 -0800 Subject: [PATCH 488/978] 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 Upstream-commit: 66f7194250ee9cfd0258b42632954ebfcd5c394b Component: cli --- components/cli/interface.go | 4 +- components/cli/plugin_install.go | 73 ++++++++++++++++++++---------- components/cli/plugin_push.go | 10 ++-- components/cli/plugin_push_test.go | 4 +- 4 files changed, 59 insertions(+), 32 deletions(-) diff --git a/components/cli/interface.go b/components/cli/interface.go index 96d65a428a..00b9adea32 100644 --- a/components/cli/interface.go +++ b/components/cli/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/components/cli/plugin_install.go b/components/cli/plugin_install.go index e7b67f2051..b305780cfb 100644 --- a/components/cli/plugin_install.go +++ b/components/cli/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/components/cli/plugin_push.go b/components/cli/plugin_push.go index d83bbdc358..1e5f963251 100644 --- a/components/cli/plugin_push.go +++ b/components/cli/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/components/cli/plugin_push_test.go b/components/cli/plugin_push_test.go index 7b8eb865d6..d9f70cdff8 100644 --- a/components/cli/plugin_push_test.go +++ b/components/cli/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 f912773c28c05500e9f9ee13cd99b58ea2f6d5b1 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 23 Dec 2016 20:09:12 +0100 Subject: [PATCH 489/978] =?UTF-8?q?Clean=20some=20stuff=20from=20runconfig?= =?UTF-8?q?=20that=20are=20cli=20only=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … or could be in `opts` package. Having `runconfig/opts` and `opts` doesn't really make sense and make it difficult to know where to put some code. Signed-off-by: Vincent Demeester Upstream-commit: 672687938244babd42941ea0630f33950566519f Component: cli --- components/cli/command/container/create.go | 9 +- components/cli/command/container/exec.go | 3 +- components/cli/command/container/opts.go | 899 ++++++++++++++++++ components/cli/command/container/opts_test.go | 857 +++++++++++++++++ components/cli/command/container/run.go | 11 +- .../cli/command/container/testdata/utf16.env | Bin 0 -> 54 bytes .../command/container/testdata/utf16be.env | Bin 0 -> 54 bytes .../cli/command/container/testdata/utf8.env | 3 + .../cli/command/container/testdata/valid.env | 1 + .../command/container/testdata/valid.label | 1 + components/cli/command/image/build.go | 11 +- components/cli/command/network/connect.go | 3 +- components/cli/command/network/create.go | 2 +- components/cli/command/secret/create.go | 2 +- components/cli/command/service/opts.go | 10 +- components/cli/command/volume/create.go | 5 +- 16 files changed, 1786 insertions(+), 31 deletions(-) create mode 100644 components/cli/command/container/opts.go create mode 100644 components/cli/command/container/opts_test.go create mode 100755 components/cli/command/container/testdata/utf16.env create mode 100755 components/cli/command/container/testdata/utf16be.env create mode 100755 components/cli/command/container/testdata/utf8.env create mode 100644 components/cli/command/container/testdata/valid.env create mode 100644 components/cli/command/container/testdata/valid.label diff --git a/components/cli/command/container/create.go b/components/cli/command/container/create.go index 7dc644d28c..804ef9c488 100644 --- a/components/cli/command/container/create.go +++ b/components/cli/command/container/create.go @@ -18,7 +18,6 @@ import ( apiclient "github.com/docker/docker/client" "github.com/docker/docker/reference" "github.com/docker/docker/registry" - runconfigopts "github.com/docker/docker/runconfig/opts" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -30,7 +29,7 @@ type createOptions struct { // NewCreateCommand creates a new cobra.Command for `docker create` func NewCreateCommand(dockerCli *command.DockerCli) *cobra.Command { var opts createOptions - var copts *runconfigopts.ContainerOptions + var copts *containerOptions cmd := &cobra.Command{ Use: "create [OPTIONS] IMAGE [COMMAND] [ARG...]", @@ -55,12 +54,12 @@ func NewCreateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Bool("help", false, "Print usage") command.AddTrustedFlags(flags, true) - copts = runconfigopts.AddFlags(flags) + copts = addFlags(flags) return cmd } -func runCreate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *createOptions, copts *runconfigopts.ContainerOptions) error { - config, hostConfig, networkingConfig, err := runconfigopts.Parse(flags, copts) +func runCreate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *createOptions, copts *containerOptions) error { + config, hostConfig, networkingConfig, err := parse(flags, copts) if err != nil { reportError(dockerCli.Err(), "create", err.Error(), true) return cli.StatusError{StatusCode: 125} diff --git a/components/cli/command/container/exec.go b/components/cli/command/container/exec.go index f0381494e2..ca47e59af7 100644 --- a/components/cli/command/container/exec.go +++ b/components/cli/command/container/exec.go @@ -13,7 +13,6 @@ import ( apiclient "github.com/docker/docker/client" options "github.com/docker/docker/opts" "github.com/docker/docker/pkg/promise" - runconfigopts "github.com/docker/docker/runconfig/opts" "github.com/spf13/cobra" ) @@ -30,7 +29,7 @@ type execOptions struct { func newExecOptions() *execOptions { var values []string return &execOptions{ - env: options.NewListOptsRef(&values, runconfigopts.ValidateEnv), + env: options.NewListOptsRef(&values, options.ValidateEnv), } } diff --git a/components/cli/command/container/opts.go b/components/cli/command/container/opts.go new file mode 100644 index 0000000000..0f41dd507c --- /dev/null +++ b/components/cli/command/container/opts.go @@ -0,0 +1,899 @@ +package container + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "path" + "strconv" + "strings" + "time" + + "github.com/docker/docker/api/types/container" + networktypes "github.com/docker/docker/api/types/network" + "github.com/docker/docker/api/types/strslice" + "github.com/docker/docker/opts" + "github.com/docker/docker/pkg/signal" + runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/docker/go-connections/nat" + units "github.com/docker/go-units" + "github.com/spf13/pflag" +) + +// containerOptions is a data object with all the options for creating a container +type containerOptions struct { + attach opts.ListOpts + volumes opts.ListOpts + tmpfs opts.ListOpts + blkioWeightDevice opts.WeightdeviceOpt + deviceReadBps opts.ThrottledeviceOpt + deviceWriteBps opts.ThrottledeviceOpt + links opts.ListOpts + aliases opts.ListOpts + linkLocalIPs opts.ListOpts + deviceReadIOps opts.ThrottledeviceOpt + deviceWriteIOps opts.ThrottledeviceOpt + env opts.ListOpts + labels opts.ListOpts + devices opts.ListOpts + ulimits *opts.UlimitOpt + sysctls *opts.MapOpts + publish opts.ListOpts + expose opts.ListOpts + dns opts.ListOpts + dnsSearch opts.ListOpts + dnsOptions opts.ListOpts + extraHosts opts.ListOpts + volumesFrom opts.ListOpts + envFile opts.ListOpts + capAdd opts.ListOpts + capDrop opts.ListOpts + groupAdd opts.ListOpts + securityOpt opts.ListOpts + storageOpt opts.ListOpts + labelsFile opts.ListOpts + loggingOpts opts.ListOpts + privileged bool + pidMode string + utsMode string + usernsMode string + publishAll bool + stdin bool + tty bool + oomKillDisable bool + oomScoreAdj int + containerIDFile string + entrypoint string + hostname string + memoryString string + memoryReservation string + memorySwap string + kernelMemory string + user string + workingDir string + cpuCount int64 + cpuShares int64 + cpuPercent int64 + cpuPeriod int64 + cpuRealtimePeriod int64 + cpuRealtimeRuntime int64 + cpuQuota int64 + cpus opts.NanoCPUs + cpusetCpus string + cpusetMems string + blkioWeight uint16 + ioMaxBandwidth string + ioMaxIOps uint64 + swappiness int64 + netMode string + macAddress string + ipv4Address string + ipv6Address string + ipcMode string + pidsLimit int64 + restartPolicy string + readonlyRootfs bool + loggingDriver string + cgroupParent string + volumeDriver string + stopSignal string + stopTimeout int + isolation string + shmSize string + noHealthcheck bool + healthCmd string + healthInterval time.Duration + healthTimeout time.Duration + healthRetries int + runtime string + autoRemove bool + init bool + initPath string + credentialSpec string + + Image string + Args []string +} + +// addFlags adds all command line flags that will be used by parse to the FlagSet +func addFlags(flags *pflag.FlagSet) *containerOptions { + copts := &containerOptions{ + aliases: opts.NewListOpts(nil), + attach: opts.NewListOpts(validateAttach), + blkioWeightDevice: opts.NewWeightdeviceOpt(opts.ValidateWeightDevice), + capAdd: opts.NewListOpts(nil), + capDrop: opts.NewListOpts(nil), + dns: opts.NewListOpts(opts.ValidateIPAddress), + dnsOptions: opts.NewListOpts(nil), + dnsSearch: opts.NewListOpts(opts.ValidateDNSSearch), + deviceReadBps: opts.NewThrottledeviceOpt(opts.ValidateThrottleBpsDevice), + deviceReadIOps: opts.NewThrottledeviceOpt(opts.ValidateThrottleIOpsDevice), + deviceWriteBps: opts.NewThrottledeviceOpt(opts.ValidateThrottleBpsDevice), + deviceWriteIOps: opts.NewThrottledeviceOpt(opts.ValidateThrottleIOpsDevice), + devices: opts.NewListOpts(validateDevice), + env: opts.NewListOpts(opts.ValidateEnv), + envFile: opts.NewListOpts(nil), + expose: opts.NewListOpts(nil), + extraHosts: opts.NewListOpts(opts.ValidateExtraHost), + groupAdd: opts.NewListOpts(nil), + labels: opts.NewListOpts(opts.ValidateEnv), + labelsFile: opts.NewListOpts(nil), + linkLocalIPs: opts.NewListOpts(nil), + links: opts.NewListOpts(opts.ValidateLink), + loggingOpts: opts.NewListOpts(nil), + publish: opts.NewListOpts(nil), + securityOpt: opts.NewListOpts(nil), + storageOpt: opts.NewListOpts(nil), + sysctls: opts.NewMapOpts(nil, opts.ValidateSysctl), + tmpfs: opts.NewListOpts(nil), + ulimits: opts.NewUlimitOpt(nil), + volumes: opts.NewListOpts(nil), + volumesFrom: opts.NewListOpts(nil), + } + + // General purpose flags + flags.VarP(&copts.attach, "attach", "a", "Attach to STDIN, STDOUT or STDERR") + flags.Var(&copts.devices, "device", "Add a host device to the container") + flags.VarP(&copts.env, "env", "e", "Set environment variables") + flags.Var(&copts.envFile, "env-file", "Read in a file of environment variables") + flags.StringVar(&copts.entrypoint, "entrypoint", "", "Overwrite the default ENTRYPOINT of the image") + flags.Var(&copts.groupAdd, "group-add", "Add additional groups to join") + flags.StringVarP(&copts.hostname, "hostname", "h", "", "Container host name") + flags.BoolVarP(&copts.stdin, "interactive", "i", false, "Keep STDIN open even if not attached") + flags.VarP(&copts.labels, "label", "l", "Set meta data on a container") + flags.Var(&copts.labelsFile, "label-file", "Read in a line delimited file of labels") + flags.BoolVar(&copts.readonlyRootfs, "read-only", false, "Mount the container's root filesystem as read only") + flags.StringVar(&copts.restartPolicy, "restart", "no", "Restart policy to apply when a container exits") + flags.StringVar(&copts.stopSignal, "stop-signal", signal.DefaultStopSignal, fmt.Sprintf("Signal to stop a container, %v by default", signal.DefaultStopSignal)) + flags.IntVar(&copts.stopTimeout, "stop-timeout", 0, "Timeout (in seconds) to stop a container") + flags.SetAnnotation("stop-timeout", "version", []string{"1.25"}) + flags.Var(copts.sysctls, "sysctl", "Sysctl options") + flags.BoolVarP(&copts.tty, "tty", "t", false, "Allocate a pseudo-TTY") + flags.Var(copts.ulimits, "ulimit", "Ulimit options") + flags.StringVarP(&copts.user, "user", "u", "", "Username or UID (format: [:])") + flags.StringVarP(&copts.workingDir, "workdir", "w", "", "Working directory inside the container") + flags.BoolVar(&copts.autoRemove, "rm", false, "Automatically remove the container when it exits") + + // Security + flags.Var(&copts.capAdd, "cap-add", "Add Linux capabilities") + flags.Var(&copts.capDrop, "cap-drop", "Drop Linux capabilities") + flags.BoolVar(&copts.privileged, "privileged", false, "Give extended privileges to this container") + flags.Var(&copts.securityOpt, "security-opt", "Security Options") + flags.StringVar(&copts.usernsMode, "userns", "", "User namespace to use") + flags.StringVar(&copts.credentialSpec, "credentialspec", "", "Credential spec for managed service account (Windows only)") + + // Network and port publishing flag + flags.Var(&copts.extraHosts, "add-host", "Add a custom host-to-IP mapping (host:ip)") + flags.Var(&copts.dns, "dns", "Set custom DNS servers") + // We allow for both "--dns-opt" and "--dns-option", although the latter is the recommended way. + // This is to be consistent with service create/update + flags.Var(&copts.dnsOptions, "dns-opt", "Set DNS options") + flags.Var(&copts.dnsOptions, "dns-option", "Set DNS options") + flags.MarkHidden("dns-opt") + flags.Var(&copts.dnsSearch, "dns-search", "Set custom DNS search domains") + flags.Var(&copts.expose, "expose", "Expose a port or a range of ports") + flags.StringVar(&copts.ipv4Address, "ip", "", "Container IPv4 address (e.g. 172.30.100.104)") + flags.StringVar(&copts.ipv6Address, "ip6", "", "Container IPv6 address (e.g. 2001:db8::33)") + flags.Var(&copts.links, "link", "Add link to another container") + flags.Var(&copts.linkLocalIPs, "link-local-ip", "Container IPv4/IPv6 link-local addresses") + flags.StringVar(&copts.macAddress, "mac-address", "", "Container MAC address (e.g. 92:d0:c6:0a:29:33)") + flags.VarP(&copts.publish, "publish", "p", "Publish a container's port(s) to the host") + flags.BoolVarP(&copts.publishAll, "publish-all", "P", false, "Publish all exposed ports to random ports") + // We allow for both "--net" and "--network", although the latter is the recommended way. + flags.StringVar(&copts.netMode, "net", "default", "Connect a container to a network") + flags.StringVar(&copts.netMode, "network", "default", "Connect a container to a network") + flags.MarkHidden("net") + // We allow for both "--net-alias" and "--network-alias", although the latter is the recommended way. + flags.Var(&copts.aliases, "net-alias", "Add network-scoped alias for the container") + flags.Var(&copts.aliases, "network-alias", "Add network-scoped alias for the container") + flags.MarkHidden("net-alias") + + // Logging and storage + flags.StringVar(&copts.loggingDriver, "log-driver", "", "Logging driver for the container") + flags.StringVar(&copts.volumeDriver, "volume-driver", "", "Optional volume driver for the container") + flags.Var(&copts.loggingOpts, "log-opt", "Log driver options") + flags.Var(&copts.storageOpt, "storage-opt", "Storage driver options for the container") + flags.Var(&copts.tmpfs, "tmpfs", "Mount a tmpfs directory") + flags.Var(&copts.volumesFrom, "volumes-from", "Mount volumes from the specified container(s)") + flags.VarP(&copts.volumes, "volume", "v", "Bind mount a volume") + + // Health-checking + flags.StringVar(&copts.healthCmd, "health-cmd", "", "Command to run to check health") + flags.DurationVar(&copts.healthInterval, "health-interval", 0, "Time between running the check (ns|us|ms|s|m|h) (default 0s)") + flags.IntVar(&copts.healthRetries, "health-retries", 0, "Consecutive failures needed to report unhealthy") + flags.DurationVar(&copts.healthTimeout, "health-timeout", 0, "Maximum time to allow one check to run (ns|us|ms|s|m|h) (default 0s)") + flags.BoolVar(&copts.noHealthcheck, "no-healthcheck", false, "Disable any container-specified HEALTHCHECK") + + // Resource management + flags.Uint16Var(&copts.blkioWeight, "blkio-weight", 0, "Block IO (relative weight), between 10 and 1000, or 0 to disable (default 0)") + flags.Var(&copts.blkioWeightDevice, "blkio-weight-device", "Block IO weight (relative device weight)") + flags.StringVar(&copts.containerIDFile, "cidfile", "", "Write the container ID to the file") + flags.StringVar(&copts.cpusetCpus, "cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)") + flags.StringVar(&copts.cpusetMems, "cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)") + flags.Int64Var(&copts.cpuCount, "cpu-count", 0, "CPU count (Windows only)") + flags.Int64Var(&copts.cpuPercent, "cpu-percent", 0, "CPU percent (Windows only)") + flags.Int64Var(&copts.cpuPeriod, "cpu-period", 0, "Limit CPU CFS (Completely Fair Scheduler) period") + flags.Int64Var(&copts.cpuQuota, "cpu-quota", 0, "Limit CPU CFS (Completely Fair Scheduler) quota") + flags.Int64Var(&copts.cpuRealtimePeriod, "cpu-rt-period", 0, "Limit CPU real-time period in microseconds") + flags.Int64Var(&copts.cpuRealtimeRuntime, "cpu-rt-runtime", 0, "Limit CPU real-time runtime in microseconds") + flags.Int64VarP(&copts.cpuShares, "cpu-shares", "c", 0, "CPU shares (relative weight)") + flags.Var(&copts.cpus, "cpus", "Number of CPUs") + flags.Var(&copts.deviceReadBps, "device-read-bps", "Limit read rate (bytes per second) from a device") + flags.Var(&copts.deviceReadIOps, "device-read-iops", "Limit read rate (IO per second) from a device") + flags.Var(&copts.deviceWriteBps, "device-write-bps", "Limit write rate (bytes per second) to a device") + flags.Var(&copts.deviceWriteIOps, "device-write-iops", "Limit write rate (IO per second) to a device") + flags.StringVar(&copts.ioMaxBandwidth, "io-maxbandwidth", "", "Maximum IO bandwidth limit for the system drive (Windows only)") + flags.Uint64Var(&copts.ioMaxIOps, "io-maxiops", 0, "Maximum IOps limit for the system drive (Windows only)") + flags.StringVar(&copts.kernelMemory, "kernel-memory", "", "Kernel memory limit") + flags.StringVarP(&copts.memoryString, "memory", "m", "", "Memory limit") + flags.StringVar(&copts.memoryReservation, "memory-reservation", "", "Memory soft limit") + flags.StringVar(&copts.memorySwap, "memory-swap", "", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap") + flags.Int64Var(&copts.swappiness, "memory-swappiness", -1, "Tune container memory swappiness (0 to 100)") + flags.BoolVar(&copts.oomKillDisable, "oom-kill-disable", false, "Disable OOM Killer") + flags.IntVar(&copts.oomScoreAdj, "oom-score-adj", 0, "Tune host's OOM preferences (-1000 to 1000)") + flags.Int64Var(&copts.pidsLimit, "pids-limit", 0, "Tune container pids limit (set -1 for unlimited)") + + // Low-level execution (cgroups, namespaces, ...) + flags.StringVar(&copts.cgroupParent, "cgroup-parent", "", "Optional parent cgroup for the container") + flags.StringVar(&copts.ipcMode, "ipc", "", "IPC namespace to use") + flags.StringVar(&copts.isolation, "isolation", "", "Container isolation technology") + flags.StringVar(&copts.pidMode, "pid", "", "PID namespace to use") + flags.StringVar(&copts.shmSize, "shm-size", "", "Size of /dev/shm, default value is 64MB") + flags.StringVar(&copts.utsMode, "uts", "", "UTS namespace to use") + flags.StringVar(&copts.runtime, "runtime", "", "Runtime to use for this container") + + flags.BoolVar(&copts.init, "init", false, "Run an init inside the container that forwards signals and reaps processes") + flags.StringVar(&copts.initPath, "init-path", "", "Path to the docker-init binary") + return copts +} + +// parse parses the args for the specified command and generates a Config, +// a HostConfig and returns them with the specified command. +// If the specified args are not valid, it will return an error. +func parse(flags *pflag.FlagSet, copts *containerOptions) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig, error) { + var ( + attachStdin = copts.attach.Get("stdin") + attachStdout = copts.attach.Get("stdout") + attachStderr = copts.attach.Get("stderr") + ) + + // Validate the input mac address + if copts.macAddress != "" { + if _, err := opts.ValidateMACAddress(copts.macAddress); err != nil { + return nil, nil, nil, fmt.Errorf("%s is not a valid mac address", copts.macAddress) + } + } + if copts.stdin { + attachStdin = true + } + // If -a is not set, attach to stdout and stderr + if copts.attach.Len() == 0 { + attachStdout = true + attachStderr = true + } + + var err error + + var memory int64 + if copts.memoryString != "" { + memory, err = units.RAMInBytes(copts.memoryString) + if err != nil { + return nil, nil, nil, err + } + } + + var memoryReservation int64 + if copts.memoryReservation != "" { + memoryReservation, err = units.RAMInBytes(copts.memoryReservation) + if err != nil { + return nil, nil, nil, err + } + } + + var memorySwap int64 + if copts.memorySwap != "" { + if copts.memorySwap == "-1" { + memorySwap = -1 + } else { + memorySwap, err = units.RAMInBytes(copts.memorySwap) + if err != nil { + return nil, nil, nil, err + } + } + } + + var kernelMemory int64 + if copts.kernelMemory != "" { + kernelMemory, err = units.RAMInBytes(copts.kernelMemory) + if err != nil { + return nil, nil, nil, err + } + } + + swappiness := copts.swappiness + if swappiness != -1 && (swappiness < 0 || swappiness > 100) { + return nil, nil, nil, fmt.Errorf("invalid value: %d. Valid memory swappiness range is 0-100", swappiness) + } + + var shmSize int64 + if copts.shmSize != "" { + shmSize, err = units.RAMInBytes(copts.shmSize) + if err != nil { + return nil, nil, nil, err + } + } + + // TODO FIXME units.RAMInBytes should have a uint64 version + var maxIOBandwidth int64 + if copts.ioMaxBandwidth != "" { + maxIOBandwidth, err = units.RAMInBytes(copts.ioMaxBandwidth) + if err != nil { + return nil, nil, nil, err + } + if maxIOBandwidth < 0 { + return nil, nil, nil, fmt.Errorf("invalid value: %s. Maximum IO Bandwidth must be positive", copts.ioMaxBandwidth) + } + } + + var binds []string + volumes := copts.volumes.GetMap() + // add any bind targets to the list of container volumes + for bind := range copts.volumes.GetMap() { + if arr := volumeSplitN(bind, 2); len(arr) > 1 { + // after creating the bind mount we want to delete it from the copts.volumes values because + // we do not want bind mounts being committed to image configs + binds = append(binds, bind) + // We should delete from the map (`volumes`) here, as deleting from copts.volumes will not work if + // there are duplicates entries. + delete(volumes, bind) + } + } + + // Can't evaluate options passed into --tmpfs until we actually mount + tmpfs := make(map[string]string) + for _, t := range copts.tmpfs.GetAll() { + if arr := strings.SplitN(t, ":", 2); len(arr) > 1 { + tmpfs[arr[0]] = arr[1] + } else { + tmpfs[arr[0]] = "" + } + } + + var ( + runCmd strslice.StrSlice + entrypoint strslice.StrSlice + ) + + if len(copts.Args) > 0 { + runCmd = strslice.StrSlice(copts.Args) + } + + if copts.entrypoint != "" { + entrypoint = strslice.StrSlice{copts.entrypoint} + } else if flags.Changed("entrypoint") { + // if `--entrypoint=` is parsed then Entrypoint is reset + entrypoint = []string{""} + } + + ports, portBindings, err := nat.ParsePortSpecs(copts.publish.GetAll()) + if err != nil { + return nil, nil, nil, err + } + + // Merge in exposed ports to the map of published ports + for _, e := range copts.expose.GetAll() { + if strings.Contains(e, ":") { + return nil, nil, nil, fmt.Errorf("invalid port format for --expose: %s", e) + } + //support two formats for expose, original format /[] or /[] + proto, port := nat.SplitProtoPort(e) + //parse the start and end port and create a sequence of ports to expose + //if expose a port, the start and end port are the same + start, end, err := nat.ParsePortRange(port) + if err != nil { + return nil, nil, nil, fmt.Errorf("invalid range format for --expose: %s, error: %s", e, err) + } + for i := start; i <= end; i++ { + p, err := nat.NewPort(proto, strconv.FormatUint(i, 10)) + if err != nil { + return nil, nil, nil, err + } + if _, exists := ports[p]; !exists { + ports[p] = struct{}{} + } + } + } + + // parse device mappings + deviceMappings := []container.DeviceMapping{} + for _, device := range copts.devices.GetAll() { + deviceMapping, err := parseDevice(device) + if err != nil { + return nil, nil, nil, err + } + deviceMappings = append(deviceMappings, deviceMapping) + } + + // collect all the environment variables for the container + envVariables, err := runconfigopts.ReadKVStrings(copts.envFile.GetAll(), copts.env.GetAll()) + if err != nil { + return nil, nil, nil, err + } + + // collect all the labels for the container + labels, err := runconfigopts.ReadKVStrings(copts.labelsFile.GetAll(), copts.labels.GetAll()) + if err != nil { + return nil, nil, nil, err + } + + ipcMode := container.IpcMode(copts.ipcMode) + if !ipcMode.Valid() { + return nil, nil, nil, fmt.Errorf("--ipc: invalid IPC mode") + } + + pidMode := container.PidMode(copts.pidMode) + if !pidMode.Valid() { + return nil, nil, nil, fmt.Errorf("--pid: invalid PID mode") + } + + utsMode := container.UTSMode(copts.utsMode) + if !utsMode.Valid() { + return nil, nil, nil, fmt.Errorf("--uts: invalid UTS mode") + } + + usernsMode := container.UsernsMode(copts.usernsMode) + if !usernsMode.Valid() { + return nil, nil, nil, fmt.Errorf("--userns: invalid USER mode") + } + + restartPolicy, err := runconfigopts.ParseRestartPolicy(copts.restartPolicy) + if err != nil { + return nil, nil, nil, err + } + + loggingOpts, err := parseLoggingOpts(copts.loggingDriver, copts.loggingOpts.GetAll()) + if err != nil { + return nil, nil, nil, err + } + + securityOpts, err := parseSecurityOpts(copts.securityOpt.GetAll()) + if err != nil { + return nil, nil, nil, err + } + + storageOpts, err := parseStorageOpts(copts.storageOpt.GetAll()) + if err != nil { + return nil, nil, nil, err + } + + // Healthcheck + var healthConfig *container.HealthConfig + haveHealthSettings := copts.healthCmd != "" || + copts.healthInterval != 0 || + copts.healthTimeout != 0 || + copts.healthRetries != 0 + if copts.noHealthcheck { + if haveHealthSettings { + return nil, nil, nil, fmt.Errorf("--no-healthcheck conflicts with --health-* options") + } + test := strslice.StrSlice{"NONE"} + healthConfig = &container.HealthConfig{Test: test} + } else if haveHealthSettings { + var probe strslice.StrSlice + if copts.healthCmd != "" { + args := []string{"CMD-SHELL", copts.healthCmd} + probe = strslice.StrSlice(args) + } + if copts.healthInterval < 0 { + return nil, nil, nil, fmt.Errorf("--health-interval cannot be negative") + } + if copts.healthTimeout < 0 { + return nil, nil, nil, fmt.Errorf("--health-timeout cannot be negative") + } + + healthConfig = &container.HealthConfig{ + Test: probe, + Interval: copts.healthInterval, + Timeout: copts.healthTimeout, + Retries: copts.healthRetries, + } + } + + resources := container.Resources{ + CgroupParent: copts.cgroupParent, + Memory: memory, + MemoryReservation: memoryReservation, + MemorySwap: memorySwap, + MemorySwappiness: &copts.swappiness, + KernelMemory: kernelMemory, + OomKillDisable: &copts.oomKillDisable, + NanoCPUs: copts.cpus.Value(), + CPUCount: copts.cpuCount, + CPUPercent: copts.cpuPercent, + CPUShares: copts.cpuShares, + CPUPeriod: copts.cpuPeriod, + CpusetCpus: copts.cpusetCpus, + CpusetMems: copts.cpusetMems, + CPUQuota: copts.cpuQuota, + CPURealtimePeriod: copts.cpuRealtimePeriod, + CPURealtimeRuntime: copts.cpuRealtimeRuntime, + PidsLimit: copts.pidsLimit, + BlkioWeight: copts.blkioWeight, + BlkioWeightDevice: copts.blkioWeightDevice.GetList(), + BlkioDeviceReadBps: copts.deviceReadBps.GetList(), + BlkioDeviceWriteBps: copts.deviceWriteBps.GetList(), + BlkioDeviceReadIOps: copts.deviceReadIOps.GetList(), + BlkioDeviceWriteIOps: copts.deviceWriteIOps.GetList(), + IOMaximumIOps: copts.ioMaxIOps, + IOMaximumBandwidth: uint64(maxIOBandwidth), + Ulimits: copts.ulimits.GetList(), + Devices: deviceMappings, + } + + config := &container.Config{ + Hostname: copts.hostname, + ExposedPorts: ports, + User: copts.user, + Tty: copts.tty, + // TODO: deprecated, it comes from -n, --networking + // it's still needed internally to set the network to disabled + // if e.g. bridge is none in daemon opts, and in inspect + NetworkDisabled: false, + OpenStdin: copts.stdin, + AttachStdin: attachStdin, + AttachStdout: attachStdout, + AttachStderr: attachStderr, + Env: envVariables, + Cmd: runCmd, + Image: copts.Image, + Volumes: volumes, + MacAddress: copts.macAddress, + Entrypoint: entrypoint, + WorkingDir: copts.workingDir, + Labels: runconfigopts.ConvertKVStringsToMap(labels), + Healthcheck: healthConfig, + } + if flags.Changed("stop-signal") { + config.StopSignal = copts.stopSignal + } + if flags.Changed("stop-timeout") { + config.StopTimeout = &copts.stopTimeout + } + + hostConfig := &container.HostConfig{ + Binds: binds, + ContainerIDFile: copts.containerIDFile, + OomScoreAdj: copts.oomScoreAdj, + AutoRemove: copts.autoRemove, + Privileged: copts.privileged, + PortBindings: portBindings, + Links: copts.links.GetAll(), + PublishAllPorts: copts.publishAll, + // Make sure the dns fields are never nil. + // New containers don't ever have those fields nil, + // but pre created containers can still have those nil values. + // See https://github.com/docker/docker/pull/17779 + // for a more detailed explanation on why we don't want that. + DNS: copts.dns.GetAllOrEmpty(), + DNSSearch: copts.dnsSearch.GetAllOrEmpty(), + DNSOptions: copts.dnsOptions.GetAllOrEmpty(), + ExtraHosts: copts.extraHosts.GetAll(), + VolumesFrom: copts.volumesFrom.GetAll(), + NetworkMode: container.NetworkMode(copts.netMode), + IpcMode: ipcMode, + PidMode: pidMode, + UTSMode: utsMode, + UsernsMode: usernsMode, + CapAdd: strslice.StrSlice(copts.capAdd.GetAll()), + CapDrop: strslice.StrSlice(copts.capDrop.GetAll()), + GroupAdd: copts.groupAdd.GetAll(), + RestartPolicy: restartPolicy, + SecurityOpt: securityOpts, + StorageOpt: storageOpts, + ReadonlyRootfs: copts.readonlyRootfs, + LogConfig: container.LogConfig{Type: copts.loggingDriver, Config: loggingOpts}, + VolumeDriver: copts.volumeDriver, + Isolation: container.Isolation(copts.isolation), + ShmSize: shmSize, + Resources: resources, + Tmpfs: tmpfs, + Sysctls: copts.sysctls.GetAll(), + Runtime: copts.runtime, + } + + // only set this value if the user provided the flag, else it should default to nil + if flags.Changed("init") { + hostConfig.Init = &copts.init + } + + // When allocating stdin in attached mode, close stdin at client disconnect + if config.OpenStdin && config.AttachStdin { + config.StdinOnce = true + } + + networkingConfig := &networktypes.NetworkingConfig{ + EndpointsConfig: make(map[string]*networktypes.EndpointSettings), + } + + if copts.ipv4Address != "" || copts.ipv6Address != "" || copts.linkLocalIPs.Len() > 0 { + epConfig := &networktypes.EndpointSettings{} + networkingConfig.EndpointsConfig[string(hostConfig.NetworkMode)] = epConfig + + epConfig.IPAMConfig = &networktypes.EndpointIPAMConfig{ + IPv4Address: copts.ipv4Address, + IPv6Address: copts.ipv6Address, + } + + if copts.linkLocalIPs.Len() > 0 { + epConfig.IPAMConfig.LinkLocalIPs = make([]string, copts.linkLocalIPs.Len()) + copy(epConfig.IPAMConfig.LinkLocalIPs, copts.linkLocalIPs.GetAll()) + } + } + + if hostConfig.NetworkMode.IsUserDefined() && len(hostConfig.Links) > 0 { + epConfig := networkingConfig.EndpointsConfig[string(hostConfig.NetworkMode)] + if epConfig == nil { + epConfig = &networktypes.EndpointSettings{} + } + epConfig.Links = make([]string, len(hostConfig.Links)) + copy(epConfig.Links, hostConfig.Links) + networkingConfig.EndpointsConfig[string(hostConfig.NetworkMode)] = epConfig + } + + if copts.aliases.Len() > 0 { + epConfig := networkingConfig.EndpointsConfig[string(hostConfig.NetworkMode)] + if epConfig == nil { + epConfig = &networktypes.EndpointSettings{} + } + epConfig.Aliases = make([]string, copts.aliases.Len()) + copy(epConfig.Aliases, copts.aliases.GetAll()) + networkingConfig.EndpointsConfig[string(hostConfig.NetworkMode)] = epConfig + } + + return config, hostConfig, networkingConfig, nil +} + +func parseLoggingOpts(loggingDriver string, loggingOpts []string) (map[string]string, error) { + loggingOptsMap := runconfigopts.ConvertKVStringsToMap(loggingOpts) + if loggingDriver == "none" && len(loggingOpts) > 0 { + return map[string]string{}, fmt.Errorf("invalid logging opts for driver %s", loggingDriver) + } + return loggingOptsMap, nil +} + +// takes a local seccomp daemon, reads the file contents for sending to the daemon +func parseSecurityOpts(securityOpts []string) ([]string, error) { + for key, opt := range securityOpts { + con := strings.SplitN(opt, "=", 2) + if len(con) == 1 && con[0] != "no-new-privileges" { + if strings.Contains(opt, ":") { + con = strings.SplitN(opt, ":", 2) + } else { + return securityOpts, fmt.Errorf("Invalid --security-opt: %q", opt) + } + } + if con[0] == "seccomp" && con[1] != "unconfined" { + f, err := ioutil.ReadFile(con[1]) + if err != nil { + return securityOpts, fmt.Errorf("opening seccomp profile (%s) failed: %v", con[1], err) + } + b := bytes.NewBuffer(nil) + if err := json.Compact(b, f); err != nil { + return securityOpts, fmt.Errorf("compacting json for seccomp profile (%s) failed: %v", con[1], err) + } + securityOpts[key] = fmt.Sprintf("seccomp=%s", b.Bytes()) + } + } + + return securityOpts, nil +} + +// parses storage options per container into a map +func parseStorageOpts(storageOpts []string) (map[string]string, error) { + m := make(map[string]string) + for _, option := range storageOpts { + if strings.Contains(option, "=") { + opt := strings.SplitN(option, "=", 2) + m[opt[0]] = opt[1] + } else { + return nil, fmt.Errorf("invalid storage option") + } + } + return m, nil +} + +// parseDevice parses a device mapping string to a container.DeviceMapping struct +func parseDevice(device string) (container.DeviceMapping, error) { + src := "" + dst := "" + permissions := "rwm" + arr := strings.Split(device, ":") + switch len(arr) { + case 3: + permissions = arr[2] + fallthrough + case 2: + if validDeviceMode(arr[1]) { + permissions = arr[1] + } else { + dst = arr[1] + } + fallthrough + case 1: + src = arr[0] + default: + return container.DeviceMapping{}, fmt.Errorf("invalid device specification: %s", device) + } + + if dst == "" { + dst = src + } + + deviceMapping := container.DeviceMapping{ + PathOnHost: src, + PathInContainer: dst, + CgroupPermissions: permissions, + } + return deviceMapping, nil +} + +// validDeviceMode checks if the mode for device is valid or not. +// Valid mode is a composition of r (read), w (write), and m (mknod). +func validDeviceMode(mode string) bool { + var legalDeviceMode = map[rune]bool{ + 'r': true, + 'w': true, + 'm': true, + } + if mode == "" { + return false + } + for _, c := range mode { + if !legalDeviceMode[c] { + return false + } + legalDeviceMode[c] = false + } + return true +} + +// validateDevice validates a path for devices +// It will make sure 'val' is in the form: +// [host-dir:]container-path[:mode] +// It also validates the device mode. +func validateDevice(val string) (string, error) { + return validatePath(val, validDeviceMode) +} + +func validatePath(val string, validator func(string) bool) (string, error) { + var containerPath string + var mode string + + if strings.Count(val, ":") > 2 { + return val, fmt.Errorf("bad format for path: %s", val) + } + + split := strings.SplitN(val, ":", 3) + if split[0] == "" { + return val, fmt.Errorf("bad format for path: %s", val) + } + switch len(split) { + case 1: + containerPath = split[0] + val = path.Clean(containerPath) + case 2: + if isValid := validator(split[1]); isValid { + containerPath = split[0] + mode = split[1] + val = fmt.Sprintf("%s:%s", path.Clean(containerPath), mode) + } else { + containerPath = split[1] + val = fmt.Sprintf("%s:%s", split[0], path.Clean(containerPath)) + } + case 3: + containerPath = split[1] + mode = split[2] + if isValid := validator(split[2]); !isValid { + return val, fmt.Errorf("bad mode specified: %s", mode) + } + val = fmt.Sprintf("%s:%s:%s", split[0], containerPath, mode) + } + + if !path.IsAbs(containerPath) { + return val, fmt.Errorf("%s is not an absolute path", containerPath) + } + return val, nil +} + +// volumeSplitN splits raw into a maximum of n parts, separated by a separator colon. +// A separator colon is the last `:` character in the regex `[:\\]?[a-zA-Z]:` (note `\\` is `\` escaped). +// In Windows driver letter appears in two situations: +// a. `^[a-zA-Z]:` (A colon followed by `^[a-zA-Z]:` is OK as colon is the separator in volume option) +// b. A string in the format like `\\?\C:\Windows\...` (UNC). +// Therefore, a driver letter can only follow either a `:` or `\\` +// This allows to correctly split strings such as `C:\foo:D:\:rw` or `/tmp/q:/foo`. +func volumeSplitN(raw string, n int) []string { + var array []string + if len(raw) == 0 || raw[0] == ':' { + // invalid + return nil + } + // numberOfParts counts the number of parts separated by a separator colon + numberOfParts := 0 + // left represents the left-most cursor in raw, updated at every `:` character considered as a separator. + left := 0 + // right represents the right-most cursor in raw incremented with the loop. Note this + // starts at index 1 as index 0 is already handle above as a special case. + for right := 1; right < len(raw); right++ { + // stop parsing if reached maximum number of parts + if n >= 0 && numberOfParts >= n { + break + } + if raw[right] != ':' { + continue + } + potentialDriveLetter := raw[right-1] + if (potentialDriveLetter >= 'A' && potentialDriveLetter <= 'Z') || (potentialDriveLetter >= 'a' && potentialDriveLetter <= 'z') { + if right > 1 { + beforePotentialDriveLetter := raw[right-2] + // Only `:` or `\\` are checked (`/` could fall into the case of `/tmp/q:/foo`) + if beforePotentialDriveLetter != ':' && beforePotentialDriveLetter != '\\' { + // e.g. `C:` is not preceded by any delimiter, therefore it was not a drive letter but a path ending with `C:`. + array = append(array, raw[left:right]) + left = right + 1 + numberOfParts++ + } + // else, `C:` is considered as a drive letter and not as a delimiter, so we continue parsing. + } + // if right == 1, then `C:` is the beginning of the raw string, therefore `:` is again not considered a delimiter and we continue parsing. + } else { + // if `:` is not preceded by a potential drive letter, then consider it as a delimiter. + array = append(array, raw[left:right]) + left = right + 1 + numberOfParts++ + } + } + // need to take care of the last part + if left < len(raw) { + if n >= 0 && numberOfParts >= n { + // if the maximum number of parts is reached, just append the rest to the last part + // left-1 is at the last `:` that needs to be included since not considered a separator. + array[n-1] += raw[left-1:] + } else { + array = append(array, raw[left:]) + } + } + return array +} + +// validateAttach validates that the specified string is a valid attach option. +func validateAttach(val string) (string, error) { + s := strings.ToLower(val) + for _, str := range []string{"stdin", "stdout", "stderr"} { + if s == str { + return s, nil + } + } + return val, fmt.Errorf("valid streams are STDIN, STDOUT and STDERR") +} diff --git a/components/cli/command/container/opts_test.go b/components/cli/command/container/opts_test.go new file mode 100644 index 0000000000..d02a0f7bfc --- /dev/null +++ b/components/cli/command/container/opts_test.go @@ -0,0 +1,857 @@ +package container + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "runtime" + "strings" + "testing" + "time" + + "github.com/docker/docker/api/types/container" + networktypes "github.com/docker/docker/api/types/network" + "github.com/docker/docker/runconfig" + "github.com/docker/go-connections/nat" + "github.com/spf13/pflag" +) + +func TestValidateAttach(t *testing.T) { + valid := []string{ + "stdin", + "stdout", + "stderr", + "STDIN", + "STDOUT", + "STDERR", + } + if _, err := validateAttach("invalid"); err == nil { + t.Fatalf("Expected error with [valid streams are STDIN, STDOUT and STDERR], got nothing") + } + + for _, attach := range valid { + value, err := validateAttach(attach) + if err != nil { + t.Fatal(err) + } + if value != strings.ToLower(attach) { + t.Fatalf("Expected [%v], got [%v]", attach, value) + } + } +} + +func parseRun(args []string) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig, error) { + flags := pflag.NewFlagSet("run", pflag.ContinueOnError) + flags.SetOutput(ioutil.Discard) + flags.Usage = nil + copts := addFlags(flags) + if err := flags.Parse(args); err != nil { + return nil, nil, nil, err + } + return parse(flags, copts) +} + +func parsetest(t *testing.T, args string) (*container.Config, *container.HostConfig, error) { + config, hostConfig, _, err := parseRun(strings.Split(args+" ubuntu bash", " ")) + return config, hostConfig, err +} + +func mustParse(t *testing.T, args string) (*container.Config, *container.HostConfig) { + config, hostConfig, err := parsetest(t, args) + if err != nil { + t.Fatal(err) + } + return config, hostConfig +} + +func TestParseRunLinks(t *testing.T) { + if _, hostConfig := mustParse(t, "--link a:b"); len(hostConfig.Links) == 0 || hostConfig.Links[0] != "a:b" { + t.Fatalf("Error parsing links. Expected []string{\"a:b\"}, received: %v", hostConfig.Links) + } + if _, hostConfig := mustParse(t, "--link a:b --link c:d"); len(hostConfig.Links) < 2 || hostConfig.Links[0] != "a:b" || hostConfig.Links[1] != "c:d" { + t.Fatalf("Error parsing links. Expected []string{\"a:b\", \"c:d\"}, received: %v", hostConfig.Links) + } + if _, hostConfig := mustParse(t, ""); len(hostConfig.Links) != 0 { + t.Fatalf("Error parsing links. No link expected, received: %v", hostConfig.Links) + } +} + +func TestParseRunAttach(t *testing.T) { + if config, _ := mustParse(t, "-a stdin"); !config.AttachStdin || config.AttachStdout || config.AttachStderr { + t.Fatalf("Error parsing attach flags. Expect only Stdin enabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr) + } + if config, _ := mustParse(t, "-a stdin -a stdout"); !config.AttachStdin || !config.AttachStdout || config.AttachStderr { + t.Fatalf("Error parsing attach flags. Expect only Stdin and Stdout enabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr) + } + if config, _ := mustParse(t, "-a stdin -a stdout -a stderr"); !config.AttachStdin || !config.AttachStdout || !config.AttachStderr { + t.Fatalf("Error parsing attach flags. Expect all attach enabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr) + } + if config, _ := mustParse(t, ""); config.AttachStdin || !config.AttachStdout || !config.AttachStderr { + t.Fatalf("Error parsing attach flags. Expect Stdin disabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr) + } + if config, _ := mustParse(t, "-i"); !config.AttachStdin || !config.AttachStdout || !config.AttachStderr { + t.Fatalf("Error parsing attach flags. Expect Stdin enabled. Received: in: %v, out: %v, err: %v", config.AttachStdin, config.AttachStdout, config.AttachStderr) + } + + if _, _, err := parsetest(t, "-a"); err == nil { + t.Fatalf("Error parsing attach flags, `-a` should be an error but is not") + } + if _, _, err := parsetest(t, "-a invalid"); err == nil { + t.Fatalf("Error parsing attach flags, `-a invalid` should be an error but is not") + } + if _, _, err := parsetest(t, "-a invalid -a stdout"); err == nil { + t.Fatalf("Error parsing attach flags, `-a stdout -a invalid` should be an error but is not") + } + if _, _, err := parsetest(t, "-a stdout -a stderr -d"); err == nil { + t.Fatalf("Error parsing attach flags, `-a stdout -a stderr -d` should be an error but is not") + } + if _, _, err := parsetest(t, "-a stdin -d"); err == nil { + t.Fatalf("Error parsing attach flags, `-a stdin -d` should be an error but is not") + } + if _, _, err := parsetest(t, "-a stdout -d"); err == nil { + t.Fatalf("Error parsing attach flags, `-a stdout -d` should be an error but is not") + } + if _, _, err := parsetest(t, "-a stderr -d"); err == nil { + t.Fatalf("Error parsing attach flags, `-a stderr -d` should be an error but is not") + } + if _, _, err := parsetest(t, "-d --rm"); err == nil { + t.Fatalf("Error parsing attach flags, `-d --rm` should be an error but is not") + } +} + +func TestParseRunVolumes(t *testing.T) { + + // A single volume + arr, tryit := setupPlatformVolume([]string{`/tmp`}, []string{`c:\tmp`}) + if config, hostConfig := mustParse(t, tryit); hostConfig.Binds != nil { + t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds) + } else if _, exists := config.Volumes[arr[0]]; !exists { + t.Fatalf("Error parsing volume flags, %q is missing from volumes. Received %v", tryit, config.Volumes) + } + + // Two volumes + arr, tryit = setupPlatformVolume([]string{`/tmp`, `/var`}, []string{`c:\tmp`, `c:\var`}) + if config, hostConfig := mustParse(t, tryit); hostConfig.Binds != nil { + t.Fatalf("Error parsing volume flags, %q should not mount-bind anything. Received %v", tryit, hostConfig.Binds) + } else if _, exists := config.Volumes[arr[0]]; !exists { + t.Fatalf("Error parsing volume flags, %s is missing from volumes. Received %v", arr[0], config.Volumes) + } else if _, exists := config.Volumes[arr[1]]; !exists { + t.Fatalf("Error parsing volume flags, %s is missing from volumes. Received %v", arr[1], config.Volumes) + } + + // A single bind-mount + arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`}) + if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || hostConfig.Binds[0] != arr[0] { + t.Fatalf("Error parsing volume flags, %q should mount-bind the path before the colon into the path after the colon. Received %v %v", arr[0], hostConfig.Binds, config.Volumes) + } + + // Two bind-mounts. + arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/hostVar:/containerVar`}, []string{os.Getenv("ProgramData") + `:c:\ContainerPD`, os.Getenv("TEMP") + `:c:\containerTmp`}) + if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil { + t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds) + } + + // Two bind-mounts, first read-only, second read-write. + // TODO Windows: The Windows version uses read-write as that's the only mode it supports. Can change this post TP4 + arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:ro`, `/hostVar:/containerVar:rw`}, []string{os.Getenv("TEMP") + `:c:\containerTmp:rw`, os.Getenv("ProgramData") + `:c:\ContainerPD:rw`}) + if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil { + t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds) + } + + // Similar to previous test but with alternate modes which are only supported by Linux + if runtime.GOOS != "windows" { + arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:ro,Z`, `/hostVar:/containerVar:rw,Z`}, []string{}) + if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil { + t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds) + } + + arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp:Z`, `/hostVar:/containerVar:z`}, []string{}) + if _, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || compareRandomizedStrings(hostConfig.Binds[0], hostConfig.Binds[1], arr[0], arr[1]) != nil { + t.Fatalf("Error parsing volume flags, `%s and %s` did not mount-bind correctly. Received %v", arr[0], arr[1], hostConfig.Binds) + } + } + + // One bind mount and one volume + arr, tryit = setupPlatformVolume([]string{`/hostTmp:/containerTmp`, `/containerVar`}, []string{os.Getenv("TEMP") + `:c:\containerTmp`, `c:\containerTmp`}) + if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] { + t.Fatalf("Error parsing volume flags, %s and %s should only one and only one bind mount %s. Received %s", arr[0], arr[1], arr[0], hostConfig.Binds) + } else if _, exists := config.Volumes[arr[1]]; !exists { + t.Fatalf("Error parsing volume flags %s and %s. %s is missing from volumes. Received %v", arr[0], arr[1], arr[1], config.Volumes) + } + + // Root to non-c: drive letter (Windows specific) + if runtime.GOOS == "windows" { + arr, tryit = setupPlatformVolume([]string{}, []string{os.Getenv("SystemDrive") + `\:d:`}) + if config, hostConfig := mustParse(t, tryit); hostConfig.Binds == nil || len(hostConfig.Binds) > 1 || hostConfig.Binds[0] != arr[0] || len(config.Volumes) != 0 { + t.Fatalf("Error parsing %s. Should have a single bind mount and no volumes", arr[0]) + } + } + +} + +// setupPlatformVolume takes two arrays of volume specs - a Unix style +// spec and a Windows style spec. Depending on the platform being unit tested, +// it returns one of them, along with a volume string that would be passed +// on the docker CLI (e.g. -v /bar -v /foo). +func setupPlatformVolume(u []string, w []string) ([]string, string) { + var a []string + if runtime.GOOS == "windows" { + a = w + } else { + a = u + } + s := "" + for _, v := range a { + s = s + "-v " + v + " " + } + return a, s +} + +// check if (a == c && b == d) || (a == d && b == c) +// because maps are randomized +func compareRandomizedStrings(a, b, c, d string) error { + if a == c && b == d { + return nil + } + if a == d && b == c { + return nil + } + return fmt.Errorf("strings don't match") +} + +// Simple parse with MacAddress validation +func TestParseWithMacAddress(t *testing.T) { + invalidMacAddress := "--mac-address=invalidMacAddress" + validMacAddress := "--mac-address=92:d0:c6:0a:29:33" + if _, _, _, err := parseRun([]string{invalidMacAddress, "img", "cmd"}); err != nil && err.Error() != "invalidMacAddress is not a valid mac address" { + t.Fatalf("Expected an error with %v mac-address, got %v", invalidMacAddress, err) + } + if config, _ := mustParse(t, validMacAddress); config.MacAddress != "92:d0:c6:0a:29:33" { + t.Fatalf("Expected the config to have '92:d0:c6:0a:29:33' as MacAddress, got '%v'", config.MacAddress) + } +} + +func TestParseWithMemory(t *testing.T) { + invalidMemory := "--memory=invalid" + validMemory := "--memory=1G" + if _, _, _, err := parseRun([]string{invalidMemory, "img", "cmd"}); err != nil && err.Error() != "invalid size: 'invalid'" { + t.Fatalf("Expected an error with '%v' Memory, got '%v'", invalidMemory, err) + } + if _, hostconfig := mustParse(t, validMemory); hostconfig.Memory != 1073741824 { + t.Fatalf("Expected the config to have '1G' as Memory, got '%v'", hostconfig.Memory) + } +} + +func TestParseWithMemorySwap(t *testing.T) { + invalidMemory := "--memory-swap=invalid" + validMemory := "--memory-swap=1G" + anotherValidMemory := "--memory-swap=-1" + if _, _, _, err := parseRun([]string{invalidMemory, "img", "cmd"}); err == nil || err.Error() != "invalid size: 'invalid'" { + t.Fatalf("Expected an error with '%v' MemorySwap, got '%v'", invalidMemory, err) + } + if _, hostconfig := mustParse(t, validMemory); hostconfig.MemorySwap != 1073741824 { + t.Fatalf("Expected the config to have '1073741824' as MemorySwap, got '%v'", hostconfig.MemorySwap) + } + if _, hostconfig := mustParse(t, anotherValidMemory); hostconfig.MemorySwap != -1 { + t.Fatalf("Expected the config to have '-1' as MemorySwap, got '%v'", hostconfig.MemorySwap) + } +} + +func TestParseHostname(t *testing.T) { + validHostnames := map[string]string{ + "hostname": "hostname", + "host-name": "host-name", + "hostname123": "hostname123", + "123hostname": "123hostname", + "hostname-of-63-bytes-long-should-be-valid-and-without-any-error": "hostname-of-63-bytes-long-should-be-valid-and-without-any-error", + } + hostnameWithDomain := "--hostname=hostname.domainname" + hostnameWithDomainTld := "--hostname=hostname.domainname.tld" + for hostname, expectedHostname := range validHostnames { + if config, _ := mustParse(t, fmt.Sprintf("--hostname=%s", hostname)); config.Hostname != expectedHostname { + t.Fatalf("Expected the config to have 'hostname' as hostname, got '%v'", config.Hostname) + } + } + if config, _ := mustParse(t, hostnameWithDomain); config.Hostname != "hostname.domainname" && config.Domainname != "" { + t.Fatalf("Expected the config to have 'hostname' as hostname.domainname, got '%v'", config.Hostname) + } + if config, _ := mustParse(t, hostnameWithDomainTld); config.Hostname != "hostname.domainname.tld" && config.Domainname != "" { + t.Fatalf("Expected the config to have 'hostname' as hostname.domainname.tld, got '%v'", config.Hostname) + } +} + +func TestParseWithExpose(t *testing.T) { + invalids := map[string]string{ + ":": "invalid port format for --expose: :", + "8080:9090": "invalid port format for --expose: 8080:9090", + "/tcp": "invalid range format for --expose: /tcp, error: Empty string specified for ports.", + "/udp": "invalid range format for --expose: /udp, error: Empty string specified for ports.", + "NaN/tcp": `invalid range format for --expose: NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`, + "NaN-NaN/tcp": `invalid range format for --expose: NaN-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`, + "8080-NaN/tcp": `invalid range format for --expose: 8080-NaN/tcp, error: strconv.ParseUint: parsing "NaN": invalid syntax`, + "1234567890-8080/tcp": `invalid range format for --expose: 1234567890-8080/tcp, error: strconv.ParseUint: parsing "1234567890": value out of range`, + } + valids := map[string][]nat.Port{ + "8080/tcp": {"8080/tcp"}, + "8080/udp": {"8080/udp"}, + "8080/ncp": {"8080/ncp"}, + "8080-8080/udp": {"8080/udp"}, + "8080-8082/tcp": {"8080/tcp", "8081/tcp", "8082/tcp"}, + } + for expose, expectedError := range invalids { + if _, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"}); err == nil || err.Error() != expectedError { + t.Fatalf("Expected error '%v' with '--expose=%v', got '%v'", expectedError, expose, err) + } + } + for expose, exposedPorts := range valids { + config, _, _, err := parseRun([]string{fmt.Sprintf("--expose=%v", expose), "img", "cmd"}) + if err != nil { + t.Fatal(err) + } + if len(config.ExposedPorts) != len(exposedPorts) { + t.Fatalf("Expected %v exposed port, got %v", len(exposedPorts), len(config.ExposedPorts)) + } + for _, port := range exposedPorts { + if _, ok := config.ExposedPorts[port]; !ok { + t.Fatalf("Expected %v, got %v", exposedPorts, config.ExposedPorts) + } + } + } + // Merge with actual published port + config, _, _, err := parseRun([]string{"--publish=80", "--expose=80-81/tcp", "img", "cmd"}) + if err != nil { + t.Fatal(err) + } + if len(config.ExposedPorts) != 2 { + t.Fatalf("Expected 2 exposed ports, got %v", config.ExposedPorts) + } + ports := []nat.Port{"80/tcp", "81/tcp"} + for _, port := range ports { + if _, ok := config.ExposedPorts[port]; !ok { + t.Fatalf("Expected %v, got %v", ports, config.ExposedPorts) + } + } +} + +func TestParseDevice(t *testing.T) { + valids := map[string]container.DeviceMapping{ + "/dev/snd": { + PathOnHost: "/dev/snd", + PathInContainer: "/dev/snd", + CgroupPermissions: "rwm", + }, + "/dev/snd:rw": { + PathOnHost: "/dev/snd", + PathInContainer: "/dev/snd", + CgroupPermissions: "rw", + }, + "/dev/snd:/something": { + PathOnHost: "/dev/snd", + PathInContainer: "/something", + CgroupPermissions: "rwm", + }, + "/dev/snd:/something:rw": { + PathOnHost: "/dev/snd", + PathInContainer: "/something", + CgroupPermissions: "rw", + }, + } + for device, deviceMapping := range valids { + _, hostconfig, _, err := parseRun([]string{fmt.Sprintf("--device=%v", device), "img", "cmd"}) + if err != nil { + t.Fatal(err) + } + if len(hostconfig.Devices) != 1 { + t.Fatalf("Expected 1 devices, got %v", hostconfig.Devices) + } + if hostconfig.Devices[0] != deviceMapping { + t.Fatalf("Expected %v, got %v", deviceMapping, hostconfig.Devices) + } + } + +} + +func TestParseModes(t *testing.T) { + // ipc ko + if _, _, _, err := parseRun([]string{"--ipc=container:", "img", "cmd"}); err == nil || err.Error() != "--ipc: invalid IPC mode" { + t.Fatalf("Expected an error with message '--ipc: invalid IPC mode', got %v", err) + } + // ipc ok + _, hostconfig, _, err := parseRun([]string{"--ipc=host", "img", "cmd"}) + if err != nil { + t.Fatal(err) + } + if !hostconfig.IpcMode.Valid() { + t.Fatalf("Expected a valid IpcMode, got %v", hostconfig.IpcMode) + } + // pid ko + if _, _, _, err := parseRun([]string{"--pid=container:", "img", "cmd"}); err == nil || err.Error() != "--pid: invalid PID mode" { + t.Fatalf("Expected an error with message '--pid: invalid PID mode', got %v", err) + } + // pid ok + _, hostconfig, _, err = parseRun([]string{"--pid=host", "img", "cmd"}) + if err != nil { + t.Fatal(err) + } + if !hostconfig.PidMode.Valid() { + t.Fatalf("Expected a valid PidMode, got %v", hostconfig.PidMode) + } + // uts ko + if _, _, _, err := parseRun([]string{"--uts=container:", "img", "cmd"}); err == nil || err.Error() != "--uts: invalid UTS mode" { + t.Fatalf("Expected an error with message '--uts: invalid UTS mode', got %v", err) + } + // uts ok + _, hostconfig, _, err = parseRun([]string{"--uts=host", "img", "cmd"}) + if err != nil { + t.Fatal(err) + } + if !hostconfig.UTSMode.Valid() { + t.Fatalf("Expected a valid UTSMode, got %v", hostconfig.UTSMode) + } + // shm-size ko + if _, _, _, err = parseRun([]string{"--shm-size=a128m", "img", "cmd"}); err == nil || err.Error() != "invalid size: 'a128m'" { + t.Fatalf("Expected an error with message 'invalid size: a128m', got %v", err) + } + // shm-size ok + _, hostconfig, _, err = parseRun([]string{"--shm-size=128m", "img", "cmd"}) + if err != nil { + t.Fatal(err) + } + if hostconfig.ShmSize != 134217728 { + t.Fatalf("Expected a valid ShmSize, got %d", hostconfig.ShmSize) + } +} + +func TestParseRestartPolicy(t *testing.T) { + invalids := map[string]string{ + "always:2:3": "invalid restart policy format", + "on-failure:invalid": "maximum retry count must be an integer", + } + valids := map[string]container.RestartPolicy{ + "": {}, + "always": { + Name: "always", + MaximumRetryCount: 0, + }, + "on-failure:1": { + Name: "on-failure", + MaximumRetryCount: 1, + }, + } + for restart, expectedError := range invalids { + if _, _, _, err := parseRun([]string{fmt.Sprintf("--restart=%s", restart), "img", "cmd"}); err == nil || err.Error() != expectedError { + t.Fatalf("Expected an error with message '%v' for %v, got %v", expectedError, restart, err) + } + } + for restart, expected := range valids { + _, hostconfig, _, err := parseRun([]string{fmt.Sprintf("--restart=%v", restart), "img", "cmd"}) + if err != nil { + t.Fatal(err) + } + if hostconfig.RestartPolicy != expected { + t.Fatalf("Expected %v, got %v", expected, hostconfig.RestartPolicy) + } + } +} + +func TestParseHealth(t *testing.T) { + checkOk := func(args ...string) *container.HealthConfig { + config, _, _, err := parseRun(args) + if err != nil { + t.Fatalf("%#v: %v", args, err) + } + return config.Healthcheck + } + checkError := func(expected string, args ...string) { + config, _, _, err := parseRun(args) + if err == nil { + t.Fatalf("Expected error, but got %#v", config) + } + if err.Error() != expected { + t.Fatalf("Expected %#v, got %#v", expected, err) + } + } + health := checkOk("--no-healthcheck", "img", "cmd") + if health == nil || len(health.Test) != 1 || health.Test[0] != "NONE" { + t.Fatalf("--no-healthcheck failed: %#v", health) + } + + health = checkOk("--health-cmd=/check.sh -q", "img", "cmd") + if len(health.Test) != 2 || health.Test[0] != "CMD-SHELL" || health.Test[1] != "/check.sh -q" { + t.Fatalf("--health-cmd: got %#v", health.Test) + } + if health.Timeout != 0 { + t.Fatalf("--health-cmd: timeout = %f", health.Timeout) + } + + checkError("--no-healthcheck conflicts with --health-* options", + "--no-healthcheck", "--health-cmd=/check.sh -q", "img", "cmd") + + health = checkOk("--health-timeout=2s", "--health-retries=3", "--health-interval=4.5s", "img", "cmd") + if health.Timeout != 2*time.Second || health.Retries != 3 || health.Interval != 4500*time.Millisecond { + t.Fatalf("--health-*: got %#v", health) + } +} + +func TestParseLoggingOpts(t *testing.T) { + // logging opts ko + if _, _, _, err := parseRun([]string{"--log-driver=none", "--log-opt=anything", "img", "cmd"}); err == nil || err.Error() != "invalid logging opts for driver none" { + t.Fatalf("Expected an error with message 'invalid logging opts for driver none', got %v", err) + } + // logging opts ok + _, hostconfig, _, err := parseRun([]string{"--log-driver=syslog", "--log-opt=something", "img", "cmd"}) + if err != nil { + t.Fatal(err) + } + if hostconfig.LogConfig.Type != "syslog" || len(hostconfig.LogConfig.Config) != 1 { + t.Fatalf("Expected a 'syslog' LogConfig with one config, got %v", hostconfig.RestartPolicy) + } +} + +func TestParseEnvfileVariables(t *testing.T) { + e := "open nonexistent: no such file or directory" + if runtime.GOOS == "windows" { + e = "open nonexistent: The system cannot find the file specified." + } + // env ko + if _, _, _, err := parseRun([]string{"--env-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != e { + t.Fatalf("Expected an error with message '%s', got %v", e, err) + } + // env ok + config, _, _, err := parseRun([]string{"--env-file=testdata/valid.env", "img", "cmd"}) + if err != nil { + t.Fatal(err) + } + if len(config.Env) != 1 || config.Env[0] != "ENV1=value1" { + t.Fatalf("Expected a config with [ENV1=value1], got %v", config.Env) + } + config, _, _, err = parseRun([]string{"--env-file=testdata/valid.env", "--env=ENV2=value2", "img", "cmd"}) + if err != nil { + t.Fatal(err) + } + if len(config.Env) != 2 || config.Env[0] != "ENV1=value1" || config.Env[1] != "ENV2=value2" { + t.Fatalf("Expected a config with [ENV1=value1 ENV2=value2], got %v", config.Env) + } +} + +func TestParseEnvfileVariablesWithBOMUnicode(t *testing.T) { + // UTF8 with BOM + config, _, _, err := parseRun([]string{"--env-file=testdata/utf8.env", "img", "cmd"}) + if err != nil { + t.Fatal(err) + } + env := []string{"FOO=BAR", "HELLO=" + string([]byte{0xe6, 0x82, 0xa8, 0xe5, 0xa5, 0xbd}), "BAR=FOO"} + if len(config.Env) != len(env) { + t.Fatalf("Expected a config with %d env variables, got %v: %v", len(env), len(config.Env), config.Env) + } + for i, v := range env { + if config.Env[i] != v { + t.Fatalf("Expected a config with [%s], got %v", v, []byte(config.Env[i])) + } + } + + // UTF16 with BOM + e := "contains invalid utf8 bytes at line" + if _, _, _, err := parseRun([]string{"--env-file=testdata/utf16.env", "img", "cmd"}); err == nil || !strings.Contains(err.Error(), e) { + t.Fatalf("Expected an error with message '%s', got %v", e, err) + } + // UTF16BE with BOM + if _, _, _, err := parseRun([]string{"--env-file=testdata/utf16be.env", "img", "cmd"}); err == nil || !strings.Contains(err.Error(), e) { + t.Fatalf("Expected an error with message '%s', got %v", e, err) + } +} + +func TestParseLabelfileVariables(t *testing.T) { + e := "open nonexistent: no such file or directory" + if runtime.GOOS == "windows" { + e = "open nonexistent: The system cannot find the file specified." + } + // label ko + if _, _, _, err := parseRun([]string{"--label-file=nonexistent", "img", "cmd"}); err == nil || err.Error() != e { + t.Fatalf("Expected an error with message '%s', got %v", e, err) + } + // label ok + config, _, _, err := parseRun([]string{"--label-file=testdata/valid.label", "img", "cmd"}) + if err != nil { + t.Fatal(err) + } + if len(config.Labels) != 1 || config.Labels["LABEL1"] != "value1" { + t.Fatalf("Expected a config with [LABEL1:value1], got %v", config.Labels) + } + config, _, _, err = parseRun([]string{"--label-file=testdata/valid.label", "--label=LABEL2=value2", "img", "cmd"}) + if err != nil { + t.Fatal(err) + } + if len(config.Labels) != 2 || config.Labels["LABEL1"] != "value1" || config.Labels["LABEL2"] != "value2" { + t.Fatalf("Expected a config with [LABEL1:value1 LABEL2:value2], got %v", config.Labels) + } +} + +func TestParseEntryPoint(t *testing.T) { + config, _, _, err := parseRun([]string{"--entrypoint=anything", "cmd", "img"}) + if err != nil { + t.Fatal(err) + } + if len(config.Entrypoint) != 1 && config.Entrypoint[0] != "anything" { + t.Fatalf("Expected entrypoint 'anything', got %v", config.Entrypoint) + } +} + +// This tests the cases for binds which are generated through +// DecodeContainerConfig rather than Parse() +func TestDecodeContainerConfigVolumes(t *testing.T) { + + // Root to root + bindsOrVols, _ := setupPlatformVolume([]string{`/:/`}, []string{os.Getenv("SystemDrive") + `\:c:\`}) + if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { + t.Fatalf("binds %v should have failed", bindsOrVols) + } + if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { + t.Fatalf("volume %v should have failed", bindsOrVols) + } + + // No destination path + bindsOrVols, _ = setupPlatformVolume([]string{`/tmp:`}, []string{os.Getenv("TEMP") + `\:`}) + if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { + t.Fatalf("binds %v should have failed", bindsOrVols) + } + if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { + t.Fatalf("volume %v should have failed", bindsOrVols) + } + + // // No destination path or mode + bindsOrVols, _ = setupPlatformVolume([]string{`/tmp::`}, []string{os.Getenv("TEMP") + `\::`}) + if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { + t.Fatalf("binds %v should have failed", bindsOrVols) + } + if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { + t.Fatalf("volume %v should have failed", bindsOrVols) + } + + // A whole lot of nothing + bindsOrVols = []string{`:`} + if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { + t.Fatalf("binds %v should have failed", bindsOrVols) + } + if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { + t.Fatalf("volume %v should have failed", bindsOrVols) + } + + // A whole lot of nothing with no mode + bindsOrVols = []string{`::`} + if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { + t.Fatalf("binds %v should have failed", bindsOrVols) + } + if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { + t.Fatalf("volume %v should have failed", bindsOrVols) + } + + // Too much including an invalid mode + wTmp := os.Getenv("TEMP") + bindsOrVols, _ = setupPlatformVolume([]string{`/tmp:/tmp:/tmp:/tmp`}, []string{wTmp + ":" + wTmp + ":" + wTmp + ":" + wTmp}) + if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { + t.Fatalf("binds %v should have failed", bindsOrVols) + } + if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { + t.Fatalf("volume %v should have failed", bindsOrVols) + } + + // Windows specific error tests + if runtime.GOOS == "windows" { + // Volume which does not include a drive letter + bindsOrVols = []string{`\tmp`} + if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { + t.Fatalf("binds %v should have failed", bindsOrVols) + } + if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { + t.Fatalf("volume %v should have failed", bindsOrVols) + } + + // Root to C-Drive + bindsOrVols = []string{os.Getenv("SystemDrive") + `\:c:`} + if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { + t.Fatalf("binds %v should have failed", bindsOrVols) + } + if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { + t.Fatalf("volume %v should have failed", bindsOrVols) + } + + // Container path that does not include a drive letter + bindsOrVols = []string{`c:\windows:\somewhere`} + if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { + t.Fatalf("binds %v should have failed", bindsOrVols) + } + if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { + t.Fatalf("volume %v should have failed", bindsOrVols) + } + } + + // Linux-specific error tests + if runtime.GOOS != "windows" { + // Just root + bindsOrVols = []string{`/`} + if _, _, err := callDecodeContainerConfig(nil, bindsOrVols); err == nil { + t.Fatalf("binds %v should have failed", bindsOrVols) + } + if _, _, err := callDecodeContainerConfig(bindsOrVols, nil); err == nil { + t.Fatalf("volume %v should have failed", bindsOrVols) + } + + // A single volume that looks like a bind mount passed in Volumes. + // This should be handled as a bind mount, not a volume. + vols := []string{`/foo:/bar`} + if config, hostConfig, err := callDecodeContainerConfig(vols, nil); err != nil { + t.Fatal("Volume /foo:/bar should have succeeded as a volume name") + } else if hostConfig.Binds != nil { + t.Fatalf("Error parsing volume flags, /foo:/bar should not mount-bind anything. Received %v", hostConfig.Binds) + } else if _, exists := config.Volumes[vols[0]]; !exists { + t.Fatalf("Error parsing volume flags, /foo:/bar is missing from volumes. Received %v", config.Volumes) + } + + } +} + +// callDecodeContainerConfig is a utility function used by TestDecodeContainerConfigVolumes +// to call DecodeContainerConfig. It effectively does what a client would +// do when calling the daemon by constructing a JSON stream of a +// ContainerConfigWrapper which is populated by the set of volume specs +// passed into it. It returns a config and a hostconfig which can be +// validated to ensure DecodeContainerConfig has manipulated the structures +// correctly. +func callDecodeContainerConfig(volumes []string, binds []string) (*container.Config, *container.HostConfig, error) { + var ( + b []byte + err error + c *container.Config + h *container.HostConfig + ) + w := runconfig.ContainerConfigWrapper{ + Config: &container.Config{ + Volumes: map[string]struct{}{}, + }, + HostConfig: &container.HostConfig{ + NetworkMode: "none", + Binds: binds, + }, + } + for _, v := range volumes { + w.Config.Volumes[v] = struct{}{} + } + if b, err = json.Marshal(w); err != nil { + return nil, nil, fmt.Errorf("Error on marshal %s", err.Error()) + } + c, h, _, err = runconfig.DecodeContainerConfig(bytes.NewReader(b)) + if err != nil { + return nil, nil, fmt.Errorf("Error parsing %s: %v", string(b), err) + } + if c == nil || h == nil { + return nil, nil, fmt.Errorf("Empty config or hostconfig") + } + + return c, h, err +} + +func TestVolumeSplitN(t *testing.T) { + for _, x := range []struct { + input string + n int + expected []string + }{ + {`C:\foo:d:`, -1, []string{`C:\foo`, `d:`}}, + {`:C:\foo:d:`, -1, nil}, + {`/foo:/bar:ro`, 3, []string{`/foo`, `/bar`, `ro`}}, + {`/foo:/bar:ro`, 2, []string{`/foo`, `/bar:ro`}}, + {`C:\foo\:/foo`, -1, []string{`C:\foo\`, `/foo`}}, + + {`d:\`, -1, []string{`d:\`}}, + {`d:`, -1, []string{`d:`}}, + {`d:\path`, -1, []string{`d:\path`}}, + {`d:\path with space`, -1, []string{`d:\path with space`}}, + {`d:\pathandmode:rw`, -1, []string{`d:\pathandmode`, `rw`}}, + {`c:\:d:\`, -1, []string{`c:\`, `d:\`}}, + {`c:\windows\:d:`, -1, []string{`c:\windows\`, `d:`}}, + {`c:\windows:d:\s p a c e`, -1, []string{`c:\windows`, `d:\s p a c e`}}, + {`c:\windows:d:\s p a c e:RW`, -1, []string{`c:\windows`, `d:\s p a c e`, `RW`}}, + {`c:\program files:d:\s p a c e i n h o s t d i r`, -1, []string{`c:\program files`, `d:\s p a c e i n h o s t d i r`}}, + {`0123456789name:d:`, -1, []string{`0123456789name`, `d:`}}, + {`MiXeDcAsEnAmE:d:`, -1, []string{`MiXeDcAsEnAmE`, `d:`}}, + {`name:D:`, -1, []string{`name`, `D:`}}, + {`name:D::rW`, -1, []string{`name`, `D:`, `rW`}}, + {`name:D::RW`, -1, []string{`name`, `D:`, `RW`}}, + {`c:/:d:/forward/slashes/are/good/too`, -1, []string{`c:/`, `d:/forward/slashes/are/good/too`}}, + {`c:\Windows`, -1, []string{`c:\Windows`}}, + {`c:\Program Files (x86)`, -1, []string{`c:\Program Files (x86)`}}, + + {``, -1, nil}, + {`.`, -1, []string{`.`}}, + {`..\`, -1, []string{`..\`}}, + {`c:\:..\`, -1, []string{`c:\`, `..\`}}, + {`c:\:d:\:xyzzy`, -1, []string{`c:\`, `d:\`, `xyzzy`}}, + + // Cover directories with one-character name + {`/tmp/x/y:/foo/x/y`, -1, []string{`/tmp/x/y`, `/foo/x/y`}}, + } { + res := volumeSplitN(x.input, x.n) + if len(res) < len(x.expected) { + t.Fatalf("input: %v, expected: %v, got: %v", x.input, x.expected, res) + } + for i, e := range res { + if e != x.expected[i] { + t.Fatalf("input: %v, expected: %v, got: %v", x.input, x.expected, res) + } + } + } +} + +func TestValidateDevice(t *testing.T) { + valid := []string{ + "/home", + "/home:/home", + "/home:/something/else", + "/with space", + "/home:/with space", + "relative:/absolute-path", + "hostPath:/containerPath:r", + "/hostPath:/containerPath:rw", + "/hostPath:/containerPath:mrw", + } + invalid := map[string]string{ + "": "bad format for path: ", + "./": "./ is not an absolute path", + "../": "../ is not an absolute path", + "/:../": "../ is not an absolute path", + "/:path": "path is not an absolute path", + ":": "bad format for path: :", + "/tmp:": " is not an absolute path", + ":test": "bad format for path: :test", + ":/test": "bad format for path: :/test", + "tmp:": " is not an absolute path", + ":test:": "bad format for path: :test:", + "::": "bad format for path: ::", + ":::": "bad format for path: :::", + "/tmp:::": "bad format for path: /tmp:::", + ":/tmp::": "bad format for path: :/tmp::", + "path:ro": "ro is not an absolute path", + "path:rr": "rr is not an absolute path", + "a:/b:ro": "bad mode specified: ro", + "a:/b:rr": "bad mode specified: rr", + } + + for _, path := range valid { + if _, err := validateDevice(path); err != nil { + t.Fatalf("ValidateDevice(`%q`) should succeed: error %q", path, err) + } + } + + for path, expectedError := range invalid { + if _, err := validateDevice(path); err == nil { + t.Fatalf("ValidateDevice(`%q`) should have failed validation", path) + } else { + if err.Error() != expectedError { + t.Fatalf("ValidateDevice(`%q`) error should contain %q, got %q", path, expectedError, err.Error()) + } + } + } +} diff --git a/components/cli/command/container/run.go b/components/cli/command/container/run.go index 0fad93e688..f106a7e3b3 100644 --- a/components/cli/command/container/run.go +++ b/components/cli/command/container/run.go @@ -18,7 +18,6 @@ import ( opttypes "github.com/docker/docker/opts" "github.com/docker/docker/pkg/promise" "github.com/docker/docker/pkg/signal" - runconfigopts "github.com/docker/docker/runconfig/opts" "github.com/docker/libnetwork/resolvconf/dns" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -34,7 +33,7 @@ type runOptions struct { // NewRunCommand create a new `docker run` command func NewRunCommand(dockerCli *command.DockerCli) *cobra.Command { var opts runOptions - var copts *runconfigopts.ContainerOptions + var copts *containerOptions cmd := &cobra.Command{ Use: "run [OPTIONS] IMAGE [COMMAND] [ARG...]", @@ -63,11 +62,11 @@ func NewRunCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Bool("help", false, "Print usage") command.AddTrustedFlags(flags, true) - copts = runconfigopts.AddFlags(flags) + copts = addFlags(flags) return cmd } -func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions, copts *runconfigopts.ContainerOptions) error { +func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions, copts *containerOptions) error { stdout, stderr, stdin := dockerCli.Out(), dockerCli.Err(), dockerCli.In() client := dockerCli.Client() // TODO: pass this as an argument @@ -79,9 +78,9 @@ func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions ErrConflictRestartPolicyAndAutoRemove = fmt.Errorf("Conflicting options: --restart and --rm") ) - config, hostConfig, networkingConfig, err := runconfigopts.Parse(flags, copts) + config, hostConfig, networkingConfig, err := parse(flags, copts) - // just in case the Parse does not exit + // just in case the parse does not exit if err != nil { reportError(stderr, cmdPath, err.Error(), true) return cli.StatusError{StatusCode: 125} diff --git a/components/cli/command/container/testdata/utf16.env b/components/cli/command/container/testdata/utf16.env new file mode 100755 index 0000000000000000000000000000000000000000..3a73358fffbc0d5d3d4df985ccf2f4a1a29cdb2a GIT binary patch literal 54 ucmezW&yB$!2yGdh7#tab7 Date: Sun, 25 Dec 2016 20:31:52 +0100 Subject: [PATCH 490/978] Move package cliconfig to cli/config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I felt it made more sence 👼 Signed-off-by: Vincent Demeester Upstream-commit: bfe47a124a2cbe25566b4e2f4cf04ae2409c8192 Component: cli --- components/cli/command/cli.go | 10 +- components/cli/config/config.go | 120 ++++ components/cli/config/config_test.go | 621 ++++++++++++++++++ components/cli/config/configfile/file.go | 183 ++++++ components/cli/config/configfile/file_test.go | 27 + .../cli/config/credentials/credentials.go | 17 + .../cli/config/credentials/default_store.go | 22 + .../credentials/default_store_darwin.go | 3 + .../config/credentials/default_store_linux.go | 3 + .../credentials/default_store_unsupported.go | 5 + .../credentials/default_store_windows.go | 3 + .../cli/config/credentials/file_store.go | 53 ++ .../cli/config/credentials/file_store_test.go | 139 ++++ .../cli/config/credentials/native_store.go | 144 ++++ .../config/credentials/native_store_test.go | 355 ++++++++++ components/cli/flags/common.go | 4 +- components/cli/trust/trust.go | 6 +- 17 files changed, 1705 insertions(+), 10 deletions(-) create mode 100644 components/cli/config/config.go create mode 100644 components/cli/config/config_test.go create mode 100644 components/cli/config/configfile/file.go create mode 100644 components/cli/config/configfile/file_test.go create mode 100644 components/cli/config/credentials/credentials.go create mode 100644 components/cli/config/credentials/default_store.go create mode 100644 components/cli/config/credentials/default_store_darwin.go create mode 100644 components/cli/config/credentials/default_store_linux.go create mode 100644 components/cli/config/credentials/default_store_unsupported.go create mode 100644 components/cli/config/credentials/default_store_windows.go create mode 100644 components/cli/config/credentials/file_store.go create mode 100644 components/cli/config/credentials/file_store_test.go create mode 100644 components/cli/config/credentials/native_store.go create mode 100644 components/cli/config/credentials/native_store_test.go diff --git a/components/cli/command/cli.go b/components/cli/command/cli.go index 6d1dd7472e..c287ebcf77 100644 --- a/components/cli/command/cli.go +++ b/components/cli/command/cli.go @@ -12,10 +12,10 @@ import ( "github.com/docker/docker/api" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/versions" + cliconfig "github.com/docker/docker/cli/config" + "github.com/docker/docker/cli/config/configfile" + "github.com/docker/docker/cli/config/credentials" cliflags "github.com/docker/docker/cli/flags" - "github.com/docker/docker/cliconfig" - "github.com/docker/docker/cliconfig/configfile" - "github.com/docker/docker/cliconfig/credentials" "github.com/docker/docker/client" "github.com/docker/docker/dockerversion" dopts "github.com/docker/docker/opts" @@ -150,7 +150,7 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error { cli.defaultVersion = cli.client.ClientVersion() if opts.Common.TrustKey == "" { - cli.keyFile = filepath.Join(cliconfig.ConfigDir(), cliflags.DefaultTrustKeyFile) + cli.keyFile = filepath.Join(cliconfig.Dir(), cliflags.DefaultTrustKeyFile) } else { cli.keyFile = opts.Common.TrustKey } @@ -179,7 +179,7 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer) *DockerCli { // LoadDefaultConfigFile attempts to load the default config file and returns // an initialized ConfigFile struct if none is found. func LoadDefaultConfigFile(err io.Writer) *configfile.ConfigFile { - configFile, e := cliconfig.Load(cliconfig.ConfigDir()) + configFile, e := cliconfig.Load(cliconfig.Dir()) if e != nil { fmt.Fprintf(err, "WARNING: Error loading config file:%v\n", e) } diff --git a/components/cli/config/config.go b/components/cli/config/config.go new file mode 100644 index 0000000000..ab0fa5451a --- /dev/null +++ b/components/cli/config/config.go @@ -0,0 +1,120 @@ +package config + +import ( + "fmt" + "io" + "os" + "path/filepath" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli/config/configfile" + "github.com/docker/docker/pkg/homedir" +) + +const ( + // ConfigFileName is the name of config file + ConfigFileName = "config.json" + configFileDir = ".docker" + oldConfigfile = ".dockercfg" +) + +var ( + configDir = os.Getenv("DOCKER_CONFIG") +) + +func init() { + if configDir == "" { + configDir = filepath.Join(homedir.Get(), configFileDir) + } +} + +// Dir returns the directory the configuration file is stored in +func Dir() string { + return configDir +} + +// SetDir sets the directory the configuration file is stored in +func SetDir(dir string) { + configDir = dir +} + +// NewConfigFile initializes an empty configuration file for the given filename 'fn' +func NewConfigFile(fn string) *configfile.ConfigFile { + return &configfile.ConfigFile{ + AuthConfigs: make(map[string]types.AuthConfig), + HTTPHeaders: make(map[string]string), + Filename: fn, + } +} + +// LegacyLoadFromReader is a convenience function that creates a ConfigFile object from +// a non-nested reader +func LegacyLoadFromReader(configData io.Reader) (*configfile.ConfigFile, error) { + configFile := configfile.ConfigFile{ + AuthConfigs: make(map[string]types.AuthConfig), + } + err := configFile.LegacyLoadFromReader(configData) + return &configFile, err +} + +// LoadFromReader is a convenience function that creates a ConfigFile object from +// a reader +func LoadFromReader(configData io.Reader) (*configfile.ConfigFile, error) { + configFile := configfile.ConfigFile{ + AuthConfigs: make(map[string]types.AuthConfig), + } + err := configFile.LoadFromReader(configData) + return &configFile, err +} + +// Load reads the configuration files in the given directory, and sets up +// the auth config information and returns values. +// FIXME: use the internal golang config parser +func Load(configDir string) (*configfile.ConfigFile, error) { + if configDir == "" { + configDir = Dir() + } + + configFile := configfile.ConfigFile{ + AuthConfigs: make(map[string]types.AuthConfig), + Filename: filepath.Join(configDir, ConfigFileName), + } + + // Try happy path first - latest config file + if _, err := os.Stat(configFile.Filename); err == nil { + file, err := os.Open(configFile.Filename) + if err != nil { + return &configFile, fmt.Errorf("%s - %v", configFile.Filename, err) + } + defer file.Close() + err = configFile.LoadFromReader(file) + if err != nil { + err = fmt.Errorf("%s - %v", configFile.Filename, err) + } + return &configFile, err + } else if !os.IsNotExist(err) { + // if file is there but we can't stat it for any reason other + // than it doesn't exist then stop + return &configFile, fmt.Errorf("%s - %v", configFile.Filename, err) + } + + // Can't find latest config file so check for the old one + confFile := filepath.Join(homedir.Get(), oldConfigfile) + if _, err := os.Stat(confFile); err != nil { + return &configFile, nil //missing file is not an error + } + file, err := os.Open(confFile) + if err != nil { + return &configFile, fmt.Errorf("%s - %v", confFile, err) + } + defer file.Close() + err = configFile.LegacyLoadFromReader(file) + if err != nil { + return &configFile, fmt.Errorf("%s - %v", confFile, err) + } + + if configFile.HTTPHeaders == nil { + configFile.HTTPHeaders = map[string]string{} + } + return &configFile, nil +} diff --git a/components/cli/config/config_test.go b/components/cli/config/config_test.go new file mode 100644 index 0000000000..195473bb8a --- /dev/null +++ b/components/cli/config/config_test.go @@ -0,0 +1,621 @@ +package config + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/docker/docker/cli/config/configfile" + "github.com/docker/docker/pkg/homedir" +) + +func TestEmptyConfigDir(t *testing.T) { + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpHome) + + SetDir(tmpHome) + + config, err := Load("") + if err != nil { + t.Fatalf("Failed loading on empty config dir: %q", err) + } + + expectedConfigFilename := filepath.Join(tmpHome, ConfigFileName) + if config.Filename != expectedConfigFilename { + t.Fatalf("Expected config filename %s, got %s", expectedConfigFilename, config.Filename) + } + + // Now save it and make sure it shows up in new form + saveConfigAndValidateNewFormat(t, config, tmpHome) +} + +func TestMissingFile(t *testing.T) { + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpHome) + + config, err := Load(tmpHome) + if err != nil { + t.Fatalf("Failed loading on missing file: %q", err) + } + + // Now save it and make sure it shows up in new form + saveConfigAndValidateNewFormat(t, config, tmpHome) +} + +func TestSaveFileToDirs(t *testing.T) { + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpHome) + + tmpHome += "/.docker" + + config, err := Load(tmpHome) + if err != nil { + t.Fatalf("Failed loading on missing file: %q", err) + } + + // Now save it and make sure it shows up in new form + saveConfigAndValidateNewFormat(t, config, tmpHome) +} + +func TestEmptyFile(t *testing.T) { + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpHome) + + fn := filepath.Join(tmpHome, ConfigFileName) + if err := ioutil.WriteFile(fn, []byte(""), 0600); err != nil { + t.Fatal(err) + } + + _, err = Load(tmpHome) + if err == nil { + t.Fatalf("Was supposed to fail") + } +} + +func TestEmptyJSON(t *testing.T) { + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpHome) + + fn := filepath.Join(tmpHome, ConfigFileName) + if err := ioutil.WriteFile(fn, []byte("{}"), 0600); err != nil { + t.Fatal(err) + } + + config, err := Load(tmpHome) + if err != nil { + t.Fatalf("Failed loading on empty json file: %q", err) + } + + // Now save it and make sure it shows up in new form + saveConfigAndValidateNewFormat(t, config, tmpHome) +} + +func TestOldInvalidsAuth(t *testing.T) { + invalids := map[string]string{ + `username = test`: "The Auth config file is empty", + `username +password`: "Invalid Auth config file", + `username = test +email`: "Invalid auth configuration file", + } + + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpHome) + + homeKey := homedir.Key() + homeVal := homedir.Get() + + defer func() { os.Setenv(homeKey, homeVal) }() + os.Setenv(homeKey, tmpHome) + + for content, expectedError := range invalids { + fn := filepath.Join(tmpHome, oldConfigfile) + if err := ioutil.WriteFile(fn, []byte(content), 0600); err != nil { + t.Fatal(err) + } + + config, err := Load(tmpHome) + // Use Contains instead of == since the file name will change each time + if err == nil || !strings.Contains(err.Error(), expectedError) { + t.Fatalf("Should have failed\nConfig: %v\nGot: %v\nExpected: %v", config, err, expectedError) + } + + } +} + +func TestOldValidAuth(t *testing.T) { + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatal(err) + } + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpHome) + + homeKey := homedir.Key() + homeVal := homedir.Get() + + defer func() { os.Setenv(homeKey, homeVal) }() + os.Setenv(homeKey, tmpHome) + + fn := filepath.Join(tmpHome, oldConfigfile) + js := `username = am9lam9lOmhlbGxv + email = user@example.com` + if err := ioutil.WriteFile(fn, []byte(js), 0600); err != nil { + t.Fatal(err) + } + + config, err := Load(tmpHome) + if err != nil { + t.Fatal(err) + } + + // defaultIndexserver is https://index.docker.io/v1/ + ac := config.AuthConfigs["https://index.docker.io/v1/"] + if ac.Username != "joejoe" || ac.Password != "hello" { + t.Fatalf("Missing data from parsing:\n%q", config) + } + + // Now save it and make sure it shows up in new form + configStr := saveConfigAndValidateNewFormat(t, config, tmpHome) + + expConfStr := `{ + "auths": { + "https://index.docker.io/v1/": { + "auth": "am9lam9lOmhlbGxv" + } + } +}` + + if configStr != expConfStr { + t.Fatalf("Should have save in new form: \n%s\n not \n%s", configStr, expConfStr) + } +} + +func TestOldJSONInvalid(t *testing.T) { + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpHome) + + homeKey := homedir.Key() + homeVal := homedir.Get() + + defer func() { os.Setenv(homeKey, homeVal) }() + os.Setenv(homeKey, tmpHome) + + fn := filepath.Join(tmpHome, oldConfigfile) + js := `{"https://index.docker.io/v1/":{"auth":"test","email":"user@example.com"}}` + if err := ioutil.WriteFile(fn, []byte(js), 0600); err != nil { + t.Fatal(err) + } + + config, err := Load(tmpHome) + // Use Contains instead of == since the file name will change each time + if err == nil || !strings.Contains(err.Error(), "Invalid auth configuration file") { + t.Fatalf("Expected an error got : %v, %v", config, err) + } +} + +func TestOldJSON(t *testing.T) { + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpHome) + + homeKey := homedir.Key() + homeVal := homedir.Get() + + defer func() { os.Setenv(homeKey, homeVal) }() + os.Setenv(homeKey, tmpHome) + + fn := filepath.Join(tmpHome, oldConfigfile) + js := `{"https://index.docker.io/v1/":{"auth":"am9lam9lOmhlbGxv","email":"user@example.com"}}` + if err := ioutil.WriteFile(fn, []byte(js), 0600); err != nil { + t.Fatal(err) + } + + config, err := Load(tmpHome) + if err != nil { + t.Fatalf("Failed loading on empty json file: %q", err) + } + + ac := config.AuthConfigs["https://index.docker.io/v1/"] + if ac.Username != "joejoe" || ac.Password != "hello" { + t.Fatalf("Missing data from parsing:\n%q", config) + } + + // Now save it and make sure it shows up in new form + configStr := saveConfigAndValidateNewFormat(t, config, tmpHome) + + expConfStr := `{ + "auths": { + "https://index.docker.io/v1/": { + "auth": "am9lam9lOmhlbGxv", + "email": "user@example.com" + } + } +}` + + if configStr != expConfStr { + t.Fatalf("Should have save in new form: \n'%s'\n not \n'%s'\n", configStr, expConfStr) + } +} + +func TestNewJSON(t *testing.T) { + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpHome) + + fn := filepath.Join(tmpHome, ConfigFileName) + js := ` { "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv" } } }` + if err := ioutil.WriteFile(fn, []byte(js), 0600); err != nil { + t.Fatal(err) + } + + config, err := Load(tmpHome) + if err != nil { + t.Fatalf("Failed loading on empty json file: %q", err) + } + + ac := config.AuthConfigs["https://index.docker.io/v1/"] + if ac.Username != "joejoe" || ac.Password != "hello" { + t.Fatalf("Missing data from parsing:\n%q", config) + } + + // Now save it and make sure it shows up in new form + configStr := saveConfigAndValidateNewFormat(t, config, tmpHome) + + expConfStr := `{ + "auths": { + "https://index.docker.io/v1/": { + "auth": "am9lam9lOmhlbGxv" + } + } +}` + + if configStr != expConfStr { + t.Fatalf("Should have save in new form: \n%s\n not \n%s", configStr, expConfStr) + } +} + +func TestNewJSONNoEmail(t *testing.T) { + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpHome) + + fn := filepath.Join(tmpHome, ConfigFileName) + js := ` { "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv" } } }` + if err := ioutil.WriteFile(fn, []byte(js), 0600); err != nil { + t.Fatal(err) + } + + config, err := Load(tmpHome) + if err != nil { + t.Fatalf("Failed loading on empty json file: %q", err) + } + + ac := config.AuthConfigs["https://index.docker.io/v1/"] + if ac.Username != "joejoe" || ac.Password != "hello" { + t.Fatalf("Missing data from parsing:\n%q", config) + } + + // Now save it and make sure it shows up in new form + configStr := saveConfigAndValidateNewFormat(t, config, tmpHome) + + expConfStr := `{ + "auths": { + "https://index.docker.io/v1/": { + "auth": "am9lam9lOmhlbGxv" + } + } +}` + + if configStr != expConfStr { + t.Fatalf("Should have save in new form: \n%s\n not \n%s", configStr, expConfStr) + } +} + +func TestJSONWithPsFormat(t *testing.T) { + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpHome) + + fn := filepath.Join(tmpHome, ConfigFileName) + js := `{ + "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } }, + "psFormat": "table {{.ID}}\\t{{.Label \"com.docker.label.cpu\"}}" +}` + if err := ioutil.WriteFile(fn, []byte(js), 0600); err != nil { + t.Fatal(err) + } + + config, err := Load(tmpHome) + if err != nil { + t.Fatalf("Failed loading on empty json file: %q", err) + } + + if config.PsFormat != `table {{.ID}}\t{{.Label "com.docker.label.cpu"}}` { + t.Fatalf("Unknown ps format: %s\n", config.PsFormat) + } + + // Now save it and make sure it shows up in new form + configStr := saveConfigAndValidateNewFormat(t, config, tmpHome) + if !strings.Contains(configStr, `"psFormat":`) || + !strings.Contains(configStr, "{{.ID}}") { + t.Fatalf("Should have save in new form: %s", configStr) + } +} + +func TestJSONWithCredentialStore(t *testing.T) { + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpHome) + + fn := filepath.Join(tmpHome, ConfigFileName) + js := `{ + "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } }, + "credsStore": "crazy-secure-storage" +}` + if err := ioutil.WriteFile(fn, []byte(js), 0600); err != nil { + t.Fatal(err) + } + + config, err := Load(tmpHome) + if err != nil { + t.Fatalf("Failed loading on empty json file: %q", err) + } + + if config.CredentialsStore != "crazy-secure-storage" { + t.Fatalf("Unknown credential store: %s\n", config.CredentialsStore) + } + + // Now save it and make sure it shows up in new form + configStr := saveConfigAndValidateNewFormat(t, config, tmpHome) + if !strings.Contains(configStr, `"credsStore":`) || + !strings.Contains(configStr, "crazy-secure-storage") { + t.Fatalf("Should have save in new form: %s", configStr) + } +} + +func TestJSONWithCredentialHelpers(t *testing.T) { + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpHome) + + fn := filepath.Join(tmpHome, ConfigFileName) + js := `{ + "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } }, + "credHelpers": { "images.io": "images-io", "containers.com": "crazy-secure-storage" } +}` + if err := ioutil.WriteFile(fn, []byte(js), 0600); err != nil { + t.Fatal(err) + } + + config, err := Load(tmpHome) + if err != nil { + t.Fatalf("Failed loading on empty json file: %q", err) + } + + if config.CredentialHelpers == nil { + t.Fatal("config.CredentialHelpers was nil") + } else if config.CredentialHelpers["images.io"] != "images-io" || + config.CredentialHelpers["containers.com"] != "crazy-secure-storage" { + t.Fatalf("Credential helpers not deserialized properly: %v\n", config.CredentialHelpers) + } + + // Now save it and make sure it shows up in new form + configStr := saveConfigAndValidateNewFormat(t, config, tmpHome) + if !strings.Contains(configStr, `"credHelpers":`) || + !strings.Contains(configStr, "images.io") || + !strings.Contains(configStr, "images-io") || + !strings.Contains(configStr, "containers.com") || + !strings.Contains(configStr, "crazy-secure-storage") { + t.Fatalf("Should have save in new form: %s", configStr) + } +} + +// Save it and make sure it shows up in new form +func saveConfigAndValidateNewFormat(t *testing.T, config *configfile.ConfigFile, homeFolder string) string { + if err := config.Save(); err != nil { + t.Fatalf("Failed to save: %q", err) + } + + buf, err := ioutil.ReadFile(filepath.Join(homeFolder, ConfigFileName)) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(buf), `"auths":`) { + t.Fatalf("Should have save in new form: %s", string(buf)) + } + return string(buf) +} + +func TestConfigDir(t *testing.T) { + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpHome) + + if Dir() == tmpHome { + t.Fatalf("Expected ConfigDir to be different than %s by default, but was the same", tmpHome) + } + + // Update configDir + SetDir(tmpHome) + + if Dir() != tmpHome { + t.Fatalf("Expected ConfigDir to %s, but was %s", tmpHome, Dir()) + } +} + +func TestConfigFile(t *testing.T) { + configFilename := "configFilename" + configFile := NewConfigFile(configFilename) + + if configFile.Filename != configFilename { + t.Fatalf("Expected %s, got %s", configFilename, configFile.Filename) + } +} + +func TestJSONReaderNoFile(t *testing.T) { + js := ` { "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } } }` + + config, err := LoadFromReader(strings.NewReader(js)) + if err != nil { + t.Fatalf("Failed loading on empty json file: %q", err) + } + + ac := config.AuthConfigs["https://index.docker.io/v1/"] + if ac.Username != "joejoe" || ac.Password != "hello" { + t.Fatalf("Missing data from parsing:\n%q", config) + } + +} + +func TestOldJSONReaderNoFile(t *testing.T) { + js := `{"https://index.docker.io/v1/":{"auth":"am9lam9lOmhlbGxv","email":"user@example.com"}}` + + config, err := LegacyLoadFromReader(strings.NewReader(js)) + if err != nil { + t.Fatalf("Failed loading on empty json file: %q", err) + } + + ac := config.AuthConfigs["https://index.docker.io/v1/"] + if ac.Username != "joejoe" || ac.Password != "hello" { + t.Fatalf("Missing data from parsing:\n%q", config) + } +} + +func TestJSONWithPsFormatNoFile(t *testing.T) { + js := `{ + "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv", "email": "user@example.com" } }, + "psFormat": "table {{.ID}}\\t{{.Label \"com.docker.label.cpu\"}}" +}` + config, err := LoadFromReader(strings.NewReader(js)) + if err != nil { + t.Fatalf("Failed loading on empty json file: %q", err) + } + + if config.PsFormat != `table {{.ID}}\t{{.Label "com.docker.label.cpu"}}` { + t.Fatalf("Unknown ps format: %s\n", config.PsFormat) + } + +} + +func TestJSONSaveWithNoFile(t *testing.T) { + js := `{ + "auths": { "https://index.docker.io/v1/": { "auth": "am9lam9lOmhlbGxv" } }, + "psFormat": "table {{.ID}}\\t{{.Label \"com.docker.label.cpu\"}}" +}` + config, err := LoadFromReader(strings.NewReader(js)) + err = config.Save() + if err == nil { + t.Fatalf("Expected error. File should not have been able to save with no file name.") + } + + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatalf("Failed to create a temp dir: %q", err) + } + defer os.RemoveAll(tmpHome) + + fn := filepath.Join(tmpHome, ConfigFileName) + f, _ := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + defer f.Close() + + err = config.SaveToWriter(f) + if err != nil { + t.Fatalf("Failed saving to file: %q", err) + } + buf, err := ioutil.ReadFile(filepath.Join(tmpHome, ConfigFileName)) + if err != nil { + t.Fatal(err) + } + expConfStr := `{ + "auths": { + "https://index.docker.io/v1/": { + "auth": "am9lam9lOmhlbGxv" + } + }, + "psFormat": "table {{.ID}}\\t{{.Label \"com.docker.label.cpu\"}}" +}` + if string(buf) != expConfStr { + t.Fatalf("Should have save in new form: \n%s\nnot \n%s", string(buf), expConfStr) + } +} + +func TestLegacyJSONSaveWithNoFile(t *testing.T) { + + js := `{"https://index.docker.io/v1/":{"auth":"am9lam9lOmhlbGxv","email":"user@example.com"}}` + config, err := LegacyLoadFromReader(strings.NewReader(js)) + err = config.Save() + if err == nil { + t.Fatalf("Expected error. File should not have been able to save with no file name.") + } + + tmpHome, err := ioutil.TempDir("", "config-test") + if err != nil { + t.Fatalf("Failed to create a temp dir: %q", err) + } + defer os.RemoveAll(tmpHome) + + fn := filepath.Join(tmpHome, ConfigFileName) + f, _ := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + defer f.Close() + + if err = config.SaveToWriter(f); err != nil { + t.Fatalf("Failed saving to file: %q", err) + } + buf, err := ioutil.ReadFile(filepath.Join(tmpHome, ConfigFileName)) + if err != nil { + t.Fatal(err) + } + + expConfStr := `{ + "auths": { + "https://index.docker.io/v1/": { + "auth": "am9lam9lOmhlbGxv", + "email": "user@example.com" + } + } +}` + + if string(buf) != expConfStr { + t.Fatalf("Should have save in new form: \n%s\n not \n%s", string(buf), expConfStr) + } +} diff --git a/components/cli/config/configfile/file.go b/components/cli/config/configfile/file.go new file mode 100644 index 0000000000..39097133a4 --- /dev/null +++ b/components/cli/config/configfile/file.go @@ -0,0 +1,183 @@ +package configfile + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/docker/docker/api/types" +) + +const ( + // This constant is only used for really old config files when the + // URL wasn't saved as part of the config file and it was just + // assumed to be this value. + defaultIndexserver = "https://index.docker.io/v1/" +) + +// ConfigFile ~/.docker/config.json file info +type ConfigFile struct { + AuthConfigs map[string]types.AuthConfig `json:"auths"` + HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"` + PsFormat string `json:"psFormat,omitempty"` + ImagesFormat string `json:"imagesFormat,omitempty"` + NetworksFormat string `json:"networksFormat,omitempty"` + VolumesFormat string `json:"volumesFormat,omitempty"` + StatsFormat string `json:"statsFormat,omitempty"` + DetachKeys string `json:"detachKeys,omitempty"` + CredentialsStore string `json:"credsStore,omitempty"` + CredentialHelpers map[string]string `json:"credHelpers,omitempty"` + Filename string `json:"-"` // Note: for internal use only + ServiceInspectFormat string `json:"serviceInspectFormat,omitempty"` +} + +// LegacyLoadFromReader reads the non-nested configuration data given and sets up the +// auth config information with given directory and populates the receiver object +func (configFile *ConfigFile) LegacyLoadFromReader(configData io.Reader) error { + b, err := ioutil.ReadAll(configData) + if err != nil { + return err + } + + if err := json.Unmarshal(b, &configFile.AuthConfigs); err != nil { + arr := strings.Split(string(b), "\n") + if len(arr) < 2 { + return fmt.Errorf("The Auth config file is empty") + } + authConfig := types.AuthConfig{} + origAuth := strings.Split(arr[0], " = ") + if len(origAuth) != 2 { + return fmt.Errorf("Invalid Auth config file") + } + authConfig.Username, authConfig.Password, err = decodeAuth(origAuth[1]) + if err != nil { + return err + } + authConfig.ServerAddress = defaultIndexserver + configFile.AuthConfigs[defaultIndexserver] = authConfig + } else { + for k, authConfig := range configFile.AuthConfigs { + authConfig.Username, authConfig.Password, err = decodeAuth(authConfig.Auth) + if err != nil { + return err + } + authConfig.Auth = "" + authConfig.ServerAddress = k + configFile.AuthConfigs[k] = authConfig + } + } + return nil +} + +// LoadFromReader reads the configuration data given and sets up the auth config +// information with given directory and populates the receiver object +func (configFile *ConfigFile) LoadFromReader(configData io.Reader) error { + if err := json.NewDecoder(configData).Decode(&configFile); err != nil { + return err + } + var err error + for addr, ac := range configFile.AuthConfigs { + ac.Username, ac.Password, err = decodeAuth(ac.Auth) + if err != nil { + return err + } + ac.Auth = "" + ac.ServerAddress = addr + configFile.AuthConfigs[addr] = ac + } + return nil +} + +// ContainsAuth returns whether there is authentication configured +// in this file or not. +func (configFile *ConfigFile) ContainsAuth() bool { + return configFile.CredentialsStore != "" || + len(configFile.CredentialHelpers) > 0 || + len(configFile.AuthConfigs) > 0 +} + +// SaveToWriter encodes and writes out all the authorization information to +// the given writer +func (configFile *ConfigFile) SaveToWriter(writer io.Writer) error { + // Encode sensitive data into a new/temp struct + tmpAuthConfigs := make(map[string]types.AuthConfig, len(configFile.AuthConfigs)) + for k, authConfig := range configFile.AuthConfigs { + authCopy := authConfig + // encode and save the authstring, while blanking out the original fields + authCopy.Auth = encodeAuth(&authCopy) + authCopy.Username = "" + authCopy.Password = "" + authCopy.ServerAddress = "" + tmpAuthConfigs[k] = authCopy + } + + saveAuthConfigs := configFile.AuthConfigs + configFile.AuthConfigs = tmpAuthConfigs + defer func() { configFile.AuthConfigs = saveAuthConfigs }() + + data, err := json.MarshalIndent(configFile, "", "\t") + if err != nil { + return err + } + _, err = writer.Write(data) + return err +} + +// Save encodes and writes out all the authorization information +func (configFile *ConfigFile) Save() error { + if configFile.Filename == "" { + return fmt.Errorf("Can't save config with empty filename") + } + + if err := os.MkdirAll(filepath.Dir(configFile.Filename), 0700); err != nil { + return err + } + f, err := os.OpenFile(configFile.Filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return err + } + defer f.Close() + return configFile.SaveToWriter(f) +} + +// encodeAuth creates a base64 encoded string to containing authorization information +func encodeAuth(authConfig *types.AuthConfig) string { + if authConfig.Username == "" && authConfig.Password == "" { + return "" + } + + authStr := authConfig.Username + ":" + authConfig.Password + msg := []byte(authStr) + encoded := make([]byte, base64.StdEncoding.EncodedLen(len(msg))) + base64.StdEncoding.Encode(encoded, msg) + return string(encoded) +} + +// decodeAuth decodes a base64 encoded string and returns username and password +func decodeAuth(authStr string) (string, string, error) { + if authStr == "" { + return "", "", nil + } + + decLen := base64.StdEncoding.DecodedLen(len(authStr)) + decoded := make([]byte, decLen) + authByte := []byte(authStr) + n, err := base64.StdEncoding.Decode(decoded, authByte) + if err != nil { + return "", "", err + } + if n > decLen { + return "", "", fmt.Errorf("Something went wrong decoding auth config") + } + arr := strings.SplitN(string(decoded), ":", 2) + if len(arr) != 2 { + return "", "", fmt.Errorf("Invalid auth configuration file") + } + password := strings.Trim(arr[1], "\x00") + return arr[0], password, nil +} diff --git a/components/cli/config/configfile/file_test.go b/components/cli/config/configfile/file_test.go new file mode 100644 index 0000000000..435797f681 --- /dev/null +++ b/components/cli/config/configfile/file_test.go @@ -0,0 +1,27 @@ +package configfile + +import ( + "testing" + + "github.com/docker/docker/api/types" +) + +func TestEncodeAuth(t *testing.T) { + newAuthConfig := &types.AuthConfig{Username: "ken", Password: "test"} + authStr := encodeAuth(newAuthConfig) + decAuthConfig := &types.AuthConfig{} + var err error + decAuthConfig.Username, decAuthConfig.Password, err = decodeAuth(authStr) + if err != nil { + t.Fatal(err) + } + if newAuthConfig.Username != decAuthConfig.Username { + t.Fatal("Encode Username doesn't match decoded Username") + } + if newAuthConfig.Password != decAuthConfig.Password { + t.Fatal("Encode Password doesn't match decoded Password") + } + if authStr != "a2VuOnRlc3Q=" { + t.Fatal("AuthString encoding isn't correct.") + } +} diff --git a/components/cli/config/credentials/credentials.go b/components/cli/config/credentials/credentials.go new file mode 100644 index 0000000000..ca874cac51 --- /dev/null +++ b/components/cli/config/credentials/credentials.go @@ -0,0 +1,17 @@ +package credentials + +import ( + "github.com/docker/docker/api/types" +) + +// Store is the interface that any credentials store must implement. +type Store interface { + // Erase removes credentials from the store for a given server. + Erase(serverAddress string) error + // Get retrieves credentials from the store for a given server. + Get(serverAddress string) (types.AuthConfig, error) + // GetAll retrieves all the credentials from the store. + GetAll() (map[string]types.AuthConfig, error) + // Store saves credentials in the store. + Store(authConfig types.AuthConfig) error +} diff --git a/components/cli/config/credentials/default_store.go b/components/cli/config/credentials/default_store.go new file mode 100644 index 0000000000..263a4ea879 --- /dev/null +++ b/components/cli/config/credentials/default_store.go @@ -0,0 +1,22 @@ +package credentials + +import ( + "os/exec" + + "github.com/docker/docker/cli/config/configfile" +) + +// DetectDefaultStore sets the default credentials store +// if the host includes the default store helper program. +func DetectDefaultStore(c *configfile.ConfigFile) { + if c.CredentialsStore != "" { + // user defined + return + } + + if defaultCredentialsStore != "" { + if _, err := exec.LookPath(remoteCredentialsPrefix + defaultCredentialsStore); err == nil { + c.CredentialsStore = defaultCredentialsStore + } + } +} diff --git a/components/cli/config/credentials/default_store_darwin.go b/components/cli/config/credentials/default_store_darwin.go new file mode 100644 index 0000000000..63e8ed4010 --- /dev/null +++ b/components/cli/config/credentials/default_store_darwin.go @@ -0,0 +1,3 @@ +package credentials + +const defaultCredentialsStore = "osxkeychain" diff --git a/components/cli/config/credentials/default_store_linux.go b/components/cli/config/credentials/default_store_linux.go new file mode 100644 index 0000000000..864c540f6c --- /dev/null +++ b/components/cli/config/credentials/default_store_linux.go @@ -0,0 +1,3 @@ +package credentials + +const defaultCredentialsStore = "secretservice" diff --git a/components/cli/config/credentials/default_store_unsupported.go b/components/cli/config/credentials/default_store_unsupported.go new file mode 100644 index 0000000000..519ef53dcd --- /dev/null +++ b/components/cli/config/credentials/default_store_unsupported.go @@ -0,0 +1,5 @@ +// +build !windows,!darwin,!linux + +package credentials + +const defaultCredentialsStore = "" diff --git a/components/cli/config/credentials/default_store_windows.go b/components/cli/config/credentials/default_store_windows.go new file mode 100644 index 0000000000..fb6a9745cf --- /dev/null +++ b/components/cli/config/credentials/default_store_windows.go @@ -0,0 +1,3 @@ +package credentials + +const defaultCredentialsStore = "wincred" diff --git a/components/cli/config/credentials/file_store.go b/components/cli/config/credentials/file_store.go new file mode 100644 index 0000000000..3cab4a448b --- /dev/null +++ b/components/cli/config/credentials/file_store.go @@ -0,0 +1,53 @@ +package credentials + +import ( + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli/config/configfile" + "github.com/docker/docker/registry" +) + +// fileStore implements a credentials store using +// the docker configuration file to keep the credentials in plain text. +type fileStore struct { + file *configfile.ConfigFile +} + +// NewFileStore creates a new file credentials store. +func NewFileStore(file *configfile.ConfigFile) Store { + return &fileStore{ + file: file, + } +} + +// Erase removes the given credentials from the file store. +func (c *fileStore) Erase(serverAddress string) error { + delete(c.file.AuthConfigs, serverAddress) + return c.file.Save() +} + +// Get retrieves credentials for a specific server from the file store. +func (c *fileStore) Get(serverAddress string) (types.AuthConfig, error) { + authConfig, ok := c.file.AuthConfigs[serverAddress] + if !ok { + // Maybe they have a legacy config file, we will iterate the keys converting + // them to the new format and testing + for r, ac := range c.file.AuthConfigs { + if serverAddress == registry.ConvertToHostname(r) { + return ac, nil + } + } + + authConfig = types.AuthConfig{} + } + return authConfig, nil +} + +func (c *fileStore) GetAll() (map[string]types.AuthConfig, error) { + return c.file.AuthConfigs, nil +} + +// Store saves the given credentials in the file store. +func (c *fileStore) Store(authConfig types.AuthConfig) error { + c.file.AuthConfigs[authConfig.ServerAddress] = authConfig + return c.file.Save() +} diff --git a/components/cli/config/credentials/file_store_test.go b/components/cli/config/credentials/file_store_test.go new file mode 100644 index 0000000000..b6bfaa0607 --- /dev/null +++ b/components/cli/config/credentials/file_store_test.go @@ -0,0 +1,139 @@ +package credentials + +import ( + "io/ioutil" + "testing" + + "github.com/docker/docker/api/types" + cliconfig "github.com/docker/docker/cli/config" + "github.com/docker/docker/cli/config/configfile" +) + +func newConfigFile(auths map[string]types.AuthConfig) *configfile.ConfigFile { + tmp, _ := ioutil.TempFile("", "docker-test") + name := tmp.Name() + tmp.Close() + + c := cliconfig.NewConfigFile(name) + c.AuthConfigs = auths + return c +} + +func TestFileStoreAddCredentials(t *testing.T) { + f := newConfigFile(make(map[string]types.AuthConfig)) + + s := NewFileStore(f) + err := s.Store(types.AuthConfig{ + Auth: "super_secret_token", + Email: "foo@example.com", + ServerAddress: "https://example.com", + }) + + if err != nil { + t.Fatal(err) + } + + if len(f.AuthConfigs) != 1 { + t.Fatalf("expected 1 auth config, got %d", len(f.AuthConfigs)) + } + + a, ok := f.AuthConfigs["https://example.com"] + if !ok { + t.Fatalf("expected auth for https://example.com, got %v", f.AuthConfigs) + } + if a.Auth != "super_secret_token" { + t.Fatalf("expected auth `super_secret_token`, got %s", a.Auth) + } + if a.Email != "foo@example.com" { + t.Fatalf("expected email `foo@example.com`, got %s", a.Email) + } +} + +func TestFileStoreGet(t *testing.T) { + f := newConfigFile(map[string]types.AuthConfig{ + "https://example.com": { + Auth: "super_secret_token", + Email: "foo@example.com", + ServerAddress: "https://example.com", + }, + }) + + s := NewFileStore(f) + a, err := s.Get("https://example.com") + if err != nil { + t.Fatal(err) + } + if a.Auth != "super_secret_token" { + t.Fatalf("expected auth `super_secret_token`, got %s", a.Auth) + } + if a.Email != "foo@example.com" { + t.Fatalf("expected email `foo@example.com`, got %s", a.Email) + } +} + +func TestFileStoreGetAll(t *testing.T) { + s1 := "https://example.com" + s2 := "https://example2.com" + f := newConfigFile(map[string]types.AuthConfig{ + s1: { + Auth: "super_secret_token", + Email: "foo@example.com", + ServerAddress: "https://example.com", + }, + s2: { + Auth: "super_secret_token2", + Email: "foo@example2.com", + ServerAddress: "https://example2.com", + }, + }) + + s := NewFileStore(f) + as, err := s.GetAll() + if err != nil { + t.Fatal(err) + } + if len(as) != 2 { + t.Fatalf("wanted 2, got %d", len(as)) + } + if as[s1].Auth != "super_secret_token" { + t.Fatalf("expected auth `super_secret_token`, got %s", as[s1].Auth) + } + if as[s1].Email != "foo@example.com" { + t.Fatalf("expected email `foo@example.com`, got %s", as[s1].Email) + } + if as[s2].Auth != "super_secret_token2" { + t.Fatalf("expected auth `super_secret_token2`, got %s", as[s2].Auth) + } + if as[s2].Email != "foo@example2.com" { + t.Fatalf("expected email `foo@example2.com`, got %s", as[s2].Email) + } +} + +func TestFileStoreErase(t *testing.T) { + f := newConfigFile(map[string]types.AuthConfig{ + "https://example.com": { + Auth: "super_secret_token", + Email: "foo@example.com", + ServerAddress: "https://example.com", + }, + }) + + s := NewFileStore(f) + err := s.Erase("https://example.com") + if err != nil { + t.Fatal(err) + } + + // file store never returns errors, check that the auth config is empty + a, err := s.Get("https://example.com") + if err != nil { + t.Fatal(err) + } + + if a.Auth != "" { + t.Fatalf("expected empty auth token, got %s", a.Auth) + } + if a.Email != "" { + t.Fatalf("expected empty email, got %s", a.Email) + } +} diff --git a/components/cli/config/credentials/native_store.go b/components/cli/config/credentials/native_store.go new file mode 100644 index 0000000000..9e0ab7f0f8 --- /dev/null +++ b/components/cli/config/credentials/native_store.go @@ -0,0 +1,144 @@ +package credentials + +import ( + "github.com/docker/docker-credential-helpers/client" + "github.com/docker/docker-credential-helpers/credentials" + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli/config/configfile" +) + +const ( + remoteCredentialsPrefix = "docker-credential-" + tokenUsername = "" +) + +// nativeStore implements a credentials store +// using native keychain to keep credentials secure. +// It piggybacks into a file store to keep users' emails. +type nativeStore struct { + programFunc client.ProgramFunc + fileStore Store +} + +// NewNativeStore creates a new native store that +// uses a remote helper program to manage credentials. +func NewNativeStore(file *configfile.ConfigFile, helperSuffix string) Store { + name := remoteCredentialsPrefix + helperSuffix + return &nativeStore{ + programFunc: client.NewShellProgramFunc(name), + fileStore: NewFileStore(file), + } +} + +// Erase removes the given credentials from the native store. +func (c *nativeStore) Erase(serverAddress string) error { + if err := client.Erase(c.programFunc, serverAddress); err != nil { + return err + } + + // Fallback to plain text store to remove email + return c.fileStore.Erase(serverAddress) +} + +// Get retrieves credentials for a specific server from the native store. +func (c *nativeStore) Get(serverAddress string) (types.AuthConfig, error) { + // load user email if it exist or an empty auth config. + auth, _ := c.fileStore.Get(serverAddress) + + creds, err := c.getCredentialsFromStore(serverAddress) + if err != nil { + return auth, err + } + auth.Username = creds.Username + auth.IdentityToken = creds.IdentityToken + auth.Password = creds.Password + + return auth, nil +} + +// GetAll retrieves all the credentials from the native store. +func (c *nativeStore) GetAll() (map[string]types.AuthConfig, error) { + auths, err := c.listCredentialsInStore() + if err != nil { + return nil, err + } + + // Emails are only stored in the file store. + // This call can be safely eliminated when emails are removed. + fileConfigs, _ := c.fileStore.GetAll() + + authConfigs := make(map[string]types.AuthConfig) + for registry := range auths { + creds, err := c.getCredentialsFromStore(registry) + if err != nil { + return nil, err + } + ac, _ := fileConfigs[registry] // might contain Email + ac.Username = creds.Username + ac.Password = creds.Password + ac.IdentityToken = creds.IdentityToken + authConfigs[registry] = ac + } + + return authConfigs, nil +} + +// Store saves the given credentials in the file store. +func (c *nativeStore) Store(authConfig types.AuthConfig) error { + if err := c.storeCredentialsInStore(authConfig); err != nil { + return err + } + authConfig.Username = "" + authConfig.Password = "" + authConfig.IdentityToken = "" + + // Fallback to old credential in plain text to save only the email + return c.fileStore.Store(authConfig) +} + +// storeCredentialsInStore executes the command to store the credentials in the native store. +func (c *nativeStore) storeCredentialsInStore(config types.AuthConfig) error { + creds := &credentials.Credentials{ + ServerURL: config.ServerAddress, + Username: config.Username, + Secret: config.Password, + } + + if config.IdentityToken != "" { + creds.Username = tokenUsername + creds.Secret = config.IdentityToken + } + + return client.Store(c.programFunc, creds) +} + +// getCredentialsFromStore executes the command to get the credentials from the native store. +func (c *nativeStore) getCredentialsFromStore(serverAddress string) (types.AuthConfig, error) { + var ret types.AuthConfig + + creds, err := client.Get(c.programFunc, serverAddress) + if err != nil { + if credentials.IsErrCredentialsNotFound(err) { + // do not return an error if the credentials are not + // in the keyckain. Let docker ask for new credentials. + return ret, nil + } + return ret, err + } + + if creds.Username == tokenUsername { + ret.IdentityToken = creds.Secret + } else { + ret.Password = creds.Secret + ret.Username = creds.Username + } + + ret.ServerAddress = serverAddress + return ret, nil +} + +// listCredentialsInStore returns a listing of stored credentials as a map of +// URL -> username. +func (c *nativeStore) listCredentialsInStore() (map[string]string, error) { + return client.List(c.programFunc) +} diff --git a/components/cli/config/credentials/native_store_test.go b/components/cli/config/credentials/native_store_test.go new file mode 100644 index 0000000000..7664faf9e1 --- /dev/null +++ b/components/cli/config/credentials/native_store_test.go @@ -0,0 +1,355 @@ +package credentials + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "strings" + "testing" + + "github.com/docker/docker-credential-helpers/client" + "github.com/docker/docker-credential-helpers/credentials" + "github.com/docker/docker/api/types" +) + +const ( + validServerAddress = "https://index.docker.io/v1" + validServerAddress2 = "https://example.com:5002" + invalidServerAddress = "https://foobar.example.com" + missingCredsAddress = "https://missing.docker.io/v1" +) + +var errCommandExited = fmt.Errorf("exited 1") + +// mockCommand simulates interactions between the docker client and a remote +// credentials helper. +// Unit tests inject this mocked command into the remote to control execution. +type mockCommand struct { + arg string + input io.Reader +} + +// Output returns responses from the remote credentials helper. +// It mocks those responses based in the input in the mock. +func (m *mockCommand) Output() ([]byte, error) { + in, err := ioutil.ReadAll(m.input) + if err != nil { + return nil, err + } + inS := string(in) + + switch m.arg { + case "erase": + switch inS { + case validServerAddress: + return nil, nil + default: + return []byte("program failed"), errCommandExited + } + case "get": + switch inS { + case validServerAddress: + return []byte(`{"Username": "foo", "Secret": "bar"}`), nil + case validServerAddress2: + return []byte(`{"Username": "", "Secret": "abcd1234"}`), nil + case missingCredsAddress: + return []byte(credentials.NewErrCredentialsNotFound().Error()), errCommandExited + case invalidServerAddress: + return []byte("program failed"), errCommandExited + } + case "store": + var c credentials.Credentials + err := json.NewDecoder(strings.NewReader(inS)).Decode(&c) + if err != nil { + return []byte("program failed"), errCommandExited + } + switch c.ServerURL { + case validServerAddress: + return nil, nil + default: + return []byte("program failed"), errCommandExited + } + case "list": + return []byte(fmt.Sprintf(`{"%s": "%s", "%s": "%s"}`, validServerAddress, "foo", validServerAddress2, "")), nil + } + + return []byte(fmt.Sprintf("unknown argument %q with %q", m.arg, inS)), errCommandExited +} + +// Input sets the input to send to a remote credentials helper. +func (m *mockCommand) Input(in io.Reader) { + m.input = in +} + +func mockCommandFn(args ...string) client.Program { + return &mockCommand{ + arg: args[0], + } +} + +func TestNativeStoreAddCredentials(t *testing.T) { + f := newConfigFile(make(map[string]types.AuthConfig)) + f.CredentialsStore = "mock" + + s := &nativeStore{ + programFunc: mockCommandFn, + fileStore: NewFileStore(f), + } + err := s.Store(types.AuthConfig{ + Username: "foo", + Password: "bar", + Email: "foo@example.com", + ServerAddress: validServerAddress, + }) + + if err != nil { + t.Fatal(err) + } + + if len(f.AuthConfigs) != 1 { + t.Fatalf("expected 1 auth config, got %d", len(f.AuthConfigs)) + } + + a, ok := f.AuthConfigs[validServerAddress] + if !ok { + t.Fatalf("expected auth for %s, got %v", validServerAddress, f.AuthConfigs) + } + if a.Auth != "" { + t.Fatalf("expected auth to be empty, got %s", a.Auth) + } + if a.Username != "" { + t.Fatalf("expected username to be empty, got %s", a.Username) + } + if a.Password != "" { + t.Fatalf("expected password to be empty, got %s", a.Password) + } + if a.IdentityToken != "" { + t.Fatalf("expected identity token to be empty, got %s", a.IdentityToken) + } + if a.Email != "foo@example.com" { + t.Fatalf("expected email `foo@example.com`, got %s", a.Email) + } +} + +func TestNativeStoreAddInvalidCredentials(t *testing.T) { + f := newConfigFile(make(map[string]types.AuthConfig)) + f.CredentialsStore = "mock" + + s := &nativeStore{ + programFunc: mockCommandFn, + fileStore: NewFileStore(f), + } + err := s.Store(types.AuthConfig{ + Username: "foo", + Password: "bar", + Email: "foo@example.com", + ServerAddress: invalidServerAddress, + }) + + if err == nil { + t.Fatal("expected error, got nil") + } + + if !strings.Contains(err.Error(), "program failed") { + t.Fatalf("expected `program failed`, got %v", err) + } + + if len(f.AuthConfigs) != 0 { + t.Fatalf("expected 0 auth config, got %d", len(f.AuthConfigs)) + } +} + +func TestNativeStoreGet(t *testing.T) { + f := newConfigFile(map[string]types.AuthConfig{ + validServerAddress: { + Email: "foo@example.com", + }, + }) + f.CredentialsStore = "mock" + + s := &nativeStore{ + programFunc: mockCommandFn, + fileStore: NewFileStore(f), + } + a, err := s.Get(validServerAddress) + if err != nil { + t.Fatal(err) + } + + if a.Username != "foo" { + t.Fatalf("expected username `foo`, got %s", a.Username) + } + if a.Password != "bar" { + t.Fatalf("expected password `bar`, got %s", a.Password) + } + if a.IdentityToken != "" { + t.Fatalf("expected identity token to be empty, got %s", a.IdentityToken) + } + if a.Email != "foo@example.com" { + t.Fatalf("expected email `foo@example.com`, got %s", a.Email) + } +} + +func TestNativeStoreGetIdentityToken(t *testing.T) { + f := newConfigFile(map[string]types.AuthConfig{ + validServerAddress2: { + Email: "foo@example2.com", + }, + }) + f.CredentialsStore = "mock" + + s := &nativeStore{ + programFunc: mockCommandFn, + fileStore: NewFileStore(f), + } + a, err := s.Get(validServerAddress2) + if err != nil { + t.Fatal(err) + } + + if a.Username != "" { + t.Fatalf("expected username to be empty, got %s", a.Username) + } + if a.Password != "" { + t.Fatalf("expected password to be empty, got %s", a.Password) + } + if a.IdentityToken != "abcd1234" { + t.Fatalf("expected identity token `abcd1234`, got %s", a.IdentityToken) + } + if a.Email != "foo@example2.com" { + t.Fatalf("expected email `foo@example2.com`, got %s", a.Email) + } +} + +func TestNativeStoreGetAll(t *testing.T) { + f := newConfigFile(map[string]types.AuthConfig{ + validServerAddress: { + Email: "foo@example.com", + }, + }) + f.CredentialsStore = "mock" + + s := &nativeStore{ + programFunc: mockCommandFn, + fileStore: NewFileStore(f), + } + as, err := s.GetAll() + if err != nil { + t.Fatal(err) + } + + if len(as) != 2 { + t.Fatalf("wanted 2, got %d", len(as)) + } + + if as[validServerAddress].Username != "foo" { + t.Fatalf("expected username `foo` for %s, got %s", validServerAddress, as[validServerAddress].Username) + } + if as[validServerAddress].Password != "bar" { + t.Fatalf("expected password `bar` for %s, got %s", validServerAddress, as[validServerAddress].Password) + } + if as[validServerAddress].IdentityToken != "" { + t.Fatalf("expected identity to be empty for %s, got %s", validServerAddress, as[validServerAddress].IdentityToken) + } + if as[validServerAddress].Email != "foo@example.com" { + t.Fatalf("expected email `foo@example.com` for %s, got %s", validServerAddress, as[validServerAddress].Email) + } + if as[validServerAddress2].Username != "" { + t.Fatalf("expected username to be empty for %s, got %s", validServerAddress2, as[validServerAddress2].Username) + } + if as[validServerAddress2].Password != "" { + t.Fatalf("expected password to be empty for %s, got %s", validServerAddress2, as[validServerAddress2].Password) + } + if as[validServerAddress2].IdentityToken != "abcd1234" { + t.Fatalf("expected identity token `abcd1324` for %s, got %s", validServerAddress2, as[validServerAddress2].IdentityToken) + } + if as[validServerAddress2].Email != "" { + t.Fatalf("expected no email for %s, got %s", validServerAddress2, as[validServerAddress2].Email) + } +} + +func TestNativeStoreGetMissingCredentials(t *testing.T) { + f := newConfigFile(map[string]types.AuthConfig{ + validServerAddress: { + Email: "foo@example.com", + }, + }) + f.CredentialsStore = "mock" + + s := &nativeStore{ + programFunc: mockCommandFn, + fileStore: NewFileStore(f), + } + _, err := s.Get(missingCredsAddress) + if err != nil { + // missing credentials do not produce an error + t.Fatal(err) + } +} + +func TestNativeStoreGetInvalidAddress(t *testing.T) { + f := newConfigFile(map[string]types.AuthConfig{ + validServerAddress: { + Email: "foo@example.com", + }, + }) + f.CredentialsStore = "mock" + + s := &nativeStore{ + programFunc: mockCommandFn, + fileStore: NewFileStore(f), + } + _, err := s.Get(invalidServerAddress) + if err == nil { + t.Fatal("expected error, got nil") + } + + if !strings.Contains(err.Error(), "program failed") { + t.Fatalf("expected `program failed`, got %v", err) + } +} + +func TestNativeStoreErase(t *testing.T) { + f := newConfigFile(map[string]types.AuthConfig{ + validServerAddress: { + Email: "foo@example.com", + }, + }) + f.CredentialsStore = "mock" + + s := &nativeStore{ + programFunc: mockCommandFn, + fileStore: NewFileStore(f), + } + err := s.Erase(validServerAddress) + if err != nil { + t.Fatal(err) + } + + if len(f.AuthConfigs) != 0 { + t.Fatalf("expected 0 auth configs, got %d", len(f.AuthConfigs)) + } +} + +func TestNativeStoreEraseInvalidAddress(t *testing.T) { + f := newConfigFile(map[string]types.AuthConfig{ + validServerAddress: { + Email: "foo@example.com", + }, + }) + f.CredentialsStore = "mock" + + s := &nativeStore{ + programFunc: mockCommandFn, + fileStore: NewFileStore(f), + } + err := s.Erase(invalidServerAddress) + if err == nil { + t.Fatal("expected error, got nil") + } + + if !strings.Contains(err.Error(), "program failed") { + t.Fatalf("expected `program failed`, got %v", err) + } +} diff --git a/components/cli/flags/common.go b/components/cli/flags/common.go index 490c2922fc..9d3245c99c 100644 --- a/components/cli/flags/common.go +++ b/components/cli/flags/common.go @@ -6,7 +6,7 @@ import ( "path/filepath" "github.com/Sirupsen/logrus" - "github.com/docker/docker/cliconfig" + cliconfig "github.com/docker/docker/cli/config" "github.com/docker/docker/opts" "github.com/docker/go-connections/tlsconfig" "github.com/spf13/pflag" @@ -49,7 +49,7 @@ func NewCommonOptions() *CommonOptions { // InstallFlags adds flags for the common options on the FlagSet func (commonOpts *CommonOptions) InstallFlags(flags *pflag.FlagSet) { if dockerCertPath == "" { - dockerCertPath = cliconfig.ConfigDir() + dockerCertPath = cliconfig.Dir() } flags.BoolVarP(&commonOpts.Debug, "debug", "D", false, "Enable debug mode") diff --git a/components/cli/trust/trust.go b/components/cli/trust/trust.go index 0f3482f2d7..495bfa344d 100644 --- a/components/cli/trust/trust.go +++ b/components/cli/trust/trust.go @@ -18,7 +18,7 @@ import ( "github.com/docker/docker/api/types" registrytypes "github.com/docker/docker/api/types/registry" "github.com/docker/docker/cli/command" - "github.com/docker/docker/cliconfig" + cliconfig "github.com/docker/docker/cli/config" "github.com/docker/docker/registry" "github.com/docker/go-connections/tlsconfig" "github.com/docker/notary" @@ -37,7 +37,7 @@ var ( ) func trustDirectory() string { - return filepath.Join(cliconfig.ConfigDir(), "trust") + return filepath.Join(cliconfig.Dir(), "trust") } // certificateDirectory returns the directory containing @@ -49,7 +49,7 @@ func certificateDirectory(server string) (string, error) { return "", err } - return filepath.Join(cliconfig.ConfigDir(), "tls", u.Host), nil + return filepath.Join(cliconfig.Dir(), "tls", u.Host), nil } // Server returns the base URL for the trust server. From aeae0b960a8cc6fd0e55942edca09646b497f4bd Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Sun, 25 Dec 2016 20:31:52 +0100 Subject: [PATCH 491/978] Move package cliconfig to cli/config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I felt it made more sence 👼 Signed-off-by: Vincent Demeester Upstream-commit: d29175b73c2628259f5442aed0a820e5686f100b Component: cli --- components/cli/docker.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/cli/docker.go b/components/cli/docker.go index f4033738b7..685f565c8d 100644 --- a/components/cli/docker.go +++ b/components/cli/docker.go @@ -10,9 +10,9 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/commands" + cliconfig "github.com/docker/docker/cli/config" "github.com/docker/docker/cli/debug" cliflags "github.com/docker/docker/cli/flags" - "github.com/docker/docker/cliconfig" "github.com/docker/docker/dockerversion" "github.com/docker/docker/pkg/term" "github.com/spf13/cobra" @@ -75,7 +75,7 @@ func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command { flags = cmd.Flags() flags.BoolVarP(&opts.Version, "version", "v", false, "Print version information and quit") - flags.StringVar(&opts.ConfigDir, "config", cliconfig.ConfigDir(), "Location of client config files") + flags.StringVar(&opts.ConfigDir, "config", cliconfig.Dir(), "Location of client config files") opts.Common.InstallFlags(flags) cmd.SetOutput(dockerCli.Out()) @@ -126,7 +126,7 @@ func dockerPreRun(opts *cliflags.ClientOptions) { cliflags.SetLogLevel(opts.Common.LogLevel) if opts.ConfigDir != "" { - cliconfig.SetConfigDir(opts.ConfigDir) + cliconfig.SetDir(opts.ConfigDir) } if opts.Common.Debug { From f820fb530f0c8f157a07b75e1ee864965059e47c Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sun, 25 Dec 2016 01:05:37 +0100 Subject: [PATCH 492/978] Minor cleanups in cli/command/container This change does some minor cleanups in the cli/command/container package; - sort imports - replace `fmt.Fprintf()` with `fmt.Fprintln()` if no formatting is used - replace `fmt.Errorf()` with `errors.New()` if no formatting is used - remove some redundant `else`'s Signed-off-by: Sebastiaan van Stijn Upstream-commit: f459796896f5e2074486540ae34cebcd015c6d4b Component: cli --- components/cli/command/container/attach.go | 9 ++++---- components/cli/command/container/cmd.go | 3 +-- components/cli/command/container/commit.go | 3 +-- components/cli/command/container/cp.go | 14 ++++++------ components/cli/command/container/create.go | 24 ++++++++++----------- components/cli/command/container/diff.go | 8 +++---- components/cli/command/container/exec.go | 7 +++--- components/cli/command/container/export.go | 3 +-- components/cli/command/container/inspect.go | 3 +-- components/cli/command/container/kill.go | 8 +++---- components/cli/command/container/list.go | 3 +-- components/cli/command/container/logs.go | 3 +-- components/cli/command/container/pause.go | 10 ++++----- components/cli/command/container/port.go | 3 +-- components/cli/command/container/prune.go | 3 +-- components/cli/command/container/rename.go | 8 +++---- components/cli/command/container/restart.go | 10 ++++----- components/cli/command/container/rm.go | 16 +++++++------- components/cli/command/container/run.go | 21 +++++++++--------- components/cli/command/container/start.go | 18 ++++++++-------- components/cli/command/container/stats.go | 6 +++--- components/cli/command/container/stop.go | 10 ++++----- components/cli/command/container/top.go | 3 +-- components/cli/command/container/unpause.go | 10 ++++----- components/cli/command/container/update.go | 12 +++++------ components/cli/command/container/utils.go | 3 +-- components/cli/command/container/wait.go | 10 ++++----- 27 files changed, 109 insertions(+), 122 deletions(-) diff --git a/components/cli/command/container/attach.go b/components/cli/command/container/attach.go index 31bb109344..073914dc35 100644 --- a/components/cli/command/container/attach.go +++ b/components/cli/command/container/attach.go @@ -1,18 +1,17 @@ package container import ( - "fmt" + "errors" "io" "net/http/httputil" - "golang.org/x/net/context" - "github.com/Sirupsen/logrus" "github.com/docker/docker/api/types" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/pkg/signal" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type attachOptions struct { @@ -54,11 +53,11 @@ func runAttach(dockerCli *command.DockerCli, opts *attachOptions) error { } if !c.State.Running { - return fmt.Errorf("You cannot attach to a stopped container, start it first") + return errors.New("You cannot attach to a stopped container, start it first") } if c.State.Paused { - return fmt.Errorf("You cannot attach to a paused container, unpause it first") + return errors.New("You cannot attach to a paused container, unpause it first") } if err := dockerCli.In().CheckTty(!opts.noStdin, c.Config.Tty); err != nil { diff --git a/components/cli/command/container/cmd.go b/components/cli/command/container/cmd.go index 3e9b4880ac..b78411e0a3 100644 --- a/components/cli/command/container/cmd.go +++ b/components/cli/command/container/cmd.go @@ -1,10 +1,9 @@ package container import ( - "github.com/spf13/cobra" - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" ) // NewContainerCommand returns a cobra command for `container` subcommands diff --git a/components/cli/command/container/commit.go b/components/cli/command/container/commit.go index cf8d0102a6..8f67d96d87 100644 --- a/components/cli/command/container/commit.go +++ b/components/cli/command/container/commit.go @@ -3,13 +3,12 @@ package container import ( "fmt" - "golang.org/x/net/context" - "github.com/docker/docker/api/types" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" dockeropts "github.com/docker/docker/opts" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type commitOptions struct { diff --git a/components/cli/command/container/cp.go b/components/cli/command/container/cp.go index 17ab2accf9..8df850b360 100644 --- a/components/cli/command/container/cp.go +++ b/components/cli/command/container/cp.go @@ -1,20 +1,20 @@ package container import ( + "errors" "fmt" "io" "os" "path/filepath" "strings" - "golang.org/x/net/context" - "github.com/docker/docker/api/types" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/system" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type copyOptions struct { @@ -53,10 +53,10 @@ func NewCopyCommand(dockerCli *command.DockerCli) *cobra.Command { Args: cli.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { if args[0] == "" { - return fmt.Errorf("source can not be empty") + return errors.New("source can not be empty") } if args[1] == "" { - return fmt.Errorf("destination can not be empty") + return errors.New("destination can not be empty") } opts.source = args[0] opts.destination = args[1] @@ -96,10 +96,10 @@ func runCopy(dockerCli *command.DockerCli, opts copyOptions) error { return copyToContainer(ctx, dockerCli, srcPath, dstContainer, dstPath, cpParam) case acrossContainers: // Copying between containers isn't supported. - return fmt.Errorf("copying between containers is not supported") + return errors.New("copying between containers is not supported") default: // User didn't specify any container. - return fmt.Errorf("must specify at least one container source") + return errors.New("must specify at least one container source") } } @@ -227,7 +227,7 @@ func copyToContainer(ctx context.Context, dockerCli *command.DockerCli, srcPath, content = os.Stdin resolvedDstPath = dstInfo.Path if !dstInfo.IsDir { - return fmt.Errorf("destination %q must be a directory", fmt.Sprintf("%s:%s", dstContainer, dstPath)) + return fmt.Errorf("destination \"%s:%s\" must be a directory", dstContainer, dstPath) } } else { // Prepare source copy info. diff --git a/components/cli/command/container/create.go b/components/cli/command/container/create.go index 7dc644d28c..bc4fde5713 100644 --- a/components/cli/command/container/create.go +++ b/components/cli/command/container/create.go @@ -5,22 +5,21 @@ import ( "io" "os" - "golang.org/x/net/context" - - "github.com/docker/docker/cli" - "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/command/image" - "github.com/docker/docker/pkg/jsonmessage" - // FIXME migrate to docker/distribution/reference "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" networktypes "github.com/docker/docker/api/types/network" + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/image" apiclient "github.com/docker/docker/client" + "github.com/docker/docker/pkg/jsonmessage" + // FIXME migrate to docker/distribution/reference "github.com/docker/docker/reference" "github.com/docker/docker/registry" runconfigopts "github.com/docker/docker/runconfig/opts" "github.com/spf13/cobra" "github.com/spf13/pflag" + "golang.org/x/net/context" ) type createOptions struct { @@ -69,7 +68,7 @@ func runCreate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *createO if err != nil { return err } - fmt.Fprintf(dockerCli.Out(), "%s\n", response.ID) + fmt.Fprintln(dockerCli.Out(), response.ID) return nil } @@ -118,10 +117,11 @@ type cidFile struct { func (cid *cidFile) Close() error { cid.file.Close() - if !cid.written { - if err := os.Remove(cid.path); err != nil { - return fmt.Errorf("failed to remove the CID file '%s': %s \n", cid.path, err) - } + if cid.written { + return nil + } + if err := os.Remove(cid.path); err != nil { + return fmt.Errorf("failed to remove the CID file '%s': %s \n", cid.path, err) } return nil diff --git a/components/cli/command/container/diff.go b/components/cli/command/container/diff.go index 168af74172..81260b05be 100644 --- a/components/cli/command/container/diff.go +++ b/components/cli/command/container/diff.go @@ -1,14 +1,14 @@ package container import ( + "errors" "fmt" - "golang.org/x/net/context" - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/pkg/archive" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type diffOptions struct { @@ -32,7 +32,7 @@ func NewDiffCommand(dockerCli *command.DockerCli) *cobra.Command { func runDiff(dockerCli *command.DockerCli, opts *diffOptions) error { if opts.container == "" { - return fmt.Errorf("Container name cannot be empty") + return errors.New("Container name cannot be empty") } ctx := context.Background() @@ -51,7 +51,7 @@ func runDiff(dockerCli *command.DockerCli, opts *diffOptions) error { case archive.ChangeDelete: kind = "D" } - fmt.Fprintf(dockerCli.Out(), "%s %s\n", kind, change.Path) + fmt.Fprintln(dockerCli.Out(), kind, change.Path) } return nil diff --git a/components/cli/command/container/exec.go b/components/cli/command/container/exec.go index f0381494e2..2253d44d54 100644 --- a/components/cli/command/container/exec.go +++ b/components/cli/command/container/exec.go @@ -4,8 +4,6 @@ import ( "fmt" "io" - "golang.org/x/net/context" - "github.com/Sirupsen/logrus" "github.com/docker/docker/api/types" "github.com/docker/docker/cli" @@ -15,6 +13,7 @@ import ( "github.com/docker/docker/pkg/promise" runconfigopts "github.com/docker/docker/runconfig/opts" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type execOptions struct { @@ -88,7 +87,7 @@ func runExec(dockerCli *command.DockerCli, opts *execOptions, container string, execID := response.ID if execID == "" { - fmt.Fprintf(dockerCli.Out(), "exec ID empty") + fmt.Fprintln(dockerCli.Out(), "exec ID empty") return nil } @@ -143,7 +142,7 @@ func runExec(dockerCli *command.DockerCli, opts *execOptions, container string, if execConfig.Tty && dockerCli.In().IsTerminal() { if err := MonitorTtySize(ctx, dockerCli, execID, true); err != nil { - fmt.Fprintf(dockerCli.Err(), "Error monitoring TTY size: %s\n", err) + fmt.Fprintln(dockerCli.Err(), "Error monitoring TTY size:", err) } } diff --git a/components/cli/command/container/export.go b/components/cli/command/container/export.go index 8fa2e5d77e..42f90bbaaa 100644 --- a/components/cli/command/container/export.go +++ b/components/cli/command/container/export.go @@ -4,11 +4,10 @@ import ( "errors" "io" - "golang.org/x/net/context" - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type exportOptions struct { diff --git a/components/cli/command/container/inspect.go b/components/cli/command/container/inspect.go index 08a8d244df..d08b38dc96 100644 --- a/components/cli/command/container/inspect.go +++ b/components/cli/command/container/inspect.go @@ -1,12 +1,11 @@ package container import ( - "golang.org/x/net/context" - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/inspect" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type inspectOptions struct { diff --git a/components/cli/command/container/kill.go b/components/cli/command/container/kill.go index 6da91a40e3..5c7f7ba14b 100644 --- a/components/cli/command/container/kill.go +++ b/components/cli/command/container/kill.go @@ -1,14 +1,14 @@ package container import ( + "errors" "fmt" "strings" - "golang.org/x/net/context" - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type killOptions struct { @@ -46,11 +46,11 @@ func runKill(dockerCli *command.DockerCli, opts *killOptions) error { if err := <-errChan; err != nil { errs = append(errs, err.Error()) } else { - fmt.Fprintf(dockerCli.Out(), "%s\n", name) + fmt.Fprintln(dockerCli.Out(), name) } } if len(errs) > 0 { - return fmt.Errorf("%s", strings.Join(errs, "\n")) + return errors.New(strings.Join(errs, "\n")) } return nil } diff --git a/components/cli/command/container/list.go b/components/cli/command/container/list.go index 5104e9b6c0..451c531a8b 100644 --- a/components/cli/command/container/list.go +++ b/components/cli/command/container/list.go @@ -3,8 +3,6 @@ package container import ( "io/ioutil" - "golang.org/x/net/context" - "github.com/docker/docker/api/types" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" @@ -12,6 +10,7 @@ import ( "github.com/docker/docker/opts" "github.com/docker/docker/pkg/templates" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type psOptions struct { diff --git a/components/cli/command/container/logs.go b/components/cli/command/container/logs.go index 9f1d9f90dd..2e1ce5205c 100644 --- a/components/cli/command/container/logs.go +++ b/components/cli/command/container/logs.go @@ -3,13 +3,12 @@ package container import ( "io" - "golang.org/x/net/context" - "github.com/docker/docker/api/types" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/pkg/stdcopy" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type logsOptions struct { diff --git a/components/cli/command/container/pause.go b/components/cli/command/container/pause.go index 6817cf60eb..7d42ca571e 100644 --- a/components/cli/command/container/pause.go +++ b/components/cli/command/container/pause.go @@ -1,14 +1,14 @@ package container import ( + "errors" "fmt" "strings" - "golang.org/x/net/context" - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type pauseOptions struct { @@ -38,12 +38,12 @@ func runPause(dockerCli *command.DockerCli, opts *pauseOptions) error { for _, container := range opts.containers { if err := <-errChan; err != nil { errs = append(errs, err.Error()) - } else { - fmt.Fprintf(dockerCli.Out(), "%s\n", container) + continue } + fmt.Fprintln(dockerCli.Out(), container) } if len(errs) > 0 { - return fmt.Errorf("%s", strings.Join(errs, "\n")) + return errors.New(strings.Join(errs, "\n")) } return nil } diff --git a/components/cli/command/container/port.go b/components/cli/command/container/port.go index ea15290145..dd1a6b245f 100644 --- a/components/cli/command/container/port.go +++ b/components/cli/command/container/port.go @@ -4,12 +4,11 @@ import ( "fmt" "strings" - "golang.org/x/net/context" - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/go-connections/nat" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type portOptions struct { diff --git a/components/cli/command/container/prune.go b/components/cli/command/container/prune.go index 064f4c08e0..0aad66e6ee 100644 --- a/components/cli/command/container/prune.go +++ b/components/cli/command/container/prune.go @@ -3,13 +3,12 @@ package container import ( "fmt" - "golang.org/x/net/context" - "github.com/docker/docker/api/types/filters" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" units "github.com/docker/go-units" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type pruneOptions struct { diff --git a/components/cli/command/container/rename.go b/components/cli/command/container/rename.go index 346fb7b3b9..a24711ad3f 100644 --- a/components/cli/command/container/rename.go +++ b/components/cli/command/container/rename.go @@ -1,14 +1,14 @@ package container import ( + "errors" "fmt" "strings" - "golang.org/x/net/context" - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type renameOptions struct { @@ -40,11 +40,11 @@ func runRename(dockerCli *command.DockerCli, opts *renameOptions) error { newName := strings.TrimSpace(opts.newName) if oldName == "" || newName == "" { - return fmt.Errorf("Error: Neither old nor new names may be empty") + return errors.New("Error: Neither old nor new names may be empty") } if err := dockerCli.Client().ContainerRename(ctx, oldName, newName); err != nil { - fmt.Fprintf(dockerCli.Err(), "%s\n", err) + fmt.Fprintln(dockerCli.Err(), err) return fmt.Errorf("Error: failed to rename container named %s", oldName) } return nil diff --git a/components/cli/command/container/restart.go b/components/cli/command/container/restart.go index fc3ba93c84..0a3dd9218d 100644 --- a/components/cli/command/container/restart.go +++ b/components/cli/command/container/restart.go @@ -1,15 +1,15 @@ package container import ( + "errors" "fmt" "strings" "time" - "golang.org/x/net/context" - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type restartOptions struct { @@ -51,12 +51,12 @@ func runRestart(dockerCli *command.DockerCli, opts *restartOptions) error { for _, name := range opts.containers { if err := dockerCli.Client().ContainerRestart(ctx, name, timeout); err != nil { errs = append(errs, err.Error()) - } else { - fmt.Fprintf(dockerCli.Out(), "%s\n", name) + continue } + fmt.Fprintln(dockerCli.Out(), name) } if len(errs) > 0 { - return fmt.Errorf("%s", strings.Join(errs, "\n")) + return errors.New(strings.Join(errs, "\n")) } return nil } diff --git a/components/cli/command/container/rm.go b/components/cli/command/container/rm.go index 60724f194b..c02533d787 100644 --- a/components/cli/command/container/rm.go +++ b/components/cli/command/container/rm.go @@ -1,15 +1,15 @@ package container import ( + "errors" "fmt" "strings" - "golang.org/x/net/context" - "github.com/docker/docker/api/types" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type rmOptions struct { @@ -52,22 +52,22 @@ func runRm(dockerCli *command.DockerCli, opts *rmOptions) error { } errChan := parallelOperation(ctx, opts.containers, func(ctx context.Context, container string) error { - if container == "" { - return fmt.Errorf("Container name cannot be empty") - } container = strings.Trim(container, "/") + if container == "" { + return errors.New("Container name cannot be empty") + } return dockerCli.Client().ContainerRemove(ctx, container, options) }) for _, name := range opts.containers { if err := <-errChan; err != nil { errs = append(errs, err.Error()) - } else { - fmt.Fprintf(dockerCli.Out(), "%s\n", name) + continue } + fmt.Fprintln(dockerCli.Out(), name) } if len(errs) > 0 { - return fmt.Errorf("%s", strings.Join(errs, "\n")) + return errors.New(strings.Join(errs, "\n")) } return nil } diff --git a/components/cli/command/container/run.go b/components/cli/command/container/run.go index 0fad93e688..2bfc49f286 100644 --- a/components/cli/command/container/run.go +++ b/components/cli/command/container/run.go @@ -1,6 +1,7 @@ package container import ( + "errors" "fmt" "io" "net/http/httputil" @@ -9,8 +10,6 @@ import ( "strings" "syscall" - "golang.org/x/net/context" - "github.com/Sirupsen/logrus" "github.com/docker/docker/api/types" "github.com/docker/docker/cli" @@ -22,6 +21,7 @@ import ( "github.com/docker/libnetwork/resolvconf/dns" "github.com/spf13/cobra" "github.com/spf13/pflag" + "golang.org/x/net/context" ) type runOptions struct { @@ -75,8 +75,8 @@ func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions var ( flAttach *opttypes.ListOpts - ErrConflictAttachDetach = fmt.Errorf("Conflicting options: -a and -d") - ErrConflictRestartPolicyAndAutoRemove = fmt.Errorf("Conflicting options: --restart and --rm") + ErrConflictAttachDetach = errors.New("Conflicting options: -a and -d") + ErrConflictRestartPolicyAndAutoRemove = errors.New("Conflicting options: --restart and --rm") ) config, hostConfig, networkingConfig, err := runconfigopts.Parse(flags, copts) @@ -91,7 +91,7 @@ func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions return ErrConflictRestartPolicyAndAutoRemove } if hostConfig.OomKillDisable != nil && *hostConfig.OomKillDisable && hostConfig.Memory == 0 { - fmt.Fprintf(stderr, "WARNING: Disabling the OOM killer on containers without setting a '-m/--memory' limit may be dangerous.\n") + fmt.Fprintln(stderr, "WARNING: Disabling the OOM killer on containers without setting a '-m/--memory' limit may be dangerous.") } if len(hostConfig.DNS) > 0 { @@ -158,7 +158,7 @@ func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions waitDisplayID = make(chan struct{}) go func() { defer close(waitDisplayID) - fmt.Fprintf(stdout, "%s\n", createResponse.ID) + fmt.Fprintln(stdout, createResponse.ID) }() } attach := config.AttachStdin || config.AttachStdout || config.AttachStderr @@ -203,11 +203,10 @@ func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions defer resp.Close() errCh = promise.Go(func() error { - errHijack := holdHijackedConnection(ctx, dockerCli, config.Tty, in, out, cerr, resp) - if errHijack == nil { - return errAttach + if errHijack := holdHijackedConnection(ctx, dockerCli, config.Tty, in, out, cerr, resp); errHijack != nil { + return errHijack } - return errHijack + return errAttach }) } @@ -233,7 +232,7 @@ func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions if (config.AttachStdin || config.AttachStdout || config.AttachStderr) && config.Tty && dockerCli.Out().IsTerminal() { if err := MonitorTtySize(ctx, dockerCli, createResponse.ID, false); err != nil { - fmt.Fprintf(stderr, "Error monitoring TTY size: %s\n", err) + fmt.Fprintln(stderr, "Error monitoring TTY size:", err) } } diff --git a/components/cli/command/container/start.go b/components/cli/command/container/start.go index 3521a41949..f5d8ca0bc4 100644 --- a/components/cli/command/container/start.go +++ b/components/cli/command/container/start.go @@ -1,19 +1,19 @@ package container import ( + "errors" "fmt" "io" "net/http/httputil" "strings" - "golang.org/x/net/context" - "github.com/docker/docker/api/types" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/pkg/promise" "github.com/docker/docker/pkg/signal" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type startOptions struct { @@ -59,7 +59,7 @@ func runStart(dockerCli *command.DockerCli, opts *startOptions) error { // We're going to attach to a container. // 1. Ensure we only have one container. if len(opts.containers) > 1 { - return fmt.Errorf("You cannot start and attach multiple containers at once.") + return errors.New("You cannot start and attach multiple containers at once.") } // 2. Attach to the container. @@ -131,7 +131,7 @@ func runStart(dockerCli *command.DockerCli, opts *startOptions) error { // 5. Wait for attachment to break. if c.Config.Tty && dockerCli.Out().IsTerminal() { if err := MonitorTtySize(ctx, dockerCli, c.ID, false); err != nil { - fmt.Fprintf(dockerCli.Err(), "Error monitoring TTY size: %s\n", err) + fmt.Fprintln(dockerCli.Err(), "Error monitoring TTY size:", err) } } if attchErr := <-cErr; attchErr != nil { @@ -143,7 +143,7 @@ func runStart(dockerCli *command.DockerCli, opts *startOptions) error { } } else if opts.checkpoint != "" { if len(opts.containers) > 1 { - return fmt.Errorf("You cannot restore multiple containers at once.") + return errors.New("You cannot restore multiple containers at once.") } container := opts.containers[0] startOptions := types.ContainerStartOptions{ @@ -165,15 +165,15 @@ func startContainersWithoutAttachments(ctx context.Context, dockerCli *command.D var failedContainers []string for _, container := range containers { if err := dockerCli.Client().ContainerStart(ctx, container, types.ContainerStartOptions{}); err != nil { - fmt.Fprintf(dockerCli.Err(), "%s\n", err) + fmt.Fprintln(dockerCli.Err(), err) failedContainers = append(failedContainers, container) - } else { - fmt.Fprintf(dockerCli.Out(), "%s\n", container) + continue } + fmt.Fprintln(dockerCli.Out(), container) } if len(failedContainers) > 0 { - return fmt.Errorf("Error: failed to start containers: %v", strings.Join(failedContainers, ", ")) + return fmt.Errorf("Error: failed to start containers: %s", strings.Join(failedContainers, ", ")) } return nil } diff --git a/components/cli/command/container/stats.go b/components/cli/command/container/stats.go index ebbd36e7e4..593db27b2a 100644 --- a/components/cli/command/container/stats.go +++ b/components/cli/command/container/stats.go @@ -1,14 +1,13 @@ package container import ( + "errors" "fmt" "io" "strings" "sync" "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" @@ -16,6 +15,7 @@ import ( "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/formatter" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type statsOptions struct { @@ -179,7 +179,7 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error { } cStats.mu.Unlock() if len(errs) > 0 { - return fmt.Errorf("%s", strings.Join(errs, "\n")) + return errors.New(strings.Join(errs, "\n")) } } diff --git a/components/cli/command/container/stop.go b/components/cli/command/container/stop.go index c68ede5368..48fd63a9f0 100644 --- a/components/cli/command/container/stop.go +++ b/components/cli/command/container/stop.go @@ -1,15 +1,15 @@ package container import ( + "errors" "fmt" "strings" "time" - "golang.org/x/net/context" - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type stopOptions struct { @@ -56,12 +56,12 @@ func runStop(dockerCli *command.DockerCli, opts *stopOptions) error { for _, container := range opts.containers { if err := <-errChan; err != nil { errs = append(errs, err.Error()) - } else { - fmt.Fprintf(dockerCli.Out(), "%s\n", container) + continue } + fmt.Fprintln(dockerCli.Out(), container) } if len(errs) > 0 { - return fmt.Errorf("%s", strings.Join(errs, "\n")) + return errors.New(strings.Join(errs, "\n")) } return nil } diff --git a/components/cli/command/container/top.go b/components/cli/command/container/top.go index 160153ba7f..4a6d3ed5cf 100644 --- a/components/cli/command/container/top.go +++ b/components/cli/command/container/top.go @@ -5,11 +5,10 @@ import ( "strings" "text/tabwriter" - "golang.org/x/net/context" - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type topOptions struct { diff --git a/components/cli/command/container/unpause.go b/components/cli/command/container/unpause.go index c4d8d4841e..5f342da0d7 100644 --- a/components/cli/command/container/unpause.go +++ b/components/cli/command/container/unpause.go @@ -1,14 +1,14 @@ package container import ( + "errors" "fmt" "strings" - "golang.org/x/net/context" - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type unpauseOptions struct { @@ -39,12 +39,12 @@ func runUnpause(dockerCli *command.DockerCli, opts *unpauseOptions) error { for _, container := range opts.containers { if err := <-errChan; err != nil { errs = append(errs, err.Error()) - } else { - fmt.Fprintf(dockerCli.Out(), "%s\n", container) + continue } + fmt.Fprintln(dockerCli.Out(), container) } if len(errs) > 0 { - return fmt.Errorf("%s", strings.Join(errs, "\n")) + return errors.New(strings.Join(errs, "\n")) } return nil } diff --git a/components/cli/command/container/update.go b/components/cli/command/container/update.go index 75765856c5..6a7cc820e9 100644 --- a/components/cli/command/container/update.go +++ b/components/cli/command/container/update.go @@ -1,17 +1,17 @@ package container import ( + "errors" "fmt" "strings" - "golang.org/x/net/context" - containertypes "github.com/docker/docker/api/types/container" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" runconfigopts "github.com/docker/docker/runconfig/opts" "github.com/docker/go-units" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type updateOptions struct { @@ -71,7 +71,7 @@ func runUpdate(dockerCli *command.DockerCli, opts *updateOptions) error { var err error if opts.nFlag == 0 { - return fmt.Errorf("You must provide one or more flags when using this command.") + return errors.New("You must provide one or more flags when using this command.") } var memory int64 @@ -149,15 +149,15 @@ func runUpdate(dockerCli *command.DockerCli, opts *updateOptions) error { if err != nil { errs = append(errs, err.Error()) } else { - fmt.Fprintf(dockerCli.Out(), "%s\n", container) + fmt.Fprintln(dockerCli.Out(), container) } warns = append(warns, r.Warnings...) } if len(warns) > 0 { - fmt.Fprintf(dockerCli.Out(), "%s", strings.Join(warns, "\n")) + fmt.Fprintln(dockerCli.Out(), strings.Join(warns, "\n")) } if len(errs) > 0 { - return fmt.Errorf("%s", strings.Join(errs, "\n")) + return errors.New(strings.Join(errs, "\n")) } return nil } diff --git a/components/cli/command/container/utils.go b/components/cli/command/container/utils.go index 6bef92463c..e4664b745c 100644 --- a/components/cli/command/container/utils.go +++ b/components/cli/command/container/utils.go @@ -3,8 +3,6 @@ package container import ( "strconv" - "golang.org/x/net/context" - "github.com/Sirupsen/logrus" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/events" @@ -12,6 +10,7 @@ import ( "github.com/docker/docker/api/types/versions" "github.com/docker/docker/cli/command" clientapi "github.com/docker/docker/client" + "golang.org/x/net/context" ) func waitExitOrRemoved(ctx context.Context, dockerCli *command.DockerCli, containerID string, waitRemove bool) chan int { diff --git a/components/cli/command/container/wait.go b/components/cli/command/container/wait.go index 19ccf7ac25..d8dce6ef1a 100644 --- a/components/cli/command/container/wait.go +++ b/components/cli/command/container/wait.go @@ -1,14 +1,14 @@ package container import ( + "errors" "fmt" "strings" - "golang.org/x/net/context" - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type waitOptions struct { @@ -39,12 +39,12 @@ func runWait(dockerCli *command.DockerCli, opts *waitOptions) error { status, err := dockerCli.Client().ContainerWait(ctx, container) if err != nil { errs = append(errs, err.Error()) - } else { - fmt.Fprintf(dockerCli.Out(), "%d\n", status) + continue } + fmt.Fprintf(dockerCli.Out(), "%d\n", status) } if len(errs) > 0 { - return fmt.Errorf("%s", strings.Join(errs, "\n")) + return errors.New(strings.Join(errs, "\n")) } return nil } From 2f472a7b3d5dee1e2338009c37f7faad7328a7b0 Mon Sep 17 00:00:00 2001 From: allencloud Date: Mon, 21 Nov 2016 18:22:22 +0800 Subject: [PATCH 493/978] split function out of command description scope Signed-off-by: allencloud Upstream-commit: 3e7dca79007f7e51d7794b589261e94d8d0742b2 Component: cli --- components/cli/command/swarm/join_token.go | 129 ++++++++++++--------- components/cli/command/swarm/unlock.go | 66 ++++++----- components/cli/command/swarm/unlock_key.go | 100 ++++++++-------- 3 files changed, 163 insertions(+), 132 deletions(-) diff --git a/components/cli/command/swarm/join_token.go b/components/cli/command/swarm/join_token.go index 3a17a8020f..d800b769ba 100644 --- a/components/cli/command/swarm/join_token.go +++ b/components/cli/command/swarm/join_token.go @@ -12,92 +12,107 @@ import ( "golang.org/x/net/context" ) +type joinTokenOptions struct { + role string + rotate bool + quiet bool +} + func newJoinTokenCommand(dockerCli *command.DockerCli) *cobra.Command { - var rotate, quiet bool + opts := joinTokenOptions{} cmd := &cobra.Command{ Use: "join-token [OPTIONS] (worker|manager)", Short: "Manage join tokens", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - worker := args[0] == "worker" - manager := args[0] == "manager" - - if !worker && !manager { - return errors.New("unknown role " + args[0]) - } - - client := dockerCli.Client() - ctx := context.Background() - - if rotate { - var flags swarm.UpdateFlags - - swarm, err := client.SwarmInspect(ctx) - if err != nil { - return err - } - - flags.RotateWorkerToken = worker - flags.RotateManagerToken = manager - - err = client.SwarmUpdate(ctx, swarm.Version, swarm.Spec, flags) - if err != nil { - return err - } - if !quiet { - fmt.Fprintf(dockerCli.Out(), "Successfully rotated %s join token.\n\n", args[0]) - } - } - - swarm, err := client.SwarmInspect(ctx) - if err != nil { - return err - } - - if quiet { - if worker { - fmt.Fprintln(dockerCli.Out(), swarm.JoinTokens.Worker) - } else { - fmt.Fprintln(dockerCli.Out(), swarm.JoinTokens.Manager) - } - } else { - info, err := client.Info(ctx) - if err != nil { - return err - } - return printJoinCommand(ctx, dockerCli, info.Swarm.NodeID, worker, manager) - } - return nil + opts.role = args[0] + return runJoinToken(dockerCli, opts) }, } flags := cmd.Flags() - flags.BoolVar(&rotate, flagRotate, false, "Rotate join token") - flags.BoolVarP(&quiet, flagQuiet, "q", false, "Only display token") + flags.BoolVar(&opts.rotate, flagRotate, false, "Rotate join token") + flags.BoolVarP(&opts.quiet, flagQuiet, "q", false, "Only display token") return cmd } -func printJoinCommand(ctx context.Context, dockerCli *command.DockerCli, nodeID string, worker bool, manager bool) error { - client := dockerCli.Client() +func runJoinToken(dockerCli *command.DockerCli, opts joinTokenOptions) error { + worker := opts.role == "worker" + manager := opts.role == "manager" - swarm, err := client.SwarmInspect(ctx) + if !worker && !manager { + return errors.New("unknown role " + opts.role) + } + + client := dockerCli.Client() + ctx := context.Background() + + if opts.rotate { + flags := swarm.UpdateFlags{ + RotateWorkerToken: worker, + RotateManagerToken: manager, + } + + sw, err := client.SwarmInspect(ctx) + if err != nil { + return err + } + + if err := client.SwarmUpdate(ctx, sw.Version, sw.Spec, flags); err != nil { + return err + } + + if !opts.quiet { + fmt.Fprintf(dockerCli.Out(), "Successfully rotated %s join token.\n\n", opts.role) + } + } + + // second SwarmInspect in this function, + // this is necessary since SwarmUpdate after first changes the join tokens + sw, err := client.SwarmInspect(ctx) if err != nil { return err } + if opts.quiet && worker { + fmt.Fprintln(dockerCli.Out(), sw.JoinTokens.Worker) + return nil + } + + if opts.quiet && manager { + fmt.Fprintln(dockerCli.Out(), sw.JoinTokens.Manager) + return nil + } + + info, err := client.Info(ctx) + if err != nil { + return err + } + + return printJoinCommand(ctx, dockerCli, info.Swarm.NodeID, worker, manager) +} + +func printJoinCommand(ctx context.Context, dockerCli *command.DockerCli, nodeID string, worker bool, manager bool) error { + client := dockerCli.Client() + node, _, err := client.NodeInspectWithRaw(ctx, nodeID) if err != nil { return err } + sw, err := client.SwarmInspect(ctx) + if err != nil { + return err + } + if node.ManagerStatus != nil { if worker { - fmt.Fprintf(dockerCli.Out(), "To add a worker to this swarm, run the following command:\n\n docker swarm join \\\n --token %s \\\n %s\n\n", swarm.JoinTokens.Worker, node.ManagerStatus.Addr) + fmt.Fprintf(dockerCli.Out(), "To add a worker to this swarm, run the following command:\n\n docker swarm join \\\n --token %s \\\n %s\n\n", sw.JoinTokens.Worker, node.ManagerStatus.Addr) } if manager { - fmt.Fprintf(dockerCli.Out(), "To add a manager to this swarm, run the following command:\n\n docker swarm join \\\n --token %s \\\n %s\n\n", swarm.JoinTokens.Manager, node.ManagerStatus.Addr) + fmt.Fprintf(dockerCli.Out(), "To add a manager to this swarm, run the following command:\n\n docker swarm join \\\n --token %s \\\n %s\n\n", sw.JoinTokens.Manager, node.ManagerStatus.Addr) } } diff --git a/components/cli/command/swarm/unlock.go b/components/cli/command/swarm/unlock.go index abb9e89fe7..f7d418760b 100644 --- a/components/cli/command/swarm/unlock.go +++ b/components/cli/command/swarm/unlock.go @@ -16,46 +16,54 @@ import ( "golang.org/x/net/context" ) +type unlockOptions struct{} + func newUnlockCommand(dockerCli *command.DockerCli) *cobra.Command { + opts := unlockOptions{} + cmd := &cobra.Command{ Use: "unlock", Short: "Unlock swarm", - Args: cli.ExactArgs(0), + Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - client := dockerCli.Client() - ctx := context.Background() - - // First see if the node is actually part of a swarm, and if it's is actually locked first. - // If it's in any other state than locked, don't ask for the key. - info, err := client.Info(ctx) - if err != nil { - return err - } - - switch info.Swarm.LocalNodeState { - case swarm.LocalNodeStateInactive: - return errors.New("Error: This node is not part of a swarm") - case swarm.LocalNodeStateLocked: - break - default: - return errors.New("Error: swarm is not locked") - } - - key, err := readKey(dockerCli.In(), "Please enter unlock key: ") - if err != nil { - return err - } - req := swarm.UnlockRequest{ - UnlockKey: key, - } - - return client.SwarmUnlock(ctx, req) + return runUnlock(dockerCli, opts) }, } return cmd } +func runUnlock(dockerCli *command.DockerCli, opts unlockOptions) error { + client := dockerCli.Client() + ctx := context.Background() + + // First see if the node is actually part of a swarm, and if it's is actually locked first. + // If it's in any other state than locked, don't ask for the key. + info, err := client.Info(ctx) + if err != nil { + return err + } + + switch info.Swarm.LocalNodeState { + case swarm.LocalNodeStateInactive: + return errors.New("Error: This node is not part of a swarm") + case swarm.LocalNodeStateLocked: + break + default: + return errors.New("Error: swarm is not locked") + } + + key, err := readKey(dockerCli.In(), "Please enter unlock key: ") + if err != nil { + return err + } + req := swarm.UnlockRequest{ + UnlockKey: key, + } + + return client.SwarmUnlock(ctx, req) +} + func readKey(in *command.InStream, prompt string) (string, error) { if in.IsTerminal() { fmt.Print(prompt) diff --git a/components/cli/command/swarm/unlock_key.go b/components/cli/command/swarm/unlock_key.go index 96450f55b8..e571e6645f 100644 --- a/components/cli/command/swarm/unlock_key.go +++ b/components/cli/command/swarm/unlock_key.go @@ -12,68 +12,76 @@ import ( "golang.org/x/net/context" ) +type unlockKeyOptions struct { + rotate bool + quiet bool +} + func newUnlockKeyCommand(dockerCli *command.DockerCli) *cobra.Command { - var rotate, quiet bool + opts := unlockKeyOptions{} cmd := &cobra.Command{ Use: "unlock-key [OPTIONS]", Short: "Manage the unlock key", Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { - client := dockerCli.Client() - ctx := context.Background() - - if rotate { - flags := swarm.UpdateFlags{RotateManagerUnlockKey: true} - - swarm, err := client.SwarmInspect(ctx) - if err != nil { - return err - } - - if !swarm.Spec.EncryptionConfig.AutoLockManagers { - return errors.New("cannot rotate because autolock is not turned on") - } - - err = client.SwarmUpdate(ctx, swarm.Version, swarm.Spec, flags) - if err != nil { - return err - } - if !quiet { - fmt.Fprintf(dockerCli.Out(), "Successfully rotated manager unlock key.\n\n") - } - } - - unlockKeyResp, err := client.SwarmGetUnlockKey(ctx) - if err != nil { - return errors.Wrap(err, "could not fetch unlock key") - } - - if unlockKeyResp.UnlockKey == "" { - return errors.New("no unlock key is set") - } - - if quiet { - fmt.Fprintln(dockerCli.Out(), unlockKeyResp.UnlockKey) - } else { - printUnlockCommand(ctx, dockerCli, unlockKeyResp.UnlockKey) - } - return nil + return runUnlockKey(dockerCli, opts) }, } flags := cmd.Flags() - flags.BoolVar(&rotate, flagRotate, false, "Rotate unlock key") - flags.BoolVarP(&quiet, flagQuiet, "q", false, "Only display token") + flags.BoolVar(&opts.rotate, flagRotate, false, "Rotate unlock key") + flags.BoolVarP(&opts.quiet, flagQuiet, "q", false, "Only display token") return cmd } -func printUnlockCommand(ctx context.Context, dockerCli *command.DockerCli, unlockKey string) { - if len(unlockKey) == 0 { - return +func runUnlockKey(dockerCli *command.DockerCli, opts unlockKeyOptions) error { + client := dockerCli.Client() + ctx := context.Background() + + if opts.rotate { + flags := swarm.UpdateFlags{RotateManagerUnlockKey: true} + + sw, err := client.SwarmInspect(ctx) + if err != nil { + return err + } + + if !sw.Spec.EncryptionConfig.AutoLockManagers { + return errors.New("cannot rotate because autolock is not turned on") + } + + if err := client.SwarmUpdate(ctx, sw.Version, sw.Spec, flags); err != nil { + return err + } + + if !opts.quiet { + fmt.Fprintf(dockerCli.Out(), "Successfully rotated manager unlock key.\n\n") + } } - fmt.Fprintf(dockerCli.Out(), "To unlock a swarm manager after it restarts, run the `docker swarm unlock`\ncommand and provide the following key:\n\n %s\n\nPlease remember to store this key in a password manager, since without it you\nwill not be able to restart the manager.\n", unlockKey) + unlockKeyResp, err := client.SwarmGetUnlockKey(ctx) + if err != nil { + return errors.Wrap(err, "could not fetch unlock key") + } + + if unlockKeyResp.UnlockKey == "" { + return errors.New("no unlock key is set") + } + + if opts.quiet { + fmt.Fprintln(dockerCli.Out(), unlockKeyResp.UnlockKey) + return nil + } + + printUnlockCommand(ctx, dockerCli, unlockKeyResp.UnlockKey) + return nil +} + +func printUnlockCommand(ctx context.Context, dockerCli *command.DockerCli, unlockKey string) { + if len(unlockKey) > 0 { + fmt.Fprintf(dockerCli.Out(), "To unlock a swarm manager after it restarts, run the `docker swarm unlock`\ncommand and provide the following key:\n\n %s\n\nPlease remember to store this key in a password manager, since without it you\nwill not be able to restart the manager.\n", unlockKey) + } return } From 61848b38b0bae14929f7b01b7371a8e06675a494 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Mon, 26 Dec 2016 13:47:43 -0800 Subject: [PATCH 494/978] Remove `docker stack ps -a` to match removal of `docker service/node ps -a` In #28507 and #28885, `docker service/node ps -a` has been removed so that information about slots are show up even without `-a` flag. The output of `docker stack ps` reused the same output as `docker service/node ps`. However, the `-a` was still there. It might make sense to remove `docker stack ps -a` as well to bring consistency with `docker service/node ps`. This fix is related to #28507, #28885, and #25983. Signed-off-by: Yong Tang Upstream-commit: 65be5677bd59ccedc128d0fc19e78c428cf68191 Component: cli --- components/cli/command/stack/ps.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/components/cli/command/stack/ps.go b/components/cli/command/stack/ps.go index 497fb97b5e..7bbcf54205 100644 --- a/components/cli/command/stack/ps.go +++ b/components/cli/command/stack/ps.go @@ -6,7 +6,6 @@ import ( "golang.org/x/net/context" "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/idresolver" @@ -16,7 +15,6 @@ import ( ) type psOptions struct { - all bool filter opts.FilterOpt noTrunc bool namespace string @@ -36,7 +34,6 @@ func newPsCommand(dockerCli *command.DockerCli) *cobra.Command { }, } flags := cmd.Flags() - flags.BoolVarP(&opts.all, "all", "a", false, "Display all tasks") flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output") flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names") flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") @@ -50,10 +47,6 @@ func runPS(dockerCli *command.DockerCli, opts psOptions) error { ctx := context.Background() filter := getStackFilterFromOpt(opts.namespace, opts.filter) - if !opts.all && !filter.Include("desired-state") { - filter.Add("desired-state", string(swarm.TaskStateRunning)) - filter.Add("desired-state", string(swarm.TaskStateAccepted)) - } tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: filter}) if err != nil { From 0d7462bd1f9dc79febf79c956d4f661babcccb77 Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Thu, 22 Dec 2016 11:44:09 -0800 Subject: [PATCH 495/978] Define PushResult in api types Signed-off-by: Tonis Tiigi Upstream-commit: bcc61e1300ae4cc12ea8a377efafec59aed4ea9a Component: cli --- components/cli/command/image/trust.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/components/cli/command/image/trust.go b/components/cli/command/image/trust.go index f32c301959..192bc047c9 100644 --- a/components/cli/command/image/trust.go +++ b/components/cli/command/image/trust.go @@ -9,19 +9,17 @@ import ( "path" "sort" - "golang.org/x/net/context" - "github.com/Sirupsen/logrus" "github.com/docker/distribution/digest" "github.com/docker/docker/api/types" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/trust" - "github.com/docker/docker/distribution" "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/reference" "github.com/docker/docker/registry" "github.com/docker/notary/client" "github.com/docker/notary/tuf/data" + "golang.org/x/net/context" ) type target struct { @@ -52,17 +50,19 @@ func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry return } - var pushResult distribution.PushResult + var pushResult types.PushResult err := json.Unmarshal(*aux, &pushResult) - if err == nil && pushResult.Tag != "" && pushResult.Digest.Validate() == nil { - h, err := hex.DecodeString(pushResult.Digest.Hex()) - if err != nil { - target = nil - return + if err == nil && pushResult.Tag != "" { + if dgst, err := digest.ParseDigest(pushResult.Digest); err == nil { + h, err := hex.DecodeString(dgst.Hex()) + if err != nil { + target = nil + return + } + target.Name = pushResult.Tag + target.Hashes = data.Hashes{string(dgst.Algorithm()): h} + target.Length = int64(pushResult.Size) } - target.Name = pushResult.Tag - target.Hashes = data.Hashes{string(pushResult.Digest.Algorithm()): h} - target.Length = int64(pushResult.Size) } } From 337c5bb793cc903ce7ad83fd6d5c738a6b1b5987 Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Thu, 22 Dec 2016 13:25:02 -0800 Subject: [PATCH 496/978] Move builder cli helper functions to own pkg Signed-off-by: Tonis Tiigi Upstream-commit: c41bfce39acce23c7268144c22bb776d9c9c38b1 Component: cli --- components/cli/command/image/build.go | 14 +- components/cli/command/image/build/context.go | 265 ++++++++++++ .../cli/command/image/build/context_test.go | 383 ++++++++++++++++++ .../cli/command/image/build/context_unix.go | 11 + .../command/image/build/context_windows.go | 17 + 5 files changed, 683 insertions(+), 7 deletions(-) create mode 100644 components/cli/command/image/build/context.go create mode 100644 components/cli/command/image/build/context_test.go create mode 100644 components/cli/command/image/build/context_unix.go create mode 100644 components/cli/command/image/build/context_windows.go diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go index e3e7ff2b02..f194659e08 100644 --- a/components/cli/command/image/build.go +++ b/components/cli/command/image/build.go @@ -16,10 +16,10 @@ import ( "github.com/docker/docker/api" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" - "github.com/docker/docker/builder" "github.com/docker/docker/builder/dockerignore" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/image/build" "github.com/docker/docker/opts" "github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/fileutils" @@ -29,7 +29,7 @@ import ( "github.com/docker/docker/pkg/urlutil" "github.com/docker/docker/reference" runconfigopts "github.com/docker/docker/runconfig/opts" - "github.com/docker/go-units" + units "github.com/docker/go-units" "github.com/spf13/cobra" ) @@ -156,13 +156,13 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { switch { case specifiedContext == "-": - buildCtx, relDockerfile, err = builder.GetContextFromReader(dockerCli.In(), options.dockerfileName) + buildCtx, relDockerfile, err = build.GetContextFromReader(dockerCli.In(), options.dockerfileName) case urlutil.IsGitURL(specifiedContext): - tempDir, relDockerfile, err = builder.GetContextFromGitURL(specifiedContext, options.dockerfileName) + tempDir, relDockerfile, err = build.GetContextFromGitURL(specifiedContext, options.dockerfileName) case urlutil.IsURL(specifiedContext): - buildCtx, relDockerfile, err = builder.GetContextFromURL(progBuff, specifiedContext, options.dockerfileName) + buildCtx, relDockerfile, err = build.GetContextFromURL(progBuff, specifiedContext, options.dockerfileName) default: - contextDir, relDockerfile, err = builder.GetContextFromLocalDir(specifiedContext, options.dockerfileName) + contextDir, relDockerfile, err = build.GetContextFromLocalDir(specifiedContext, options.dockerfileName) } if err != nil { @@ -198,7 +198,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { } } - if err := builder.ValidateContextDirectory(contextDir, excludes); err != nil { + if err := build.ValidateContextDirectory(contextDir, excludes); err != nil { return fmt.Errorf("Error checking context: '%s'.", err) } diff --git a/components/cli/command/image/build/context.go b/components/cli/command/image/build/context.go new file mode 100644 index 0000000000..86157c359d --- /dev/null +++ b/components/cli/command/image/build/context.go @@ -0,0 +1,265 @@ +package build + +import ( + "bufio" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/fileutils" + "github.com/docker/docker/pkg/gitutils" + "github.com/docker/docker/pkg/httputils" + "github.com/docker/docker/pkg/ioutils" + "github.com/docker/docker/pkg/progress" + "github.com/docker/docker/pkg/streamformatter" +) + +const ( + // DefaultDockerfileName is the Default filename with Docker commands, read by docker build + DefaultDockerfileName string = "Dockerfile" +) + +// ValidateContextDirectory checks if all the contents of the directory +// can be read and returns an error if some files can't be read +// symlinks which point to non-existing files don't trigger an error +func ValidateContextDirectory(srcPath string, excludes []string) error { + contextRoot, err := getContextRoot(srcPath) + if err != nil { + return err + } + return filepath.Walk(contextRoot, func(filePath string, f os.FileInfo, err error) error { + if err != nil { + if os.IsPermission(err) { + return fmt.Errorf("can't stat '%s'", filePath) + } + if os.IsNotExist(err) { + return nil + } + return err + } + + // skip this directory/file if it's not in the path, it won't get added to the context + if relFilePath, err := filepath.Rel(contextRoot, filePath); err != nil { + return err + } else if skip, err := fileutils.Matches(relFilePath, excludes); err != nil { + return err + } else if skip { + if f.IsDir() { + return filepath.SkipDir + } + return nil + } + + // skip checking if symlinks point to non-existing files, such symlinks can be useful + // also skip named pipes, because they hanging on open + if f.Mode()&(os.ModeSymlink|os.ModeNamedPipe) != 0 { + return nil + } + + if !f.IsDir() { + currentFile, err := os.Open(filePath) + if err != nil && os.IsPermission(err) { + return fmt.Errorf("no permission to read from '%s'", filePath) + } + currentFile.Close() + } + return nil + }) +} + +// GetContextFromReader will read the contents of the given reader as either a +// Dockerfile or tar archive. Returns a tar archive used as a context and a +// path to the Dockerfile inside the tar. +func GetContextFromReader(r io.ReadCloser, dockerfileName string) (out io.ReadCloser, relDockerfile string, err error) { + buf := bufio.NewReader(r) + + magic, err := buf.Peek(archive.HeaderSize) + if err != nil && err != io.EOF { + return nil, "", fmt.Errorf("failed to peek context header from STDIN: %v", err) + } + + if archive.IsArchive(magic) { + return ioutils.NewReadCloserWrapper(buf, func() error { return r.Close() }), dockerfileName, nil + } + + // Input should be read as a Dockerfile. + tmpDir, err := ioutil.TempDir("", "docker-build-context-") + if err != nil { + return nil, "", fmt.Errorf("unbale to create temporary context directory: %v", err) + } + + f, err := os.Create(filepath.Join(tmpDir, DefaultDockerfileName)) + if err != nil { + return nil, "", err + } + _, err = io.Copy(f, buf) + if err != nil { + f.Close() + return nil, "", err + } + + if err := f.Close(); err != nil { + return nil, "", err + } + if err := r.Close(); err != nil { + return nil, "", err + } + + tar, err := archive.Tar(tmpDir, archive.Uncompressed) + if err != nil { + return nil, "", err + } + + return ioutils.NewReadCloserWrapper(tar, func() error { + err := tar.Close() + os.RemoveAll(tmpDir) + return err + }), DefaultDockerfileName, nil + +} + +// GetContextFromGitURL uses a Git URL as context for a `docker build`. The +// git repo is cloned into a temporary directory used as the context directory. +// Returns the absolute path to the temporary context directory, the relative +// path of the dockerfile in that context directory, and a non-nil error on +// success. +func GetContextFromGitURL(gitURL, dockerfileName string) (absContextDir, relDockerfile string, err error) { + if _, err := exec.LookPath("git"); err != nil { + return "", "", fmt.Errorf("unable to find 'git': %v", err) + } + if absContextDir, err = gitutils.Clone(gitURL); err != nil { + return "", "", fmt.Errorf("unable to 'git clone' to temporary context directory: %v", err) + } + + return getDockerfileRelPath(absContextDir, dockerfileName) +} + +// GetContextFromURL uses a remote URL as context for a `docker build`. The +// remote resource is downloaded as either a Dockerfile or a tar archive. +// Returns the tar archive used for the context and a path of the +// dockerfile inside the tar. +func GetContextFromURL(out io.Writer, remoteURL, dockerfileName string) (io.ReadCloser, string, error) { + response, err := httputils.Download(remoteURL) + if err != nil { + return nil, "", fmt.Errorf("unable to download remote context %s: %v", remoteURL, err) + } + progressOutput := streamformatter.NewStreamFormatter().NewProgressOutput(out, true) + + // Pass the response body through a progress reader. + progReader := progress.NewProgressReader(response.Body, progressOutput, response.ContentLength, "", fmt.Sprintf("Downloading build context from remote url: %s", remoteURL)) + + return GetContextFromReader(ioutils.NewReadCloserWrapper(progReader, func() error { return response.Body.Close() }), dockerfileName) +} + +// GetContextFromLocalDir uses the given local directory as context for a +// `docker build`. Returns the absolute path to the local context directory, +// the relative path of the dockerfile in that context directory, and a non-nil +// error on success. +func GetContextFromLocalDir(localDir, dockerfileName string) (absContextDir, relDockerfile string, err error) { + // When using a local context directory, when the Dockerfile is specified + // with the `-f/--file` option then it is considered relative to the + // current directory and not the context directory. + if dockerfileName != "" { + if dockerfileName, err = filepath.Abs(dockerfileName); err != nil { + return "", "", fmt.Errorf("unable to get absolute path to Dockerfile: %v", err) + } + } + + return getDockerfileRelPath(localDir, dockerfileName) +} + +// getDockerfileRelPath uses the given context directory for a `docker build` +// and returns the absolute path to the context directory, the relative path of +// the dockerfile in that context directory, and a non-nil error on success. +func getDockerfileRelPath(givenContextDir, givenDockerfile string) (absContextDir, relDockerfile string, err error) { + if absContextDir, err = filepath.Abs(givenContextDir); err != nil { + return "", "", fmt.Errorf("unable to get absolute context directory of given context directory %q: %v", givenContextDir, err) + } + + // The context dir might be a symbolic link, so follow it to the actual + // target directory. + // + // FIXME. We use isUNC (always false on non-Windows platforms) to workaround + // an issue in golang. On Windows, EvalSymLinks does not work on UNC file + // paths (those starting with \\). This hack means that when using links + // on UNC paths, they will not be followed. + if !isUNC(absContextDir) { + absContextDir, err = filepath.EvalSymlinks(absContextDir) + if err != nil { + return "", "", fmt.Errorf("unable to evaluate symlinks in context path: %v", err) + } + } + + stat, err := os.Lstat(absContextDir) + if err != nil { + return "", "", fmt.Errorf("unable to stat context directory %q: %v", absContextDir, err) + } + + if !stat.IsDir() { + return "", "", fmt.Errorf("context must be a directory: %s", absContextDir) + } + + absDockerfile := givenDockerfile + if absDockerfile == "" { + // No -f/--file was specified so use the default relative to the + // context directory. + absDockerfile = filepath.Join(absContextDir, DefaultDockerfileName) + + // Just to be nice ;-) look for 'dockerfile' too but only + // use it if we found it, otherwise ignore this check + if _, err = os.Lstat(absDockerfile); os.IsNotExist(err) { + altPath := filepath.Join(absContextDir, strings.ToLower(DefaultDockerfileName)) + if _, err = os.Lstat(altPath); err == nil { + absDockerfile = altPath + } + } + } + + // If not already an absolute path, the Dockerfile path should be joined to + // the base directory. + if !filepath.IsAbs(absDockerfile) { + absDockerfile = filepath.Join(absContextDir, absDockerfile) + } + + // Evaluate symlinks in the path to the Dockerfile too. + // + // FIXME. We use isUNC (always false on non-Windows platforms) to workaround + // an issue in golang. On Windows, EvalSymLinks does not work on UNC file + // paths (those starting with \\). This hack means that when using links + // on UNC paths, they will not be followed. + if !isUNC(absDockerfile) { + absDockerfile, err = filepath.EvalSymlinks(absDockerfile) + if err != nil { + return "", "", fmt.Errorf("unable to evaluate symlinks in Dockerfile path: %v", err) + } + } + + if _, err := os.Lstat(absDockerfile); err != nil { + if os.IsNotExist(err) { + return "", "", fmt.Errorf("Cannot locate Dockerfile: %q", absDockerfile) + } + return "", "", fmt.Errorf("unable to stat Dockerfile: %v", err) + } + + if relDockerfile, err = filepath.Rel(absContextDir, absDockerfile); err != nil { + return "", "", fmt.Errorf("unable to get relative Dockerfile path: %v", err) + } + + if strings.HasPrefix(relDockerfile, ".."+string(filepath.Separator)) { + return "", "", fmt.Errorf("The Dockerfile (%s) must be within the build context (%s)", givenDockerfile, givenContextDir) + } + + return absContextDir, relDockerfile, nil +} + +// isUNC returns true if the path is UNC (one starting \\). It always returns +// false on Linux. +func isUNC(path string) bool { + return runtime.GOOS == "windows" && strings.HasPrefix(path, `\\`) +} diff --git a/components/cli/command/image/build/context_test.go b/components/cli/command/image/build/context_test.go new file mode 100644 index 0000000000..afa04a4fcd --- /dev/null +++ b/components/cli/command/image/build/context_test.go @@ -0,0 +1,383 @@ +package build + +import ( + "archive/tar" + "bytes" + "io" + "io/ioutil" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/docker/docker/pkg/archive" +) + +const dockerfileContents = "FROM busybox" + +var prepareEmpty = func(t *testing.T) (string, func()) { + return "", func() {} +} + +var prepareNoFiles = func(t *testing.T) (string, func()) { + return createTestTempDir(t, "", "builder-context-test") +} + +var prepareOneFile = func(t *testing.T) (string, func()) { + contextDir, cleanup := createTestTempDir(t, "", "builder-context-test") + createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents, 0777) + return contextDir, cleanup +} + +func testValidateContextDirectory(t *testing.T, prepare func(t *testing.T) (string, func()), excludes []string) { + contextDir, cleanup := prepare(t) + defer cleanup() + + err := ValidateContextDirectory(contextDir, excludes) + + if err != nil { + t.Fatalf("Error should be nil, got: %s", err) + } +} + +func TestGetContextFromLocalDirNoDockerfile(t *testing.T) { + contextDir, cleanup := createTestTempDir(t, "", "builder-context-test") + defer cleanup() + + absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, "") + + if err == nil { + t.Fatalf("Error should not be nil") + } + + if absContextDir != "" { + t.Fatalf("Absolute directory path should be empty, got: %s", absContextDir) + } + + if relDockerfile != "" { + t.Fatalf("Relative path to Dockerfile should be empty, got: %s", relDockerfile) + } +} + +func TestGetContextFromLocalDirNotExistingDir(t *testing.T) { + contextDir, cleanup := createTestTempDir(t, "", "builder-context-test") + defer cleanup() + + fakePath := filepath.Join(contextDir, "fake") + + absContextDir, relDockerfile, err := GetContextFromLocalDir(fakePath, "") + + if err == nil { + t.Fatalf("Error should not be nil") + } + + if absContextDir != "" { + t.Fatalf("Absolute directory path should be empty, got: %s", absContextDir) + } + + if relDockerfile != "" { + t.Fatalf("Relative path to Dockerfile should be empty, got: %s", relDockerfile) + } +} + +func TestGetContextFromLocalDirNotExistingDockerfile(t *testing.T) { + contextDir, cleanup := createTestTempDir(t, "", "builder-context-test") + defer cleanup() + + fakePath := filepath.Join(contextDir, "fake") + + absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, fakePath) + + if err == nil { + t.Fatalf("Error should not be nil") + } + + if absContextDir != "" { + t.Fatalf("Absolute directory path should be empty, got: %s", absContextDir) + } + + if relDockerfile != "" { + t.Fatalf("Relative path to Dockerfile should be empty, got: %s", relDockerfile) + } +} + +func TestGetContextFromLocalDirWithNoDirectory(t *testing.T) { + contextDir, dirCleanup := createTestTempDir(t, "", "builder-context-test") + defer dirCleanup() + + createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents, 0777) + + chdirCleanup := chdir(t, contextDir) + defer chdirCleanup() + + absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, "") + + if err != nil { + t.Fatalf("Error when getting context from local dir: %s", err) + } + + if absContextDir != contextDir { + t.Fatalf("Absolute directory path should be equal to %s, got: %s", contextDir, absContextDir) + } + + if relDockerfile != DefaultDockerfileName { + t.Fatalf("Relative path to dockerfile should be equal to %s, got: %s", DefaultDockerfileName, relDockerfile) + } +} + +func TestGetContextFromLocalDirWithDockerfile(t *testing.T) { + contextDir, cleanup := createTestTempDir(t, "", "builder-context-test") + defer cleanup() + + createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents, 0777) + + absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, "") + + if err != nil { + t.Fatalf("Error when getting context from local dir: %s", err) + } + + if absContextDir != contextDir { + t.Fatalf("Absolute directory path should be equal to %s, got: %s", contextDir, absContextDir) + } + + if relDockerfile != DefaultDockerfileName { + t.Fatalf("Relative path to dockerfile should be equal to %s, got: %s", DefaultDockerfileName, relDockerfile) + } +} + +func TestGetContextFromLocalDirLocalFile(t *testing.T) { + contextDir, cleanup := createTestTempDir(t, "", "builder-context-test") + defer cleanup() + + createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents, 0777) + testFilename := createTestTempFile(t, contextDir, "tmpTest", "test", 0777) + + absContextDir, relDockerfile, err := GetContextFromLocalDir(testFilename, "") + + if err == nil { + t.Fatalf("Error should not be nil") + } + + if absContextDir != "" { + t.Fatalf("Absolute directory path should be empty, got: %s", absContextDir) + } + + if relDockerfile != "" { + t.Fatalf("Relative path to Dockerfile should be empty, got: %s", relDockerfile) + } +} + +func TestGetContextFromLocalDirWithCustomDockerfile(t *testing.T) { + contextDir, cleanup := createTestTempDir(t, "", "builder-context-test") + defer cleanup() + + chdirCleanup := chdir(t, contextDir) + defer chdirCleanup() + + createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents, 0777) + + absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, DefaultDockerfileName) + + if err != nil { + t.Fatalf("Error when getting context from local dir: %s", err) + } + + if absContextDir != contextDir { + t.Fatalf("Absolute directory path should be equal to %s, got: %s", contextDir, absContextDir) + } + + if relDockerfile != DefaultDockerfileName { + t.Fatalf("Relative path to dockerfile should be equal to %s, got: %s", DefaultDockerfileName, relDockerfile) + } + +} + +func TestGetContextFromReaderString(t *testing.T) { + tarArchive, relDockerfile, err := GetContextFromReader(ioutil.NopCloser(strings.NewReader(dockerfileContents)), "") + + if err != nil { + t.Fatalf("Error when executing GetContextFromReader: %s", err) + } + + tarReader := tar.NewReader(tarArchive) + + _, err = tarReader.Next() + + if err != nil { + t.Fatalf("Error when reading tar archive: %s", err) + } + + buff := new(bytes.Buffer) + buff.ReadFrom(tarReader) + contents := buff.String() + + _, err = tarReader.Next() + + if err != io.EOF { + t.Fatalf("Tar stream too long: %s", err) + } + + if err = tarArchive.Close(); err != nil { + t.Fatalf("Error when closing tar stream: %s", err) + } + + if dockerfileContents != contents { + t.Fatalf("Uncompressed tar archive does not equal: %s, got: %s", dockerfileContents, contents) + } + + if relDockerfile != DefaultDockerfileName { + t.Fatalf("Relative path not equals %s, got: %s", DefaultDockerfileName, relDockerfile) + } +} + +func TestGetContextFromReaderTar(t *testing.T) { + contextDir, cleanup := createTestTempDir(t, "", "builder-context-test") + defer cleanup() + + createTestTempFile(t, contextDir, DefaultDockerfileName, dockerfileContents, 0777) + + tarStream, err := archive.Tar(contextDir, archive.Uncompressed) + + if err != nil { + t.Fatalf("Error when creating tar: %s", err) + } + + tarArchive, relDockerfile, err := GetContextFromReader(tarStream, DefaultDockerfileName) + + if err != nil { + t.Fatalf("Error when executing GetContextFromReader: %s", err) + } + + tarReader := tar.NewReader(tarArchive) + + header, err := tarReader.Next() + + if err != nil { + t.Fatalf("Error when reading tar archive: %s", err) + } + + if header.Name != DefaultDockerfileName { + t.Fatalf("Dockerfile name should be: %s, got: %s", DefaultDockerfileName, header.Name) + } + + buff := new(bytes.Buffer) + buff.ReadFrom(tarReader) + contents := buff.String() + + _, err = tarReader.Next() + + if err != io.EOF { + t.Fatalf("Tar stream too long: %s", err) + } + + if err = tarArchive.Close(); err != nil { + t.Fatalf("Error when closing tar stream: %s", err) + } + + if dockerfileContents != contents { + t.Fatalf("Uncompressed tar archive does not equal: %s, got: %s", dockerfileContents, contents) + } + + if relDockerfile != DefaultDockerfileName { + t.Fatalf("Relative path not equals %s, got: %s", DefaultDockerfileName, relDockerfile) + } +} + +func TestValidateContextDirectoryEmptyContext(t *testing.T) { + // This isn't a valid test on Windows. See https://play.golang.org/p/RR6z6jxR81. + // The test will ultimately end up calling filepath.Abs(""). On Windows, + // golang will error. On Linux, golang will return /. Due to there being + // drive letters on Windows, this is probably the correct behaviour for + // Windows. + if runtime.GOOS == "windows" { + t.Skip("Invalid test on Windows") + } + testValidateContextDirectory(t, prepareEmpty, []string{}) +} + +func TestValidateContextDirectoryContextWithNoFiles(t *testing.T) { + testValidateContextDirectory(t, prepareNoFiles, []string{}) +} + +func TestValidateContextDirectoryWithOneFile(t *testing.T) { + testValidateContextDirectory(t, prepareOneFile, []string{}) +} + +func TestValidateContextDirectoryWithOneFileExcludes(t *testing.T) { + testValidateContextDirectory(t, prepareOneFile, []string{DefaultDockerfileName}) +} + +// createTestTempDir creates a temporary directory for testing. +// It returns the created path and a cleanup function which is meant to be used as deferred call. +// When an error occurs, it terminates the test. +func createTestTempDir(t *testing.T, dir, prefix string) (string, func()) { + path, err := ioutil.TempDir(dir, prefix) + + if err != nil { + t.Fatalf("Error when creating directory %s with prefix %s: %s", dir, prefix, err) + } + + return path, func() { + err = os.RemoveAll(path) + + if err != nil { + t.Fatalf("Error when removing directory %s: %s", path, err) + } + } +} + +// createTestTempSubdir creates a temporary directory for testing. +// It returns the created path but doesn't provide a cleanup function, +// so createTestTempSubdir should be used only for creating temporary subdirectories +// whose parent directories are properly cleaned up. +// When an error occurs, it terminates the test. +func createTestTempSubdir(t *testing.T, dir, prefix string) string { + path, err := ioutil.TempDir(dir, prefix) + + if err != nil { + t.Fatalf("Error when creating directory %s with prefix %s: %s", dir, prefix, err) + } + + return path +} + +// createTestTempFile creates a temporary file within dir with specific contents and permissions. +// When an error occurs, it terminates the test +func createTestTempFile(t *testing.T, dir, filename, contents string, perm os.FileMode) string { + filePath := filepath.Join(dir, filename) + err := ioutil.WriteFile(filePath, []byte(contents), perm) + + if err != nil { + t.Fatalf("Error when creating %s file: %s", filename, err) + } + + return filePath +} + +// chdir changes current working directory to dir. +// It returns a function which changes working directory back to the previous one. +// This function is meant to be executed as a deferred call. +// When an error occurs, it terminates the test. +func chdir(t *testing.T, dir string) func() { + workingDirectory, err := os.Getwd() + + if err != nil { + t.Fatalf("Error when retrieving working directory: %s", err) + } + + err = os.Chdir(dir) + + if err != nil { + t.Fatalf("Error when changing directory to %s: %s", dir, err) + } + + return func() { + err = os.Chdir(workingDirectory) + + if err != nil { + t.Fatalf("Error when changing back to working directory (%s): %s", workingDirectory, err) + } + } +} diff --git a/components/cli/command/image/build/context_unix.go b/components/cli/command/image/build/context_unix.go new file mode 100644 index 0000000000..cb2634f079 --- /dev/null +++ b/components/cli/command/image/build/context_unix.go @@ -0,0 +1,11 @@ +// +build !windows + +package build + +import ( + "path/filepath" +) + +func getContextRoot(srcPath string) (string, error) { + return filepath.Join(srcPath, "."), nil +} diff --git a/components/cli/command/image/build/context_windows.go b/components/cli/command/image/build/context_windows.go new file mode 100644 index 0000000000..c577cfa7be --- /dev/null +++ b/components/cli/command/image/build/context_windows.go @@ -0,0 +1,17 @@ +// +build windows + +package build + +import ( + "path/filepath" + + "github.com/docker/docker/pkg/longpath" +) + +func getContextRoot(srcPath string) (string, error) { + cr, err := filepath.Abs(srcPath) + if err != nil { + return "", err + } + return longpath.AddPrefix(cr), nil +} From 50328acf85357beefabe74d7cc61b7973438ebdb Mon Sep 17 00:00:00 2001 From: allencloud Date: Fri, 23 Dec 2016 20:48:25 +0800 Subject: [PATCH 497/978] fix nits in comments Signed-off-by: allencloud Upstream-commit: 4e68d651b35689f1d583b5c7a95e61d5af2d3a34 Component: cli --- components/cli/command/swarm/unlock.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/swarm/unlock.go b/components/cli/command/swarm/unlock.go index f7d418760b..aa752e2148 100644 --- a/components/cli/command/swarm/unlock.go +++ b/components/cli/command/swarm/unlock.go @@ -37,7 +37,7 @@ func runUnlock(dockerCli *command.DockerCli, opts unlockOptions) error { client := dockerCli.Client() ctx := context.Background() - // First see if the node is actually part of a swarm, and if it's is actually locked first. + // First see if the node is actually part of a swarm, and if it is actually locked first. // If it's in any other state than locked, don't ask for the key. info, err := client.Info(ctx) if err != nil { From abab494a4fc01ae2bc7eb2c8596254d22c74b9f4 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Tue, 27 Dec 2016 12:51:00 -0800 Subject: [PATCH 498/978] Support for docker content trust for plugins Add integration test for docker content trust Signed-off-by: Derek McGowan (github: dmcgowan) Upstream-commit: fcaa89f296d768153418301ffff218c0501a27b3 Component: cli --- components/cli/command/container/create.go | 2 +- components/cli/command/image/build.go | 2 +- components/cli/command/image/trust.go | 23 ++++++++-- components/cli/command/plugin/install.go | 53 +++++++++++++++++++++- components/cli/command/plugin/push.go | 12 +++++ components/cli/trust/trust.go | 13 +++++- 6 files changed, 95 insertions(+), 10 deletions(-) diff --git a/components/cli/command/container/create.go b/components/cli/command/container/create.go index 7dc644d28c..d5e63bd9ef 100644 --- a/components/cli/command/container/create.go +++ b/components/cli/command/container/create.go @@ -170,7 +170,7 @@ func createContainer(ctx context.Context, dockerCli *command.DockerCli, config * if ref, ok := ref.(reference.NamedTagged); ok && command.IsTrusted() { var err error - trustedRef, err = image.TrustedReference(ctx, dockerCli, ref) + trustedRef, err = image.TrustedReference(ctx, dockerCli, ref, nil) if err != nil { return nil, err } diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go index e3e7ff2b02..0c88af5fcd 100644 --- a/components/cli/command/image/build.go +++ b/components/cli/command/image/build.go @@ -235,7 +235,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { var resolvedTags []*resolvedTag if command.IsTrusted() { translator := func(ctx context.Context, ref reference.NamedTagged) (reference.Canonical, error) { - return TrustedReference(ctx, dockerCli, ref) + return TrustedReference(ctx, dockerCli, ref, nil) } // Wrap the tar archive to replace the Dockerfile entry with the rewritten // Dockerfile which uses trusted pulls. diff --git a/components/cli/command/image/trust.go b/components/cli/command/image/trust.go index f32c301959..5136a22156 100644 --- a/components/cli/command/image/trust.go +++ b/components/cli/command/image/trust.go @@ -39,6 +39,11 @@ func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry defer responseBody.Close() + return PushTrustedReference(cli, repoInfo, ref, authConfig, responseBody) +} + +// PushTrustedReference pushes a canonical reference to the trust server. +func PushTrustedReference(cli *command.DockerCli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, in io.Reader) error { // If it is a trusted push we would like to find the target entry which match the // tag provided in the function and then do an AddTarget later. target := &client.Target{} @@ -75,14 +80,14 @@ func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry default: // We want trust signatures to always take an explicit tag, // otherwise it will act as an untrusted push. - if err = jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), nil); err != nil { + if err := jsonmessage.DisplayJSONMessagesToStream(in, cli.Out(), nil); err != nil { return err } fmt.Fprintln(cli.Out(), "No tag specified, skipping trust metadata push") return nil } - if err = jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), handleTarget); err != nil { + if err := jsonmessage.DisplayJSONMessagesToStream(in, cli.Out(), handleTarget); err != nil { return err } @@ -315,8 +320,16 @@ func imagePullPrivileged(ctx context.Context, cli *command.DockerCli, authConfig } // TrustedReference returns the canonical trusted reference for an image reference -func TrustedReference(ctx context.Context, cli *command.DockerCli, ref reference.NamedTagged) (reference.Canonical, error) { - repoInfo, err := registry.ParseRepositoryInfo(ref) +func TrustedReference(ctx context.Context, cli *command.DockerCli, ref reference.NamedTagged, rs registry.Service) (reference.Canonical, error) { + var ( + repoInfo *registry.RepositoryInfo + err error + ) + if rs != nil { + repoInfo, err = rs.ResolveRepository(ref) + } else { + repoInfo, err = registry.ParseRepositoryInfo(ref) + } if err != nil { return nil, err } @@ -332,7 +345,7 @@ func TrustedReference(ctx context.Context, cli *command.DockerCli, ref reference t, err := notaryRepo.GetTargetByName(ref.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole) if err != nil { - return nil, err + return nil, trust.NotaryError(repoInfo.FullName(), err) } // Only list tags in the top level targets role or the releases delegation role - ignore // all other delegation roles diff --git a/components/cli/command/plugin/install.go b/components/cli/command/plugin/install.go index 71bdeeff22..a64dc2525a 100644 --- a/components/cli/command/plugin/install.go +++ b/components/cli/command/plugin/install.go @@ -11,6 +11,7 @@ import ( registrytypes "github.com/docker/docker/api/types/registry" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/image" "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/reference" "github.com/docker/docker/registry" @@ -46,6 +47,8 @@ func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command { flags.BoolVar(&options.disable, "disable", false, "Do not enable the plugin on install") flags.StringVar(&options.alias, "alias", "", "Local name for plugin") + command.AddTrustedFlags(flags, true) + return cmd } @@ -63,6 +66,24 @@ func getRepoIndexFromUnnormalizedRef(ref distreference.Named) (*registrytypes.In return repoInfo.Index, nil } +type pluginRegistryService struct { + registry.Service +} + +func (s pluginRegistryService) ResolveRepository(name reference.Named) (repoInfo *registry.RepositoryInfo, err error) { + repoInfo, err = s.Service.ResolveRepository(name) + if repoInfo != nil { + repoInfo.Class = "plugin" + } + return +} + +func newRegistryService() registry.Service { + return pluginRegistryService{ + Service: registry.NewService(registry.ServiceOptions{V2Only: true}), + } +} + func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error { // Parse name using distribution reference package to support name // containing both tag and digest. Names with both tag and digest @@ -85,13 +106,41 @@ func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error { } alias = aref.String() } + ctx := context.Background() index, err := getRepoIndexFromUnnormalizedRef(ref) if err != nil { return err } - ctx := context.Background() + remote := ref.String() + + _, isCanonical := ref.(distreference.Canonical) + if command.IsTrusted() && !isCanonical { + if alias == "" { + alias = ref.String() + } + var nt reference.NamedTagged + named, err := reference.ParseNamed(ref.Name()) + if err != nil { + return err + } + if tagged, ok := ref.(distreference.Tagged); ok { + nt, err = reference.WithTag(named, tagged.Tag()) + if err != nil { + return err + } + } else { + named = reference.WithDefaultTag(named) + nt = named.(reference.NamedTagged) + } + + trusted, err := image.TrustedReference(ctx, dockerCli, nt, newRegistryService()) + if err != nil { + return err + } + remote = trusted.String() + } authConfig := command.ResolveAuthConfig(ctx, dockerCli, index) @@ -104,7 +153,7 @@ func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error { options := types.PluginInstallOptions{ RegistryAuth: encodedAuth, - RemoteRef: ref.String(), + RemoteRef: remote, Disabled: opts.disable, AcceptAllPermissions: opts.grantPerms, AcceptPermissionsFunc: acceptPrivileges(dockerCli, opts.name), diff --git a/components/cli/command/plugin/push.go b/components/cli/command/plugin/push.go index 667379cdd2..b0766307f3 100644 --- a/components/cli/command/plugin/push.go +++ b/components/cli/command/plugin/push.go @@ -7,6 +7,7 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/image" "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/reference" "github.com/docker/docker/registry" @@ -22,6 +23,11 @@ func newPushCommand(dockerCli *command.DockerCli) *cobra.Command { return runPush(dockerCli, args[0]) }, } + + flags := cmd.Flags() + + command.AddTrustedFlags(flags, true) + return cmd } @@ -55,5 +61,11 @@ func runPush(dockerCli *command.DockerCli, name string) error { return err } defer responseBody.Close() + + if command.IsTrusted() { + repoInfo.Class = "plugin" + return image.PushTrustedReference(dockerCli, repoInfo, named, authConfig, responseBody) + } + return jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil) } diff --git a/components/cli/trust/trust.go b/components/cli/trust/trust.go index 0f3482f2d7..51914f74b0 100644 --- a/components/cli/trust/trust.go +++ b/components/cli/trust/trust.go @@ -147,8 +147,19 @@ func GetNotaryRepository(streams command.Streams, repoInfo *registry.RepositoryI } } + scope := auth.RepositoryScope{ + Repository: repoInfo.FullName(), + Actions: actions, + Class: repoInfo.Class, + } creds := simpleCredentialStore{auth: authConfig} - tokenHandler := auth.NewTokenHandler(authTransport, creds, repoInfo.FullName(), actions...) + tokenHandlerOptions := auth.TokenHandlerOptions{ + Transport: authTransport, + Credentials: creds, + Scopes: []auth.Scope{scope}, + ClientID: registry.AuthClientID, + } + tokenHandler := auth.NewTokenHandlerWithOptions(tokenHandlerOptions) basicHandler := auth.NewBasicHandler(creds) modifiers = append(modifiers, transport.RequestModifier(auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))) tr := transport.NewTransport(base, modifiers...) From 6a9af71e6e83db959aa440a040fe490cbc502854 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 20 Dec 2016 16:26:49 -0500 Subject: [PATCH 499/978] Replace vendor of aanand/compose-file with a local copy. Add go-bindata for including the schema. Signed-off-by: Daniel Nephin Upstream-commit: 52c01570361c5f2dc1bb03623baa31f42dc912e3 Component: cli --- components/cli/command/stack/deploy.go | 4 +- components/cli/compose/convert/compose.go | 2 +- .../cli/compose/convert/compose_test.go | 2 +- components/cli/compose/convert/service.go | 2 +- .../cli/compose/convert/service_test.go | 2 +- components/cli/compose/convert/volume.go | 2 +- components/cli/compose/convert/volume_test.go | 2 +- .../compose/interpolation/interpolation.go | 90 ++ .../interpolation/interpolation_test.go | 59 ++ components/cli/compose/loader/example1.env | 8 + components/cli/compose/loader/example2.env | 1 + .../cli/compose/loader/full-example.yml | 287 +++++++ components/cli/compose/loader/loader.go | 611 ++++++++++++++ components/cli/compose/loader/loader_test.go | 782 ++++++++++++++++++ components/cli/compose/schema/bindata.go | 237 ++++++ .../schema/data/config_schema_v3.0.json | 379 +++++++++ components/cli/compose/schema/schema.go | 113 +++ components/cli/compose/schema/schema_test.go | 35 + components/cli/compose/template/template.go | 100 +++ .../cli/compose/template/template_test.go | 83 ++ components/cli/compose/types/types.go | 232 ++++++ 21 files changed, 3025 insertions(+), 8 deletions(-) create mode 100644 components/cli/compose/interpolation/interpolation.go create mode 100644 components/cli/compose/interpolation/interpolation_test.go create mode 100644 components/cli/compose/loader/example1.env create mode 100644 components/cli/compose/loader/example2.env create mode 100644 components/cli/compose/loader/full-example.yml create mode 100644 components/cli/compose/loader/loader.go create mode 100644 components/cli/compose/loader/loader_test.go create mode 100644 components/cli/compose/schema/bindata.go create mode 100644 components/cli/compose/schema/data/config_schema_v3.0.json create mode 100644 components/cli/compose/schema/schema.go create mode 100644 components/cli/compose/schema/schema_test.go create mode 100644 components/cli/compose/template/template.go create mode 100644 components/cli/compose/template/template_test.go create mode 100644 components/cli/compose/types/types.go diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index 32ebd62d3f..f4730db556 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -11,13 +11,13 @@ import ( "github.com/spf13/cobra" "golang.org/x/net/context" - "github.com/aanand/compose-file/loader" - composetypes "github.com/aanand/compose-file/types" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/compose/convert" + "github.com/docker/docker/cli/compose/loader" + composetypes "github.com/docker/docker/cli/compose/types" dockerclient "github.com/docker/docker/client" ) diff --git a/components/cli/compose/convert/compose.go b/components/cli/compose/convert/compose.go index e0684482b8..7c410844c7 100644 --- a/components/cli/compose/convert/compose.go +++ b/components/cli/compose/convert/compose.go @@ -1,9 +1,9 @@ package convert import ( - composetypes "github.com/aanand/compose-file/types" "github.com/docker/docker/api/types" networktypes "github.com/docker/docker/api/types/network" + composetypes "github.com/docker/docker/cli/compose/types" ) const ( diff --git a/components/cli/compose/convert/compose_test.go b/components/cli/compose/convert/compose_test.go index 8f8e8ea6d8..27a67047d8 100644 --- a/components/cli/compose/convert/compose_test.go +++ b/components/cli/compose/convert/compose_test.go @@ -3,9 +3,9 @@ package convert import ( "testing" - composetypes "github.com/aanand/compose-file/types" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/network" + composetypes "github.com/docker/docker/cli/compose/types" "github.com/docker/docker/pkg/testutil/assert" ) diff --git a/components/cli/compose/convert/service.go b/components/cli/compose/convert/service.go index 458b518a46..2a8ed8288d 100644 --- a/components/cli/compose/convert/service.go +++ b/components/cli/compose/convert/service.go @@ -4,9 +4,9 @@ import ( "fmt" "time" - composetypes "github.com/aanand/compose-file/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/swarm" + composetypes "github.com/docker/docker/cli/compose/types" "github.com/docker/docker/opts" runconfigopts "github.com/docker/docker/runconfig/opts" "github.com/docker/go-connections/nat" diff --git a/components/cli/compose/convert/service_test.go b/components/cli/compose/convert/service_test.go index a6884917de..45da764325 100644 --- a/components/cli/compose/convert/service_test.go +++ b/components/cli/compose/convert/service_test.go @@ -6,9 +6,9 @@ import ( "testing" "time" - composetypes "github.com/aanand/compose-file/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/swarm" + composetypes "github.com/docker/docker/cli/compose/types" "github.com/docker/docker/pkg/testutil/assert" ) diff --git a/components/cli/compose/convert/volume.go b/components/cli/compose/convert/volume.go index 3a7504106a..24442d4dc7 100644 --- a/components/cli/compose/convert/volume.go +++ b/components/cli/compose/convert/volume.go @@ -4,8 +4,8 @@ import ( "fmt" "strings" - composetypes "github.com/aanand/compose-file/types" "github.com/docker/docker/api/types/mount" + composetypes "github.com/docker/docker/cli/compose/types" ) type volumes map[string]composetypes.VolumeConfig diff --git a/components/cli/compose/convert/volume_test.go b/components/cli/compose/convert/volume_test.go index bcbfb08b95..1132136b22 100644 --- a/components/cli/compose/convert/volume_test.go +++ b/components/cli/compose/convert/volume_test.go @@ -3,8 +3,8 @@ package convert import ( "testing" - composetypes "github.com/aanand/compose-file/types" "github.com/docker/docker/api/types/mount" + composetypes "github.com/docker/docker/cli/compose/types" "github.com/docker/docker/pkg/testutil/assert" ) diff --git a/components/cli/compose/interpolation/interpolation.go b/components/cli/compose/interpolation/interpolation.go new file mode 100644 index 0000000000..734f28ec9d --- /dev/null +++ b/components/cli/compose/interpolation/interpolation.go @@ -0,0 +1,90 @@ +package interpolation + +import ( + "fmt" + + "github.com/docker/docker/cli/compose/template" + "github.com/docker/docker/cli/compose/types" +) + +// Interpolate replaces variables in a string with the values from a mapping +func Interpolate(config types.Dict, section string, mapping template.Mapping) (types.Dict, error) { + out := types.Dict{} + + for name, item := range config { + if item == nil { + out[name] = nil + continue + } + interpolatedItem, err := interpolateSectionItem(name, item.(types.Dict), section, mapping) + if err != nil { + return nil, err + } + out[name] = interpolatedItem + } + + return out, nil +} + +func interpolateSectionItem( + name string, + item types.Dict, + section string, + mapping template.Mapping, +) (types.Dict, error) { + + out := types.Dict{} + + for key, value := range item { + interpolatedValue, err := recursiveInterpolate(value, mapping) + if err != nil { + return nil, fmt.Errorf( + "Invalid interpolation format for %#v option in %s %#v: %#v", + key, section, name, err.Template, + ) + } + out[key] = interpolatedValue + } + + return out, nil + +} + +func recursiveInterpolate( + value interface{}, + mapping template.Mapping, +) (interface{}, *template.InvalidTemplateError) { + + switch value := value.(type) { + + case string: + return template.Substitute(value, mapping) + + case types.Dict: + out := types.Dict{} + for key, elem := range value { + interpolatedElem, err := recursiveInterpolate(elem, mapping) + if err != nil { + return nil, err + } + out[key] = interpolatedElem + } + return out, nil + + case []interface{}: + out := make([]interface{}, len(value)) + for i, elem := range value { + interpolatedElem, err := recursiveInterpolate(elem, mapping) + if err != nil { + return nil, err + } + out[i] = interpolatedElem + } + return out, nil + + default: + return value, nil + + } + +} diff --git a/components/cli/compose/interpolation/interpolation_test.go b/components/cli/compose/interpolation/interpolation_test.go new file mode 100644 index 0000000000..c3921701b3 --- /dev/null +++ b/components/cli/compose/interpolation/interpolation_test.go @@ -0,0 +1,59 @@ +package interpolation + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/docker/docker/cli/compose/types" +) + +var defaults = map[string]string{ + "USER": "jenny", + "FOO": "bar", +} + +func defaultMapping(name string) (string, bool) { + val, ok := defaults[name] + return val, ok +} + +func TestInterpolate(t *testing.T) { + services := types.Dict{ + "servicea": types.Dict{ + "image": "example:${USER}", + "volumes": []interface{}{"$FOO:/target"}, + "logging": types.Dict{ + "driver": "${FOO}", + "options": types.Dict{ + "user": "$USER", + }, + }, + }, + } + expected := types.Dict{ + "servicea": types.Dict{ + "image": "example:jenny", + "volumes": []interface{}{"bar:/target"}, + "logging": types.Dict{ + "driver": "bar", + "options": types.Dict{ + "user": "jenny", + }, + }, + }, + } + result, err := Interpolate(services, "service", defaultMapping) + assert.NoError(t, err) + assert.Equal(t, expected, result) +} + +func TestInvalidInterpolation(t *testing.T) { + services := types.Dict{ + "servicea": types.Dict{ + "image": "${", + }, + } + _, err := Interpolate(services, "service", defaultMapping) + assert.EqualError(t, err, `Invalid interpolation format for "image" option in service "servicea": "${"`) +} diff --git a/components/cli/compose/loader/example1.env b/components/cli/compose/loader/example1.env new file mode 100644 index 0000000000..3e7a059613 --- /dev/null +++ b/components/cli/compose/loader/example1.env @@ -0,0 +1,8 @@ +# passed through +FOO=1 + +# overridden in example2.env +BAR=1 + +# overridden in full-example.yml +BAZ=1 diff --git a/components/cli/compose/loader/example2.env b/components/cli/compose/loader/example2.env new file mode 100644 index 0000000000..0920d5ab05 --- /dev/null +++ b/components/cli/compose/loader/example2.env @@ -0,0 +1 @@ +BAR=2 diff --git a/components/cli/compose/loader/full-example.yml b/components/cli/compose/loader/full-example.yml new file mode 100644 index 0000000000..fb5686a380 --- /dev/null +++ b/components/cli/compose/loader/full-example.yml @@ -0,0 +1,287 @@ +version: "3" + +services: + foo: + cap_add: + - ALL + + cap_drop: + - NET_ADMIN + - SYS_ADMIN + + cgroup_parent: m-executor-abcd + + # String or list + command: bundle exec thin -p 3000 + # command: ["bundle", "exec", "thin", "-p", "3000"] + + container_name: my-web-container + + depends_on: + - db + - redis + + deploy: + mode: replicated + replicas: 6 + labels: [FOO=BAR] + update_config: + parallelism: 3 + delay: 10s + failure_action: continue + monitor: 60s + max_failure_ratio: 0.3 + resources: + limits: + cpus: '0.001' + memory: 50M + reservations: + cpus: '0.0001' + memory: 20M + restart_policy: + condition: on_failure + delay: 5s + max_attempts: 3 + window: 120s + placement: + constraints: [node=foo] + + devices: + - "/dev/ttyUSB0:/dev/ttyUSB0" + + # String or list + # dns: 8.8.8.8 + dns: + - 8.8.8.8 + - 9.9.9.9 + + # String or list + # dns_search: example.com + dns_search: + - dc1.example.com + - dc2.example.com + + domainname: foo.com + + # String or list + # entrypoint: /code/entrypoint.sh -p 3000 + entrypoint: ["/code/entrypoint.sh", "-p", "3000"] + + # String or list + # env_file: .env + env_file: + - ./example1.env + - ./example2.env + + # Mapping or list + # Mapping values can be strings, numbers or null + # Booleans are not allowed - must be quoted + environment: + RACK_ENV: development + SHOW: 'true' + SESSION_SECRET: + BAZ: 3 + # environment: + # - RACK_ENV=development + # - SHOW=true + # - SESSION_SECRET + + # Items can be strings or numbers + expose: + - "3000" + - 8000 + + external_links: + - redis_1 + - project_db_1:mysql + - project_db_1:postgresql + + # Mapping or list + # Mapping values must be strings + # extra_hosts: + # somehost: "162.242.195.82" + # otherhost: "50.31.209.229" + extra_hosts: + - "somehost:162.242.195.82" + - "otherhost:50.31.209.229" + + hostname: foo + + healthcheck: + test: echo "hello world" + interval: 10s + timeout: 1s + retries: 5 + + # Any valid image reference - repo, tag, id, sha + image: redis + # image: ubuntu:14.04 + # image: tutum/influxdb + # image: example-registry.com:4000/postgresql + # image: a4bc65fd + # image: busybox@sha256:38a203e1986cf79639cfb9b2e1d6e773de84002feea2d4eb006b52004ee8502d + + ipc: host + + # Mapping or list + # Mapping values can be strings, numbers or null + labels: + com.example.description: "Accounting webapp" + com.example.number: 42 + com.example.empty-label: + # labels: + # - "com.example.description=Accounting webapp" + # - "com.example.number=42" + # - "com.example.empty-label" + + links: + - db + - db:database + - redis + + logging: + driver: syslog + options: + syslog-address: "tcp://192.168.0.42:123" + + mac_address: 02:42:ac:11:65:43 + + # network_mode: "bridge" + # network_mode: "host" + # network_mode: "none" + # Use the network mode of an arbitrary container from another service + # network_mode: "service:db" + # Use the network mode of another container, specified by name or id + # network_mode: "container:some-container" + network_mode: "container:0cfeab0f748b9a743dc3da582046357c6ef497631c1a016d28d2bf9b4f899f7b" + + networks: + some-network: + aliases: + - alias1 + - alias3 + other-network: + ipv4_address: 172.16.238.10 + ipv6_address: 2001:3984:3989::10 + other-other-network: + + pid: "host" + + ports: + - 3000 + - "3000-3005" + - "8000:8000" + - "9090-9091:8080-8081" + - "49100:22" + - "127.0.0.1:8001:8001" + - "127.0.0.1:5000-5010:5000-5010" + + privileged: true + + read_only: true + + restart: always + + security_opt: + - label=level:s0:c100,c200 + - label=type:svirt_apache_t + + stdin_open: true + + stop_grace_period: 20s + + stop_signal: SIGUSR1 + + # String or list + # tmpfs: /run + tmpfs: + - /run + - /tmp + + tty: true + + ulimits: + # Single number or mapping with soft + hard limits + nproc: 65535 + nofile: + soft: 20000 + hard: 40000 + + user: someone + + volumes: + # Just specify a path and let the Engine create a volume + - /var/lib/mysql + # Specify an absolute path mapping + - /opt/data:/var/lib/mysql + # Path on the host, relative to the Compose file + - .:/code + - ./static:/var/www/html + # User-relative path + - ~/configs:/etc/configs/:ro + # Named volume + - datavolume:/var/lib/mysql + + working_dir: /code + +networks: + # Entries can be null, which specifies simply that a network + # called "{project name}_some-network" should be created and + # use the default driver + some-network: + + other-network: + driver: overlay + + driver_opts: + # Values can be strings or numbers + foo: "bar" + baz: 1 + + ipam: + driver: overlay + # driver_opts: + # # Values can be strings or numbers + # com.docker.network.enable_ipv6: "true" + # com.docker.network.numeric_value: 1 + config: + - subnet: 172.16.238.0/24 + # gateway: 172.16.238.1 + - subnet: 2001:3984:3989::/64 + # gateway: 2001:3984:3989::1 + + external-network: + # Specifies that a pre-existing network called "external-network" + # can be referred to within this file as "external-network" + external: true + + other-external-network: + # Specifies that a pre-existing network called "my-cool-network" + # can be referred to within this file as "other-external-network" + external: + name: my-cool-network + +volumes: + # Entries can be null, which specifies simply that a volume + # called "{project name}_some-volume" should be created and + # use the default driver + some-volume: + + other-volume: + driver: flocker + + driver_opts: + # Values can be strings or numbers + foo: "bar" + baz: 1 + + external-volume: + # Specifies that a pre-existing volume called "external-volume" + # can be referred to within this file as "external-volume" + external: true + + other-external-volume: + # Specifies that a pre-existing volume called "my-cool-volume" + # can be referred to within this file as "other-external-volume" + external: + name: my-cool-volume diff --git a/components/cli/compose/loader/loader.go b/components/cli/compose/loader/loader.go new file mode 100644 index 0000000000..9e46b97594 --- /dev/null +++ b/components/cli/compose/loader/loader.go @@ -0,0 +1,611 @@ +package loader + +import ( + "fmt" + "os" + "path" + "reflect" + "regexp" + "sort" + "strings" + + "github.com/docker/docker/cli/compose/interpolation" + "github.com/docker/docker/cli/compose/schema" + "github.com/docker/docker/cli/compose/types" + "github.com/docker/docker/runconfig/opts" + units "github.com/docker/go-units" + shellwords "github.com/mattn/go-shellwords" + "github.com/mitchellh/mapstructure" + yaml "gopkg.in/yaml.v2" +) + +var ( + fieldNameRegexp = regexp.MustCompile("[A-Z][a-z0-9]+") +) + +// ParseYAML reads the bytes from a file, parses the bytes into a mapping +// structure, and returns it. +func ParseYAML(source []byte) (types.Dict, error) { + var cfg interface{} + if err := yaml.Unmarshal(source, &cfg); err != nil { + return nil, err + } + cfgMap, ok := cfg.(map[interface{}]interface{}) + if !ok { + return nil, fmt.Errorf("Top-level object must be a mapping") + } + converted, err := convertToStringKeysRecursive(cfgMap, "") + if err != nil { + return nil, err + } + return converted.(types.Dict), nil +} + +// Load reads a ConfigDetails and returns a fully loaded configuration +func Load(configDetails types.ConfigDetails) (*types.Config, error) { + if len(configDetails.ConfigFiles) < 1 { + return nil, fmt.Errorf("No files specified") + } + if len(configDetails.ConfigFiles) > 1 { + return nil, fmt.Errorf("Multiple files are not yet supported") + } + + configDict := getConfigDict(configDetails) + + if services, ok := configDict["services"]; ok { + if servicesDict, ok := services.(types.Dict); ok { + forbidden := getProperties(servicesDict, types.ForbiddenProperties) + + if len(forbidden) > 0 { + return nil, &ForbiddenPropertiesError{Properties: forbidden} + } + } + } + + if err := schema.Validate(configDict); err != nil { + return nil, err + } + + cfg := types.Config{} + version := configDict["version"].(string) + if version != "3" && version != "3.0" { + return nil, fmt.Errorf(`Unsupported Compose file version: %#v. The only version supported is "3" (or "3.0")`, version) + } + + if services, ok := configDict["services"]; ok { + servicesConfig, err := interpolation.Interpolate(services.(types.Dict), "service", os.LookupEnv) + if err != nil { + return nil, err + } + + servicesList, err := loadServices(servicesConfig, configDetails.WorkingDir) + if err != nil { + return nil, err + } + + cfg.Services = servicesList + } + + if networks, ok := configDict["networks"]; ok { + networksConfig, err := interpolation.Interpolate(networks.(types.Dict), "network", os.LookupEnv) + if err != nil { + return nil, err + } + + networksMapping, err := loadNetworks(networksConfig) + if err != nil { + return nil, err + } + + cfg.Networks = networksMapping + } + + if volumes, ok := configDict["volumes"]; ok { + volumesConfig, err := interpolation.Interpolate(volumes.(types.Dict), "volume", os.LookupEnv) + if err != nil { + return nil, err + } + + volumesMapping, err := loadVolumes(volumesConfig) + if err != nil { + return nil, err + } + + cfg.Volumes = volumesMapping + } + + return &cfg, nil +} + +// GetUnsupportedProperties returns the list of any unsupported properties that are +// used in the Compose files. +func GetUnsupportedProperties(configDetails types.ConfigDetails) []string { + unsupported := map[string]bool{} + + for _, service := range getServices(getConfigDict(configDetails)) { + serviceDict := service.(types.Dict) + for _, property := range types.UnsupportedProperties { + if _, isSet := serviceDict[property]; isSet { + unsupported[property] = true + } + } + } + + return sortedKeys(unsupported) +} + +func sortedKeys(set map[string]bool) []string { + var keys []string + for key := range set { + keys = append(keys, key) + } + sort.Strings(keys) + return keys +} + +// GetDeprecatedProperties returns the list of any deprecated properties that +// are used in the compose files. +func GetDeprecatedProperties(configDetails types.ConfigDetails) map[string]string { + return getProperties(getServices(getConfigDict(configDetails)), types.DeprecatedProperties) +} + +func getProperties(services types.Dict, propertyMap map[string]string) map[string]string { + output := map[string]string{} + + for _, service := range services { + if serviceDict, ok := service.(types.Dict); ok { + for property, description := range propertyMap { + if _, isSet := serviceDict[property]; isSet { + output[property] = description + } + } + } + } + + return output +} + +// ForbiddenPropertiesError is returned when there are properties in the Compose +// file that are forbidden. +type ForbiddenPropertiesError struct { + Properties map[string]string +} + +func (e *ForbiddenPropertiesError) Error() string { + return "Configuration contains forbidden properties" +} + +// TODO: resolve multiple files into a single config +func getConfigDict(configDetails types.ConfigDetails) types.Dict { + return configDetails.ConfigFiles[0].Config +} + +func getServices(configDict types.Dict) types.Dict { + if services, ok := configDict["services"]; ok { + if servicesDict, ok := services.(types.Dict); ok { + return servicesDict + } + } + + return types.Dict{} +} + +func transform(source map[string]interface{}, target interface{}) error { + data := mapstructure.Metadata{} + config := &mapstructure.DecoderConfig{ + DecodeHook: mapstructure.ComposeDecodeHookFunc( + transformHook, + mapstructure.StringToTimeDurationHookFunc()), + Result: target, + Metadata: &data, + } + decoder, err := mapstructure.NewDecoder(config) + if err != nil { + return err + } + err = decoder.Decode(source) + // TODO: log unused keys + return err +} + +func transformHook( + source reflect.Type, + target reflect.Type, + data interface{}, +) (interface{}, error) { + switch target { + case reflect.TypeOf(types.External{}): + return transformExternal(source, target, data) + case reflect.TypeOf(make(map[string]string, 0)): + return transformMapStringString(source, target, data) + case reflect.TypeOf(types.UlimitsConfig{}): + return transformUlimits(source, target, data) + case reflect.TypeOf(types.UnitBytes(0)): + return loadSize(data) + } + switch target.Kind() { + case reflect.Struct: + return transformStruct(source, target, data) + } + return data, nil +} + +// keys needs to be converted to strings for jsonschema +// TODO: don't use types.Dict +func convertToStringKeysRecursive(value interface{}, keyPrefix string) (interface{}, error) { + if mapping, ok := value.(map[interface{}]interface{}); ok { + dict := make(types.Dict) + for key, entry := range mapping { + str, ok := key.(string) + if !ok { + var location string + if keyPrefix == "" { + location = "at top level" + } else { + location = fmt.Sprintf("in %s", keyPrefix) + } + return nil, fmt.Errorf("Non-string key %s: %#v", location, key) + } + var newKeyPrefix string + if keyPrefix == "" { + newKeyPrefix = str + } else { + newKeyPrefix = fmt.Sprintf("%s.%s", keyPrefix, str) + } + convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix) + if err != nil { + return nil, err + } + dict[str] = convertedEntry + } + return dict, nil + } + if list, ok := value.([]interface{}); ok { + var convertedList []interface{} + for index, entry := range list { + newKeyPrefix := fmt.Sprintf("%s[%d]", keyPrefix, index) + convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix) + if err != nil { + return nil, err + } + convertedList = append(convertedList, convertedEntry) + } + return convertedList, nil + } + return value, nil +} + +func loadServices(servicesDict types.Dict, workingDir string) ([]types.ServiceConfig, error) { + var services []types.ServiceConfig + + for name, serviceDef := range servicesDict { + serviceConfig, err := loadService(name, serviceDef.(types.Dict), workingDir) + if err != nil { + return nil, err + } + services = append(services, *serviceConfig) + } + + return services, nil +} + +func loadService(name string, serviceDict types.Dict, workingDir string) (*types.ServiceConfig, error) { + serviceConfig := &types.ServiceConfig{} + if err := transform(serviceDict, serviceConfig); err != nil { + return nil, err + } + serviceConfig.Name = name + + if err := resolveEnvironment(serviceConfig, serviceDict, workingDir); err != nil { + return nil, err + } + + if err := resolveVolumePaths(serviceConfig.Volumes, workingDir); err != nil { + return nil, err + } + + return serviceConfig, nil +} + +func resolveEnvironment(serviceConfig *types.ServiceConfig, serviceDict types.Dict, workingDir string) error { + environment := make(map[string]string) + + if envFileVal, ok := serviceDict["env_file"]; ok { + envFiles := loadStringOrListOfStrings(envFileVal) + + var envVars []string + + for _, file := range envFiles { + filePath := path.Join(workingDir, file) + fileVars, err := opts.ParseEnvFile(filePath) + if err != nil { + return err + } + envVars = append(envVars, fileVars...) + } + + for k, v := range opts.ConvertKVStringsToMap(envVars) { + environment[k] = v + } + } + + for k, v := range serviceConfig.Environment { + environment[k] = v + } + + serviceConfig.Environment = environment + + return nil +} + +func resolveVolumePaths(volumes []string, workingDir string) error { + for i, mapping := range volumes { + parts := strings.SplitN(mapping, ":", 2) + if len(parts) == 1 { + continue + } + + if strings.HasPrefix(parts[0], ".") { + parts[0] = path.Join(workingDir, parts[0]) + } + parts[0] = expandUser(parts[0]) + + volumes[i] = strings.Join(parts, ":") + } + + return nil +} + +// TODO: make this more robust +func expandUser(path string) string { + if strings.HasPrefix(path, "~") { + return strings.Replace(path, "~", os.Getenv("HOME"), 1) + } + return path +} + +func transformUlimits( + source reflect.Type, + target reflect.Type, + data interface{}, +) (interface{}, error) { + switch value := data.(type) { + case int: + return types.UlimitsConfig{Single: value}, nil + case types.Dict: + ulimit := types.UlimitsConfig{} + ulimit.Soft = value["soft"].(int) + ulimit.Hard = value["hard"].(int) + return ulimit, nil + default: + return data, fmt.Errorf("invalid type %T for ulimits", value) + } +} + +func loadNetworks(source types.Dict) (map[string]types.NetworkConfig, error) { + networks := make(map[string]types.NetworkConfig) + err := transform(source, &networks) + if err != nil { + return networks, err + } + for name, network := range networks { + if network.External.External && network.External.Name == "" { + network.External.Name = name + networks[name] = network + } + } + return networks, nil +} + +func loadVolumes(source types.Dict) (map[string]types.VolumeConfig, error) { + volumes := make(map[string]types.VolumeConfig) + err := transform(source, &volumes) + if err != nil { + return volumes, err + } + for name, volume := range volumes { + if volume.External.External && volume.External.Name == "" { + volume.External.Name = name + volumes[name] = volume + } + } + return volumes, nil +} + +func transformStruct( + source reflect.Type, + target reflect.Type, + data interface{}, +) (interface{}, error) { + structValue, ok := data.(map[string]interface{}) + if !ok { + // FIXME: this is necessary because of convertToStringKeysRecursive + structValue, ok = data.(types.Dict) + if !ok { + panic(fmt.Sprintf( + "transformStruct called with non-map type: %T, %s", data, data)) + } + } + + var err error + for i := 0; i < target.NumField(); i++ { + field := target.Field(i) + fieldTag := field.Tag.Get("compose") + + yamlName := toYAMLName(field.Name) + value, ok := structValue[yamlName] + if !ok { + continue + } + + structValue[yamlName], err = convertField( + fieldTag, reflect.TypeOf(value), field.Type, value) + if err != nil { + return nil, fmt.Errorf("field %s: %s", yamlName, err.Error()) + } + } + return structValue, nil +} + +func transformMapStringString( + source reflect.Type, + target reflect.Type, + data interface{}, +) (interface{}, error) { + switch value := data.(type) { + case map[string]interface{}: + return toMapStringString(value), nil + case types.Dict: + return toMapStringString(value), nil + case map[string]string: + return value, nil + default: + return data, fmt.Errorf("invalid type %T for map[string]string", value) + } +} + +func convertField( + fieldTag string, + source reflect.Type, + target reflect.Type, + data interface{}, +) (interface{}, error) { + switch fieldTag { + case "": + return data, nil + case "healthcheck": + return loadHealthcheck(data) + case "list_or_dict_equals": + return loadMappingOrList(data, "="), nil + case "list_or_dict_colon": + return loadMappingOrList(data, ":"), nil + case "list_or_struct_map": + return loadListOrStructMap(data, target) + case "string_or_list": + return loadStringOrListOfStrings(data), nil + case "list_of_strings_or_numbers": + return loadListOfStringsOrNumbers(data), nil + case "shell_command": + return loadShellCommand(data) + case "size": + return loadSize(data) + case "-": + return nil, nil + } + return data, nil +} + +func transformExternal( + source reflect.Type, + target reflect.Type, + data interface{}, +) (interface{}, error) { + switch value := data.(type) { + case bool: + return map[string]interface{}{"external": value}, nil + case types.Dict: + return map[string]interface{}{"external": true, "name": value["name"]}, nil + case map[string]interface{}: + return map[string]interface{}{"external": true, "name": value["name"]}, nil + default: + return data, fmt.Errorf("invalid type %T for external", value) + } +} + +func toYAMLName(name string) string { + nameParts := fieldNameRegexp.FindAllString(name, -1) + for i, p := range nameParts { + nameParts[i] = strings.ToLower(p) + } + return strings.Join(nameParts, "_") +} + +func loadListOrStructMap(value interface{}, target reflect.Type) (interface{}, error) { + if list, ok := value.([]interface{}); ok { + mapValue := map[interface{}]interface{}{} + for _, name := range list { + mapValue[name] = nil + } + return mapValue, nil + } + + return value, nil +} + +func loadListOfStringsOrNumbers(value interface{}) []string { + list := value.([]interface{}) + result := make([]string, len(list)) + for i, item := range list { + result[i] = fmt.Sprint(item) + } + return result +} + +func loadStringOrListOfStrings(value interface{}) []string { + if list, ok := value.([]interface{}); ok { + result := make([]string, len(list)) + for i, item := range list { + result[i] = fmt.Sprint(item) + } + return result + } + return []string{value.(string)} +} + +func loadMappingOrList(mappingOrList interface{}, sep string) map[string]string { + if mapping, ok := mappingOrList.(types.Dict); ok { + return toMapStringString(mapping) + } + if list, ok := mappingOrList.([]interface{}); ok { + result := make(map[string]string) + for _, value := range list { + parts := strings.SplitN(value.(string), sep, 2) + if len(parts) == 1 { + result[parts[0]] = "" + } else { + result[parts[0]] = parts[1] + } + } + return result + } + panic(fmt.Errorf("expected a map or a slice, got: %#v", mappingOrList)) +} + +func loadShellCommand(value interface{}) (interface{}, error) { + if str, ok := value.(string); ok { + return shellwords.Parse(str) + } + return value, nil +} + +func loadHealthcheck(value interface{}) (interface{}, error) { + if str, ok := value.(string); ok { + return append([]string{"CMD-SHELL"}, str), nil + } + return value, nil +} + +func loadSize(value interface{}) (int64, error) { + switch value := value.(type) { + case int: + return int64(value), nil + case string: + return units.RAMInBytes(value) + } + panic(fmt.Errorf("invalid type for size %T", value)) +} + +func toMapStringString(value map[string]interface{}) map[string]string { + output := make(map[string]string) + for key, value := range value { + output[key] = toString(value) + } + return output +} + +func toString(value interface{}) string { + if value == nil { + return "" + } + return fmt.Sprint(value) +} diff --git a/components/cli/compose/loader/loader_test.go b/components/cli/compose/loader/loader_test.go new file mode 100644 index 0000000000..e15be7c549 --- /dev/null +++ b/components/cli/compose/loader/loader_test.go @@ -0,0 +1,782 @@ +package loader + +import ( + "fmt" + "io/ioutil" + "os" + "sort" + "testing" + "time" + + "github.com/docker/docker/cli/compose/types" + "github.com/stretchr/testify/assert" +) + +func buildConfigDetails(source types.Dict) types.ConfigDetails { + workingDir, err := os.Getwd() + if err != nil { + panic(err) + } + + return types.ConfigDetails{ + WorkingDir: workingDir, + ConfigFiles: []types.ConfigFile{ + {Filename: "filename.yml", Config: source}, + }, + Environment: nil, + } +} + +var sampleYAML = ` +version: "3" +services: + foo: + image: busybox + networks: + with_me: + bar: + image: busybox + environment: + - FOO=1 + networks: + - with_ipam +volumes: + hello: + driver: default + driver_opts: + beep: boop +networks: + default: + driver: bridge + driver_opts: + beep: boop + with_ipam: + ipam: + driver: default + config: + - subnet: 172.28.0.0/16 +` + +var sampleDict = types.Dict{ + "version": "3", + "services": types.Dict{ + "foo": types.Dict{ + "image": "busybox", + "networks": types.Dict{"with_me": nil}, + }, + "bar": types.Dict{ + "image": "busybox", + "environment": []interface{}{"FOO=1"}, + "networks": []interface{}{"with_ipam"}, + }, + }, + "volumes": types.Dict{ + "hello": types.Dict{ + "driver": "default", + "driver_opts": types.Dict{ + "beep": "boop", + }, + }, + }, + "networks": types.Dict{ + "default": types.Dict{ + "driver": "bridge", + "driver_opts": types.Dict{ + "beep": "boop", + }, + }, + "with_ipam": types.Dict{ + "ipam": types.Dict{ + "driver": "default", + "config": []interface{}{ + types.Dict{ + "subnet": "172.28.0.0/16", + }, + }, + }, + }, + }, +} + +var sampleConfig = types.Config{ + Services: []types.ServiceConfig{ + { + Name: "foo", + Image: "busybox", + Environment: map[string]string{}, + Networks: map[string]*types.ServiceNetworkConfig{ + "with_me": nil, + }, + }, + { + Name: "bar", + Image: "busybox", + Environment: map[string]string{"FOO": "1"}, + Networks: map[string]*types.ServiceNetworkConfig{ + "with_ipam": nil, + }, + }, + }, + Networks: map[string]types.NetworkConfig{ + "default": { + Driver: "bridge", + DriverOpts: map[string]string{ + "beep": "boop", + }, + }, + "with_ipam": { + Ipam: types.IPAMConfig{ + Driver: "default", + Config: []*types.IPAMPool{ + { + Subnet: "172.28.0.0/16", + }, + }, + }, + }, + }, + Volumes: map[string]types.VolumeConfig{ + "hello": { + Driver: "default", + DriverOpts: map[string]string{ + "beep": "boop", + }, + }, + }, +} + +func TestParseYAML(t *testing.T) { + dict, err := ParseYAML([]byte(sampleYAML)) + if !assert.NoError(t, err) { + return + } + assert.Equal(t, sampleDict, dict) +} + +func TestLoad(t *testing.T) { + actual, err := Load(buildConfigDetails(sampleDict)) + if !assert.NoError(t, err) { + return + } + assert.Equal(t, serviceSort(sampleConfig.Services), serviceSort(actual.Services)) + assert.Equal(t, sampleConfig.Networks, actual.Networks) + assert.Equal(t, sampleConfig.Volumes, actual.Volumes) +} + +func TestParseAndLoad(t *testing.T) { + actual, err := loadYAML(sampleYAML) + if !assert.NoError(t, err) { + return + } + assert.Equal(t, serviceSort(sampleConfig.Services), serviceSort(actual.Services)) + assert.Equal(t, sampleConfig.Networks, actual.Networks) + assert.Equal(t, sampleConfig.Volumes, actual.Volumes) +} + +func TestInvalidTopLevelObjectType(t *testing.T) { + _, err := loadYAML("1") + assert.Error(t, err) + assert.Contains(t, err.Error(), "Top-level object must be a mapping") + + _, err = loadYAML("\"hello\"") + assert.Error(t, err) + assert.Contains(t, err.Error(), "Top-level object must be a mapping") + + _, err = loadYAML("[\"hello\"]") + assert.Error(t, err) + assert.Contains(t, err.Error(), "Top-level object must be a mapping") +} + +func TestNonStringKeys(t *testing.T) { + _, err := loadYAML(` +version: "3" +123: + foo: + image: busybox +`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "Non-string key at top level: 123") + + _, err = loadYAML(` +version: "3" +services: + foo: + image: busybox + 123: + image: busybox +`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "Non-string key in services: 123") + + _, err = loadYAML(` +version: "3" +services: + foo: + image: busybox +networks: + default: + ipam: + config: + - 123: oh dear +`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "Non-string key in networks.default.ipam.config[0]: 123") + + _, err = loadYAML(` +version: "3" +services: + dict-env: + image: busybox + environment: + 1: FOO +`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "Non-string key in services.dict-env.environment: 1") +} + +func TestSupportedVersion(t *testing.T) { + _, err := loadYAML(` +version: "3" +services: + foo: + image: busybox +`) + assert.NoError(t, err) + + _, err = loadYAML(` +version: "3.0" +services: + foo: + image: busybox +`) + assert.NoError(t, err) +} + +func TestUnsupportedVersion(t *testing.T) { + _, err := loadYAML(` +version: "2" +services: + foo: + image: busybox +`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "version") + + _, err = loadYAML(` +version: "2.0" +services: + foo: + image: busybox +`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "version") +} + +func TestInvalidVersion(t *testing.T) { + _, err := loadYAML(` +version: 3 +services: + foo: + image: busybox +`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "version must be a string") +} + +func TestV1Unsupported(t *testing.T) { + _, err := loadYAML(` +foo: + image: busybox +`) + assert.Error(t, err) +} + +func TestNonMappingObject(t *testing.T) { + _, err := loadYAML(` +version: "3" +services: + - foo: + image: busybox +`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "services must be a mapping") + + _, err = loadYAML(` +version: "3" +services: + foo: busybox +`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "services.foo must be a mapping") + + _, err = loadYAML(` +version: "3" +networks: + - default: + driver: bridge +`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "networks must be a mapping") + + _, err = loadYAML(` +version: "3" +networks: + default: bridge +`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "networks.default must be a mapping") + + _, err = loadYAML(` +version: "3" +volumes: + - data: + driver: local +`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "volumes must be a mapping") + + _, err = loadYAML(` +version: "3" +volumes: + data: local +`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "volumes.data must be a mapping") +} + +func TestNonStringImage(t *testing.T) { + _, err := loadYAML(` +version: "3" +services: + foo: + image: ["busybox", "latest"] +`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "services.foo.image must be a string") +} + +func TestValidEnvironment(t *testing.T) { + config, err := loadYAML(` +version: "3" +services: + dict-env: + image: busybox + environment: + FOO: "1" + BAR: 2 + BAZ: 2.5 + QUUX: + list-env: + image: busybox + environment: + - FOO=1 + - BAR=2 + - BAZ=2.5 + - QUUX= +`) + assert.NoError(t, err) + + expected := map[string]string{ + "FOO": "1", + "BAR": "2", + "BAZ": "2.5", + "QUUX": "", + } + + assert.Equal(t, 2, len(config.Services)) + + for _, service := range config.Services { + assert.Equal(t, expected, service.Environment) + } +} + +func TestInvalidEnvironmentValue(t *testing.T) { + _, err := loadYAML(` +version: "3" +services: + dict-env: + image: busybox + environment: + FOO: ["1"] +`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "services.dict-env.environment.FOO must be a string, number or null") +} + +func TestInvalidEnvironmentObject(t *testing.T) { + _, err := loadYAML(` +version: "3" +services: + dict-env: + image: busybox + environment: "FOO=1" +`) + assert.Error(t, err) + assert.Contains(t, err.Error(), "services.dict-env.environment must be a mapping") +} + +func TestEnvironmentInterpolation(t *testing.T) { + config, err := loadYAML(` +version: "3" +services: + test: + image: busybox + labels: + - home1=$HOME + - home2=${HOME} + - nonexistent=$NONEXISTENT + - default=${NONEXISTENT-default} +networks: + test: + driver: $HOME +volumes: + test: + driver: $HOME +`) + + assert.NoError(t, err) + + home := os.Getenv("HOME") + + expectedLabels := map[string]string{ + "home1": home, + "home2": home, + "nonexistent": "", + "default": "default", + } + + assert.Equal(t, expectedLabels, config.Services[0].Labels) + assert.Equal(t, home, config.Networks["test"].Driver) + assert.Equal(t, home, config.Volumes["test"].Driver) +} + +func TestUnsupportedProperties(t *testing.T) { + dict, err := ParseYAML([]byte(` +version: "3" +services: + web: + image: web + build: ./web + links: + - bar + db: + image: db + build: ./db +`)) + assert.NoError(t, err) + + configDetails := buildConfigDetails(dict) + + _, err = Load(configDetails) + assert.NoError(t, err) + + unsupported := GetUnsupportedProperties(configDetails) + assert.Equal(t, []string{"build", "links"}, unsupported) +} + +func TestDeprecatedProperties(t *testing.T) { + dict, err := ParseYAML([]byte(` +version: "3" +services: + web: + image: web + container_name: web + db: + image: db + container_name: db + expose: ["5434"] +`)) + assert.NoError(t, err) + + configDetails := buildConfigDetails(dict) + + _, err = Load(configDetails) + assert.NoError(t, err) + + deprecated := GetDeprecatedProperties(configDetails) + assert.Equal(t, 2, len(deprecated)) + assert.Contains(t, deprecated, "container_name") + assert.Contains(t, deprecated, "expose") +} + +func TestForbiddenProperties(t *testing.T) { + _, err := loadYAML(` +version: "3" +services: + foo: + image: busybox + volumes: + - /data + volume_driver: some-driver + bar: + extends: + service: foo +`) + + assert.Error(t, err) + assert.IsType(t, &ForbiddenPropertiesError{}, err) + fmt.Println(err) + forbidden := err.(*ForbiddenPropertiesError).Properties + + assert.Equal(t, 2, len(forbidden)) + assert.Contains(t, forbidden, "volume_driver") + assert.Contains(t, forbidden, "extends") +} + +func durationPtr(value time.Duration) *time.Duration { + return &value +} + +func int64Ptr(value int64) *int64 { + return &value +} + +func uint64Ptr(value uint64) *uint64 { + return &value +} + +func TestFullExample(t *testing.T) { + bytes, err := ioutil.ReadFile("full-example.yml") + assert.NoError(t, err) + + config, err := loadYAML(string(bytes)) + if !assert.NoError(t, err) { + return + } + + workingDir, err := os.Getwd() + assert.NoError(t, err) + + homeDir := os.Getenv("HOME") + stopGracePeriod := time.Duration(20 * time.Second) + + expectedServiceConfig := types.ServiceConfig{ + Name: "foo", + + CapAdd: []string{"ALL"}, + CapDrop: []string{"NET_ADMIN", "SYS_ADMIN"}, + CgroupParent: "m-executor-abcd", + Command: []string{"bundle", "exec", "thin", "-p", "3000"}, + ContainerName: "my-web-container", + DependsOn: []string{"db", "redis"}, + Deploy: types.DeployConfig{ + Mode: "replicated", + Replicas: uint64Ptr(6), + Labels: map[string]string{"FOO": "BAR"}, + UpdateConfig: &types.UpdateConfig{ + Parallelism: uint64Ptr(3), + Delay: time.Duration(10 * time.Second), + FailureAction: "continue", + Monitor: time.Duration(60 * time.Second), + MaxFailureRatio: 0.3, + }, + Resources: types.Resources{ + Limits: &types.Resource{ + NanoCPUs: "0.001", + MemoryBytes: 50 * 1024 * 1024, + }, + Reservations: &types.Resource{ + NanoCPUs: "0.0001", + MemoryBytes: 20 * 1024 * 1024, + }, + }, + RestartPolicy: &types.RestartPolicy{ + Condition: "on_failure", + Delay: durationPtr(5 * time.Second), + MaxAttempts: uint64Ptr(3), + Window: durationPtr(2 * time.Minute), + }, + Placement: types.Placement{ + Constraints: []string{"node=foo"}, + }, + }, + Devices: []string{"/dev/ttyUSB0:/dev/ttyUSB0"}, + DNS: []string{"8.8.8.8", "9.9.9.9"}, + DNSSearch: []string{"dc1.example.com", "dc2.example.com"}, + DomainName: "foo.com", + Entrypoint: []string{"/code/entrypoint.sh", "-p", "3000"}, + Environment: map[string]string{ + "RACK_ENV": "development", + "SHOW": "true", + "SESSION_SECRET": "", + "FOO": "1", + "BAR": "2", + "BAZ": "3", + }, + Expose: []string{"3000", "8000"}, + ExternalLinks: []string{ + "redis_1", + "project_db_1:mysql", + "project_db_1:postgresql", + }, + ExtraHosts: map[string]string{ + "otherhost": "50.31.209.229", + "somehost": "162.242.195.82", + }, + HealthCheck: &types.HealthCheckConfig{ + Test: []string{ + "CMD-SHELL", + "echo \"hello world\"", + }, + Interval: "10s", + Timeout: "1s", + Retries: uint64Ptr(5), + }, + Hostname: "foo", + Image: "redis", + Ipc: "host", + Labels: map[string]string{ + "com.example.description": "Accounting webapp", + "com.example.number": "42", + "com.example.empty-label": "", + }, + Links: []string{ + "db", + "db:database", + "redis", + }, + Logging: &types.LoggingConfig{ + Driver: "syslog", + Options: map[string]string{ + "syslog-address": "tcp://192.168.0.42:123", + }, + }, + MacAddress: "02:42:ac:11:65:43", + NetworkMode: "container:0cfeab0f748b9a743dc3da582046357c6ef497631c1a016d28d2bf9b4f899f7b", + Networks: map[string]*types.ServiceNetworkConfig{ + "some-network": { + Aliases: []string{"alias1", "alias3"}, + Ipv4Address: "", + Ipv6Address: "", + }, + "other-network": { + Ipv4Address: "172.16.238.10", + Ipv6Address: "2001:3984:3989::10", + }, + "other-other-network": nil, + }, + Pid: "host", + Ports: []string{ + "3000", + "3000-3005", + "8000:8000", + "9090-9091:8080-8081", + "49100:22", + "127.0.0.1:8001:8001", + "127.0.0.1:5000-5010:5000-5010", + }, + Privileged: true, + ReadOnly: true, + Restart: "always", + SecurityOpt: []string{ + "label=level:s0:c100,c200", + "label=type:svirt_apache_t", + }, + StdinOpen: true, + StopSignal: "SIGUSR1", + StopGracePeriod: &stopGracePeriod, + Tmpfs: []string{"/run", "/tmp"}, + Tty: true, + Ulimits: map[string]*types.UlimitsConfig{ + "nproc": { + Single: 65535, + }, + "nofile": { + Soft: 20000, + Hard: 40000, + }, + }, + User: "someone", + Volumes: []string{ + "/var/lib/mysql", + "/opt/data:/var/lib/mysql", + fmt.Sprintf("%s:/code", workingDir), + fmt.Sprintf("%s/static:/var/www/html", workingDir), + fmt.Sprintf("%s/configs:/etc/configs/:ro", homeDir), + "datavolume:/var/lib/mysql", + }, + WorkingDir: "/code", + } + + assert.Equal(t, []types.ServiceConfig{expectedServiceConfig}, config.Services) + + expectedNetworkConfig := map[string]types.NetworkConfig{ + "some-network": {}, + + "other-network": { + Driver: "overlay", + DriverOpts: map[string]string{ + "foo": "bar", + "baz": "1", + }, + Ipam: types.IPAMConfig{ + Driver: "overlay", + Config: []*types.IPAMPool{ + {Subnet: "172.16.238.0/24"}, + {Subnet: "2001:3984:3989::/64"}, + }, + }, + }, + + "external-network": { + External: types.External{ + Name: "external-network", + External: true, + }, + }, + + "other-external-network": { + External: types.External{ + Name: "my-cool-network", + External: true, + }, + }, + } + + assert.Equal(t, expectedNetworkConfig, config.Networks) + + expectedVolumeConfig := map[string]types.VolumeConfig{ + "some-volume": {}, + "other-volume": { + Driver: "flocker", + DriverOpts: map[string]string{ + "foo": "bar", + "baz": "1", + }, + }, + "external-volume": { + External: types.External{ + Name: "external-volume", + External: true, + }, + }, + "other-external-volume": { + External: types.External{ + Name: "my-cool-volume", + External: true, + }, + }, + } + + assert.Equal(t, expectedVolumeConfig, config.Volumes) +} + +func loadYAML(yaml string) (*types.Config, error) { + dict, err := ParseYAML([]byte(yaml)) + if err != nil { + return nil, err + } + + return Load(buildConfigDetails(dict)) +} + +func serviceSort(services []types.ServiceConfig) []types.ServiceConfig { + sort.Sort(servicesByName(services)) + return services +} + +type servicesByName []types.ServiceConfig + +func (sbn servicesByName) Len() int { return len(sbn) } +func (sbn servicesByName) Swap(i, j int) { sbn[i], sbn[j] = sbn[j], sbn[i] } +func (sbn servicesByName) Less(i, j int) bool { return sbn[i].Name < sbn[j].Name } diff --git a/components/cli/compose/schema/bindata.go b/components/cli/compose/schema/bindata.go new file mode 100644 index 0000000000..2acc7d29f1 --- /dev/null +++ b/components/cli/compose/schema/bindata.go @@ -0,0 +1,237 @@ +// Code generated by go-bindata. +// sources: +// data/config_schema_v3.0.json +// DO NOT EDIT! + +package schema + +import ( + "bytes" + "compress/gzip" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" +) + +func bindataRead(data []byte, name string) ([]byte, error) { + gz, err := gzip.NewReader(bytes.NewBuffer(data)) + if err != nil { + return nil, fmt.Errorf("Read %q: %v", name, err) + } + + var buf bytes.Buffer + _, err = io.Copy(&buf, gz) + clErr := gz.Close() + + if err != nil { + return nil, fmt.Errorf("Read %q: %v", name, err) + } + if clErr != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +type asset struct { + bytes []byte + info os.FileInfo +} + +type bindataFileInfo struct { + name string + size int64 + mode os.FileMode + modTime time.Time +} + +func (fi bindataFileInfo) Name() string { + return fi.name +} +func (fi bindataFileInfo) Size() int64 { + return fi.size +} +func (fi bindataFileInfo) Mode() os.FileMode { + return fi.mode +} +func (fi bindataFileInfo) ModTime() time.Time { + return fi.modTime +} +func (fi bindataFileInfo) IsDir() bool { + return false +} +func (fi bindataFileInfo) Sys() interface{} { + return nil +} + +var _dataConfig_schema_v30Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x5a\x4d\x8f\xdb\x36\x13\xbe\xfb\x57\x08\x4a\x6e\xf1\xee\x06\x78\x83\x17\x68\x6e\x3d\xf6\xd4\x9e\xbb\x50\x04\x5a\x1a\xdb\xcc\x52\x24\x33\xa4\x9c\x75\x02\xff\xf7\x82\xfa\xb2\x48\x93\xa2\x6c\x2b\x4d\x0e\xbd\x2c\xd6\xe2\xcc\x70\xbe\xf8\xcc\x70\xa4\xef\xab\x24\x49\xdf\xaa\x62\x0f\x15\x49\x3f\x26\xe9\x5e\x6b\xf9\xf1\xe9\xe9\xb3\x12\xfc\xa1\x7d\xfa\x28\x70\xf7\x54\x22\xd9\xea\x87\xf7\x1f\x9e\xda\x67\x6f\xd2\xb5\xe1\xa3\xa5\x61\x29\x04\xdf\xd2\x5d\xde\xae\xe4\x87\xff\x3d\xbe\x7f\x34\xec\x2d\x89\x3e\x4a\x30\x44\x62\xf3\x19\x0a\xdd\x3e\x43\xf8\x52\x53\x04\xc3\xfc\x9c\x1e\x00\x15\x15\x3c\xcd\xd6\x2b\xb3\x26\x51\x48\x40\x4d\x41\xa5\x1f\x13\xa3\x5c\x92\x0c\x24\xfd\x83\x91\x58\xa5\x91\xf2\x5d\xda\x3c\x3e\x35\x12\x92\x24\x55\x80\x07\x5a\x8c\x24\x0c\xaa\xbe\x79\x3a\xcb\x7f\x1a\xc8\xd6\xae\xd4\x91\xb2\xcd\x73\x49\xb4\x06\xe4\x7f\x5d\xea\xd6\x2c\x7f\x7a\x26\x0f\xdf\x7e\x7f\xf8\xfb\xfd\xc3\x6f\x8f\xf9\x43\xf6\xee\xad\xb5\x6c\xfc\x8b\xb0\x6d\xb7\x2f\x61\x4b\x39\xd5\x54\xf0\x61\xff\x74\xa0\x3c\x75\xff\x9d\x86\x8d\x49\x59\x36\xc4\x84\x59\x7b\x6f\x09\x53\x60\xdb\xcc\x41\x7f\x15\xf8\x12\xb3\x79\x20\xfb\x49\x36\x77\xfb\x7b\x6c\xb6\xcd\x39\x08\x56\x57\xd1\x08\xf6\x54\x3f\xc9\x98\x76\xfb\xfb\xe2\xb7\xea\x8d\x9e\xa4\x6d\x29\x46\x7b\x37\x0a\x5a\xd9\xee\x73\x95\x2f\xdb\xc2\xbe\x1a\x9c\x15\xf0\x52\x09\x92\x89\xa3\x79\x16\xf0\x47\x4b\x50\x01\xd7\xe9\xe0\x82\x24\x49\x37\x35\x65\xa5\xeb\x51\xc1\xe1\x4f\x23\xe2\x79\xf4\x30\x49\xbe\xbb\x07\x7b\x24\xa7\x59\xb7\x7e\x85\x03\x3e\xac\x07\x6c\x19\xd6\x0b\xc1\x35\xbc\xea\xc6\xa8\xe9\xad\x5b\x17\x88\xe2\x05\x70\x4b\x19\xcc\xe5\x20\xb8\x53\x13\x2e\x63\x54\xe9\x5c\x60\x5e\xd2\x42\xa7\x27\x87\xfd\x42\x5e\x3c\x9f\x06\xd6\xd1\xaf\x6c\xe5\x11\x98\x16\x44\xe6\xa4\x2c\x2d\x3b\x08\x22\x39\xa6\xeb\x24\xa5\x1a\x2a\xe5\x37\x31\x49\x6b\x4e\xbf\xd4\xf0\x47\x47\xa2\xb1\x06\x57\x6e\x89\x42\x2e\x2f\x78\x87\xa2\x96\xb9\x24\x68\x12\x6c\xda\xfd\x69\x21\xaa\x8a\xf0\xa5\xb2\xee\x1a\x3b\x66\x78\x5e\x70\x4d\x28\x07\xcc\x39\xa9\x62\x89\x64\x4e\x1d\xf0\x52\xe5\x6d\xfd\x9b\x4c\xa3\x6d\xde\xf2\x2b\x47\xc0\x50\x0c\x17\x8d\x47\xc9\xa7\x12\xbb\x15\x63\x52\xdb\xe8\x96\x3a\x8c\xb9\x02\x82\xc5\xfe\x46\x7e\x51\x11\xca\xe7\xf8\x0e\xb8\xc6\xa3\x14\xb4\xcd\x97\x5f\x2e\x11\x80\x1f\xf2\x01\x4b\xae\x76\x03\xf0\x03\x45\xc1\xab\xfe\x34\xcc\x01\x98\x01\xe4\x0d\xff\xab\x14\x0a\x5c\xc7\x38\x06\x8e\x97\x06\x53\x2d\x9f\xf4\x1c\xcf\xbd\xe1\xeb\x24\xe5\x75\xb5\x01\x34\x2d\x9d\x45\xb9\x15\x58\x11\xa3\x6c\xbf\xf7\x68\xd9\xf2\xb4\x27\xf3\xc6\x0e\x1c\xdb\x60\xca\x3a\x61\x39\xa3\xfc\x65\xf9\x14\x87\x57\x8d\x24\xdf\x0b\xa5\xe7\x63\xf8\x88\x7d\x0f\x84\xe9\x7d\xb1\x87\xe2\x65\x82\x7d\x4c\x65\x71\x0b\xa5\xe7\x24\x39\xad\xc8\x2e\x4e\x24\x8b\x18\x09\x23\x1b\x60\x37\xd9\xb9\xa8\xf3\x47\x62\xc5\x6e\x67\x48\x43\x19\x77\xd1\xb9\x74\xcb\xb1\x9a\x5f\x22\x3d\x00\xce\x2d\xe0\x42\x9e\x1b\x2e\x77\x31\xde\x80\x24\xf1\xee\xd3\x22\xfd\xf4\xd8\x36\x9f\x13\xa7\xaa\xf9\x8f\xb1\x34\x73\xdb\x85\xc4\xa9\xfb\xbe\x27\x8e\x85\xf3\x1a\x0a\x2b\x2a\x15\x29\x4c\xdf\x80\xa0\x02\x71\x3d\x93\x76\xcd\x7e\x5e\x89\x32\x94\xa0\x17\xc4\xae\x6f\x82\x48\x7d\x75\x21\x4c\x6e\xea\x1f\x67\x85\x2e\x7a\x81\x88\x58\x13\x52\x6f\xae\x9a\x67\x75\xe3\x29\xd6\xd0\x11\x46\x89\x82\xf8\x61\x0f\x3a\xd2\x92\x46\xe5\xe1\xc3\xcc\x9c\xf0\xf1\xfe\x7f\x92\x37\xc0\x1a\x94\x39\xbf\x47\x8e\x88\x3a\xab\xd2\x1c\x37\x9f\x22\x59\xe4\xb4\xfd\xe0\x16\x5e\xd2\x32\x8c\x15\x0d\x42\x8c\x0f\x98\x14\xa8\x2f\x4e\xd7\xbf\x53\xee\xdb\xad\xef\xae\xf6\x12\xe9\x81\x32\xd8\x81\x7d\x6b\xd9\x08\xc1\x80\x70\x0b\x7a\x10\x48\x99\x0b\xce\x8e\x33\x28\x95\x26\x18\xbd\x50\x28\x28\x6a\xa4\xfa\x98\x0b\xa9\x17\xef\x33\xd4\xbe\xca\x15\xfd\x06\x76\x34\xcf\x78\xdf\x09\xca\x2c\x1e\x5d\x52\x9e\x0b\x09\x3c\x6a\xa2\xd2\x42\xe6\x8a\xee\x38\x61\x51\x33\x0d\xe9\x0e\x49\x01\xb9\x04\xa4\xa2\xf4\x31\xac\xc7\xb1\x2d\x6b\x24\x26\x9f\x2d\x31\xba\x92\xdb\x1b\x6f\x07\x5a\xc7\x63\x56\x33\x5a\xd1\x70\x32\x7b\x50\x72\x06\x90\xb7\x20\xee\xc7\xee\x09\xdc\x3e\x6b\x4a\xb9\x86\x1d\xa0\x0f\xee\x26\x5a\x87\xe9\xce\x61\x46\xcb\xb0\x27\x68\x47\x69\x42\x8f\x86\x41\x89\xad\xf6\x33\xf8\x1a\x0a\xaf\x5e\xd6\x04\xb7\x91\xb7\xee\x14\xc9\xbc\xf4\x57\x61\xb2\xab\x46\x16\x84\xc5\x93\x17\x16\x6b\x15\xed\xee\xc6\xf3\xc5\x45\x4f\xb2\x69\x61\x4c\x66\x97\xd4\xaf\xc2\xca\x51\xf7\x8a\x09\xaf\x73\x9b\xe8\x05\xf8\x66\x7d\x63\x52\x77\xde\xf7\x3c\x24\x5c\x5f\x25\xce\x53\xd2\xc0\xe0\xcf\xe4\x07\x1e\x2c\xf0\xf0\xf9\x54\xd3\x0a\x44\xad\x23\x54\x08\x1a\xa9\xe3\xf9\x0e\xe9\x2c\x61\xa0\x7e\xcd\x4b\x7b\x49\x15\xd9\x38\xf3\xbf\x01\xa3\x6e\x0a\x6f\x72\x1e\xae\xf6\x97\xf9\xa9\xe0\x8e\x28\x17\x88\xed\x44\x6f\x3e\x0a\x99\x64\xb4\x20\x2a\x86\x32\x77\x5c\x21\x6b\x59\x12\x0d\x79\xfb\x2a\xe9\x2a\x5c\x9f\x00\x74\x49\x90\x30\x06\x8c\xaa\x6a\x0e\x40\xa6\x25\x30\x72\xbc\xa9\xe0\x35\xec\x5b\x42\x59\x8d\x90\x93\x42\x77\x6f\xab\x22\x99\x99\x56\x82\x53\x2d\xbc\x48\x31\x6f\xcb\x8a\xbc\xe6\xfd\xb6\x0d\x89\xf7\x58\x05\x1b\xaf\xb9\xb7\xbf\x51\x26\x28\x51\x63\x71\xe1\xec\x9b\x43\x74\x2e\xe4\x81\x8c\xe9\x77\xbc\x30\x1d\x41\x19\x50\x1a\x2e\xe7\x51\xfe\x68\xdd\xe8\x3a\xc1\x5c\x0a\x46\x8b\xe3\x52\x16\x16\x82\xb7\x4e\x9e\x93\x10\x77\x66\xa0\x49\x07\xd3\xe7\x54\x52\x47\x0f\x6b\xc3\xf0\x95\xf2\x52\x7c\xbd\x62\xc3\xe5\x52\x49\x32\x52\x80\x83\x77\xf7\x3a\x5a\x69\x24\x94\xeb\xab\xcb\xfa\xbd\x66\xdd\x51\xd5\x87\xfc\x8c\xa0\xfe\x40\x17\x7f\xd7\x19\x40\xfa\x42\xd6\xd1\x89\x4d\x05\x95\x40\x6f\x02\x2e\xf0\x6e\x3a\x66\x62\x4f\xb6\x40\x55\x9b\x35\xe2\xeb\xa8\xcc\x8d\x6e\xf1\xab\x44\x7c\x8c\x97\xc5\x01\x89\x4a\x52\x2d\x75\x3a\x66\x0f\x3d\x53\x6f\x0d\x4e\xa6\x87\x05\x49\x78\x60\x10\xd3\x3a\xae\x7b\x47\xa1\xea\x0d\x07\xff\x3d\xfd\xf2\x0a\xe1\x7b\x13\x3b\xff\x0e\x72\x0a\xdf\x38\xee\x03\xbd\xfe\x7d\x45\x20\xaa\xcf\x43\x27\xb9\x1e\x7c\x95\xcd\x0e\x71\xf0\x65\xc1\x72\xfa\x5f\xd9\xe0\xdd\x81\x19\xdd\xb7\x15\x11\xc8\xe8\xa8\xfe\x43\x8c\x5f\x26\xbf\x26\x8a\xe2\x8d\xb7\x83\x2b\x92\xc6\x19\x2b\x8d\x92\xe7\xf2\xea\x38\x15\xe7\xd9\x43\xf1\x8e\x23\xb3\xd5\x70\xc9\x3c\xdf\xad\xd9\x10\x3a\x35\x71\xe8\x49\x02\x43\x52\x67\xd3\xce\x79\xd3\x96\x2f\x98\xb6\x8f\xef\x26\x0a\xc5\xd4\xcb\xab\x1f\x84\xb0\x0b\x4c\x73\xfc\x31\x75\xba\xcb\xde\xbb\x97\x1f\x5f\x05\x90\x6a\xc4\x7f\xf1\x29\x96\xb1\x93\x1f\x2f\x46\x1b\xdf\xed\x31\x5b\xfb\x19\x55\x66\xf9\xc7\x21\x69\x5f\x05\x8f\x70\x22\x1b\x37\xdc\xa1\x30\x7a\x3f\xd0\x72\x87\x7c\xfd\x87\x52\xd9\xf4\x61\x5f\xf5\x7f\x4f\xab\xd3\xea\x9f\x00\x00\x00\xff\xff\xd1\xeb\xc9\xb9\x5c\x2a\x00\x00") + +func dataConfig_schema_v30JsonBytes() ([]byte, error) { + return bindataRead( + _dataConfig_schema_v30Json, + "data/config_schema_v3.0.json", + ) +} + +func dataConfig_schema_v30Json() (*asset, error) { + bytes, err := dataConfig_schema_v30JsonBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "data/config_schema_v3.0.json", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +// Asset loads and returns the asset for the given name. +// It returns an error if the asset could not be found or +// could not be loaded. +func Asset(name string) ([]byte, error) { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { + a, err := f() + if err != nil { + return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) + } + return a.bytes, nil + } + return nil, fmt.Errorf("Asset %s not found", name) +} + +// MustAsset is like Asset but panics when Asset would return an error. +// It simplifies safe initialization of global variables. +func MustAsset(name string) []byte { + a, err := Asset(name) + if err != nil { + panic("asset: Asset(" + name + "): " + err.Error()) + } + + return a +} + +// AssetInfo loads and returns the asset info for the given name. +// It returns an error if the asset could not be found or +// could not be loaded. +func AssetInfo(name string) (os.FileInfo, error) { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { + a, err := f() + if err != nil { + return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) + } + return a.info, nil + } + return nil, fmt.Errorf("AssetInfo %s not found", name) +} + +// AssetNames returns the names of the assets. +func AssetNames() []string { + names := make([]string, 0, len(_bindata)) + for name := range _bindata { + names = append(names, name) + } + return names +} + +// _bindata is a table, holding each asset generator, mapped to its name. +var _bindata = map[string]func() (*asset, error){ + "data/config_schema_v3.0.json": dataConfig_schema_v30Json, +} + +// AssetDir returns the file names below a certain +// directory embedded in the file by go-bindata. +// For example if you run go-bindata on data/... and data contains the +// following hierarchy: +// data/ +// foo.txt +// img/ +// a.png +// b.png +// then AssetDir("data") would return []string{"foo.txt", "img"} +// AssetDir("data/img") would return []string{"a.png", "b.png"} +// AssetDir("foo.txt") and AssetDir("notexist") would return an error +// AssetDir("") will return []string{"data"}. +func AssetDir(name string) ([]string, error) { + node := _bintree + if len(name) != 0 { + cannonicalName := strings.Replace(name, "\\", "/", -1) + pathList := strings.Split(cannonicalName, "/") + for _, p := range pathList { + node = node.Children[p] + if node == nil { + return nil, fmt.Errorf("Asset %s not found", name) + } + } + } + if node.Func != nil { + return nil, fmt.Errorf("Asset %s not found", name) + } + rv := make([]string, 0, len(node.Children)) + for childName := range node.Children { + rv = append(rv, childName) + } + return rv, nil +} + +type bintree struct { + Func func() (*asset, error) + Children map[string]*bintree +} +var _bintree = &bintree{nil, map[string]*bintree{ + "data": &bintree{nil, map[string]*bintree{ + "config_schema_v3.0.json": &bintree{dataConfig_schema_v30Json, map[string]*bintree{}}, + }}, +}} + +// RestoreAsset restores an asset under the given directory +func RestoreAsset(dir, name string) error { + data, err := Asset(name) + if err != nil { + return err + } + info, err := AssetInfo(name) + if err != nil { + return err + } + err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) + if err != nil { + return err + } + err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) + if err != nil { + return err + } + err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) + if err != nil { + return err + } + return nil +} + +// RestoreAssets restores an asset under the given directory recursively +func RestoreAssets(dir, name string) error { + children, err := AssetDir(name) + // File + if err != nil { + return RestoreAsset(dir, name) + } + // Dir + for _, child := range children { + err = RestoreAssets(dir, filepath.Join(name, child)) + if err != nil { + return err + } + } + return nil +} + +func _filePath(dir, name string) string { + cannonicalName := strings.Replace(name, "\\", "/", -1) + return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) +} + diff --git a/components/cli/compose/schema/data/config_schema_v3.0.json b/components/cli/compose/schema/data/config_schema_v3.0.json new file mode 100644 index 0000000000..520e57d5e2 --- /dev/null +++ b/components/cli/compose/schema/data/config_schema_v3.0.json @@ -0,0 +1,379 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "config_schema_v3.0.json", + "type": "object", + "required": ["version"], + + "properties": { + "version": { + "type": "string" + }, + + "services": { + "id": "#/properties/services", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "$ref": "#/definitions/service" + } + }, + "additionalProperties": false + }, + + "networks": { + "id": "#/properties/networks", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "$ref": "#/definitions/network" + } + } + }, + + "volumes": { + "id": "#/properties/volumes", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "$ref": "#/definitions/volume" + } + }, + "additionalProperties": false + } + }, + + "additionalProperties": false, + + "definitions": { + + "service": { + "id": "#/definitions/service", + "type": "object", + + "properties": { + "deploy": {"$ref": "#/definitions/deployment"}, + "build": { + "oneOf": [ + {"type": "string"}, + { + "type": "object", + "properties": { + "context": {"type": "string"}, + "dockerfile": {"type": "string"}, + "args": {"$ref": "#/definitions/list_or_dict"} + }, + "additionalProperties": false + } + ] + }, + "cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "cgroup_parent": {"type": "string"}, + "command": { + "oneOf": [ + {"type": "string"}, + {"type": "array", "items": {"type": "string"}} + ] + }, + "container_name": {"type": "string"}, + "depends_on": {"$ref": "#/definitions/list_of_strings"}, + "devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "dns": {"$ref": "#/definitions/string_or_list"}, + "dns_search": {"$ref": "#/definitions/string_or_list"}, + "domainname": {"type": "string"}, + "entrypoint": { + "oneOf": [ + {"type": "string"}, + {"type": "array", "items": {"type": "string"}} + ] + }, + "env_file": {"$ref": "#/definitions/string_or_list"}, + "environment": {"$ref": "#/definitions/list_or_dict"}, + + "expose": { + "type": "array", + "items": { + "type": ["string", "number"], + "format": "expose" + }, + "uniqueItems": true + }, + + "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "extra_hosts": {"$ref": "#/definitions/list_or_dict"}, + "healthcheck": {"$ref": "#/definitions/healthcheck"}, + "hostname": {"type": "string"}, + "image": {"type": "string"}, + "ipc": {"type": "string"}, + "labels": {"$ref": "#/definitions/list_or_dict"}, + "links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + + "logging": { + "type": "object", + + "properties": { + "driver": {"type": "string"}, + "options": { + "type": "object", + "patternProperties": { + "^.+$": {"type": ["string", "number", "null"]} + } + } + }, + "additionalProperties": false + }, + + "mac_address": {"type": "string"}, + "network_mode": {"type": "string"}, + + "networks": { + "oneOf": [ + {"$ref": "#/definitions/list_of_strings"}, + { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "oneOf": [ + { + "type": "object", + "properties": { + "aliases": {"$ref": "#/definitions/list_of_strings"}, + "ipv4_address": {"type": "string"}, + "ipv6_address": {"type": "string"} + }, + "additionalProperties": false + }, + {"type": "null"} + ] + } + }, + "additionalProperties": false + } + ] + }, + "pid": {"type": ["string", "null"]}, + + "ports": { + "type": "array", + "items": { + "type": ["string", "number"], + "format": "ports" + }, + "uniqueItems": true + }, + + "privileged": {"type": "boolean"}, + "read_only": {"type": "boolean"}, + "restart": {"type": "string"}, + "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "shm_size": {"type": ["number", "string"]}, + "stdin_open": {"type": "boolean"}, + "stop_signal": {"type": "string"}, + "stop_grace_period": {"type": "string", "format": "duration"}, + "tmpfs": {"$ref": "#/definitions/string_or_list"}, + "tty": {"type": "boolean"}, + "ulimits": { + "type": "object", + "patternProperties": { + "^[a-z]+$": { + "oneOf": [ + {"type": "integer"}, + { + "type":"object", + "properties": { + "hard": {"type": "integer"}, + "soft": {"type": "integer"} + }, + "required": ["soft", "hard"], + "additionalProperties": false + } + ] + } + } + }, + "user": {"type": "string"}, + "volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "working_dir": {"type": "string"} + }, + "additionalProperties": false + }, + + "healthcheck": { + "id": "#/definitions/healthcheck", + "type": ["object", "null"], + "properties": { + "interval": {"type":"string"}, + "timeout": {"type":"string"}, + "retries": {"type": "number"}, + "test": { + "oneOf": [ + {"type": "string"}, + {"type": "array", "items": {"type": "string"}} + ] + }, + "disable": {"type": "boolean"} + }, + "additionalProperties": false + }, + "deployment": { + "id": "#/definitions/deployment", + "type": ["object", "null"], + "properties": { + "mode": {"type": "string"}, + "replicas": {"type": "integer"}, + "labels": {"$ref": "#/definitions/list_or_dict"}, + "update_config": { + "type": "object", + "properties": { + "parallelism": {"type": "integer"}, + "delay": {"type": "string", "format": "duration"}, + "failure_action": {"type": "string"}, + "monitor": {"type": "string", "format": "duration"}, + "max_failure_ratio": {"type": "number"} + }, + "additionalProperties": false + }, + "resources": { + "type": "object", + "properties": { + "limits": {"$ref": "#/definitions/resource"}, + "reservations": {"$ref": "#/definitions/resource"} + } + }, + "restart_policy": { + "type": "object", + "properties": { + "condition": {"type": "string"}, + "delay": {"type": "string", "format": "duration"}, + "max_attempts": {"type": "integer"}, + "window": {"type": "string", "format": "duration"} + }, + "additionalProperties": false + }, + "placement": { + "type": "object", + "properties": { + "constraints": {"type": "array", "items": {"type": "string"}} + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + + "resource": { + "id": "#/definitions/resource", + "type": "object", + "properties": { + "cpus": {"type": "string"}, + "memory": {"type": "string"} + }, + "additionalProperties": false + }, + + "network": { + "id": "#/definitions/network", + "type": ["object", "null"], + "properties": { + "driver": {"type": "string"}, + "driver_opts": { + "type": "object", + "patternProperties": { + "^.+$": {"type": ["string", "number"]} + } + }, + "ipam": { + "type": "object", + "properties": { + "driver": {"type": "string"}, + "config": { + "type": "array", + "items": { + "type": "object", + "properties": { + "subnet": {"type": "string"} + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "external": { + "type": ["boolean", "object"], + "properties": { + "name": {"type": "string"} + }, + "additionalProperties": false + }, + "labels": {"$ref": "#/definitions/list_or_dict"} + }, + "additionalProperties": false + }, + + "volume": { + "id": "#/definitions/volume", + "type": ["object", "null"], + "properties": { + "driver": {"type": "string"}, + "driver_opts": { + "type": "object", + "patternProperties": { + "^.+$": {"type": ["string", "number"]} + } + }, + "external": { + "type": ["boolean", "object"], + "properties": { + "name": {"type": "string"} + } + } + }, + "labels": {"$ref": "#/definitions/list_or_dict"}, + "additionalProperties": false + }, + + "string_or_list": { + "oneOf": [ + {"type": "string"}, + {"$ref": "#/definitions/list_of_strings"} + ] + }, + + "list_of_strings": { + "type": "array", + "items": {"type": "string"}, + "uniqueItems": true + }, + + "list_or_dict": { + "oneOf": [ + { + "type": "object", + "patternProperties": { + ".+": { + "type": ["string", "number", "null"] + } + }, + "additionalProperties": false + }, + {"type": "array", "items": {"type": "string"}, "uniqueItems": true} + ] + }, + + "constraints": { + "service": { + "id": "#/definitions/constraints/service", + "anyOf": [ + {"required": ["build"]}, + {"required": ["image"]} + ], + "properties": { + "build": { + "required": ["context"] + } + } + } + } + } +} diff --git a/components/cli/compose/schema/schema.go b/components/cli/compose/schema/schema.go new file mode 100644 index 0000000000..6366cab48e --- /dev/null +++ b/components/cli/compose/schema/schema.go @@ -0,0 +1,113 @@ +package schema + +//go:generate go-bindata -pkg schema -nometadata data + +import ( + "fmt" + "strings" + "time" + + "github.com/xeipuuv/gojsonschema" +) + +type portsFormatChecker struct{} + +func (checker portsFormatChecker) IsFormat(input string) bool { + // TODO: implement this + return true +} + +type durationFormatChecker struct{} + +func (checker durationFormatChecker) IsFormat(input string) bool { + _, err := time.ParseDuration(input) + return err == nil +} + +func init() { + gojsonschema.FormatCheckers.Add("expose", portsFormatChecker{}) + gojsonschema.FormatCheckers.Add("ports", portsFormatChecker{}) + gojsonschema.FormatCheckers.Add("duration", durationFormatChecker{}) +} + +// Validate uses the jsonschema to validate the configuration +func Validate(config map[string]interface{}) error { + schemaData, err := Asset("data/config_schema_v3.0.json") + if err != nil { + return err + } + + schemaLoader := gojsonschema.NewStringLoader(string(schemaData)) + dataLoader := gojsonschema.NewGoLoader(config) + + result, err := gojsonschema.Validate(schemaLoader, dataLoader) + if err != nil { + return err + } + + if !result.Valid() { + return toError(result) + } + + return nil +} + +func toError(result *gojsonschema.Result) error { + err := getMostSpecificError(result.Errors()) + description := getDescription(err) + return fmt.Errorf("%s %s", err.Field(), description) +} + +func getDescription(err gojsonschema.ResultError) string { + if err.Type() == "invalid_type" { + if expectedType, ok := err.Details()["expected"].(string); ok { + return fmt.Sprintf("must be a %s", humanReadableType(expectedType)) + } + } + + return err.Description() +} + +func humanReadableType(definition string) string { + if definition[0:1] == "[" { + allTypes := strings.Split(definition[1:len(definition)-1], ",") + for i, t := range allTypes { + allTypes[i] = humanReadableType(t) + } + return fmt.Sprintf( + "%s or %s", + strings.Join(allTypes[0:len(allTypes)-1], ", "), + allTypes[len(allTypes)-1], + ) + } + if definition == "object" { + return "mapping" + } + if definition == "array" { + return "list" + } + return definition +} + +func getMostSpecificError(errors []gojsonschema.ResultError) gojsonschema.ResultError { + var mostSpecificError gojsonschema.ResultError + + for _, err := range errors { + if mostSpecificError == nil { + mostSpecificError = err + } else if specificity(err) > specificity(mostSpecificError) { + mostSpecificError = err + } else if specificity(err) == specificity(mostSpecificError) { + // Invalid type errors win in a tie-breaker for most specific field name + if err.Type() == "invalid_type" && mostSpecificError.Type() != "invalid_type" { + mostSpecificError = err + } + } + } + + return mostSpecificError +} + +func specificity(err gojsonschema.ResultError) int { + return len(strings.Split(err.Field(), ".")) +} diff --git a/components/cli/compose/schema/schema_test.go b/components/cli/compose/schema/schema_test.go new file mode 100644 index 0000000000..be98f807de --- /dev/null +++ b/components/cli/compose/schema/schema_test.go @@ -0,0 +1,35 @@ +package schema + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +type dict map[string]interface{} + +func TestValid(t *testing.T) { + config := dict{ + "version": "2.1", + "services": dict{ + "foo": dict{ + "image": "busybox", + }, + }, + } + + assert.NoError(t, Validate(config)) +} + +func TestUndefinedTopLevelOption(t *testing.T) { + config := dict{ + "version": "2.1", + "helicopters": dict{ + "foo": dict{ + "image": "busybox", + }, + }, + } + + assert.Error(t, Validate(config)) +} diff --git a/components/cli/compose/template/template.go b/components/cli/compose/template/template.go new file mode 100644 index 0000000000..28495baf50 --- /dev/null +++ b/components/cli/compose/template/template.go @@ -0,0 +1,100 @@ +package template + +import ( + "fmt" + "regexp" + "strings" +) + +var delimiter = "\\$" +var substitution = "[_a-z][_a-z0-9]*(?::?-[^}]+)?" + +var patternString = fmt.Sprintf( + "%s(?i:(?P%s)|(?P%s)|{(?P%s)}|(?P))", + delimiter, delimiter, substitution, substitution, +) + +var pattern = regexp.MustCompile(patternString) + +// InvalidTemplateError is returned when a variable template is not in a valid +// format +type InvalidTemplateError struct { + Template string +} + +func (e InvalidTemplateError) Error() string { + return fmt.Sprintf("Invalid template: %#v", e.Template) +} + +// Mapping is a user-supplied function which maps from variable names to values. +// Returns the value as a string and a bool indicating whether +// the value is present, to distinguish between an empty string +// and the absence of a value. +type Mapping func(string) (string, bool) + +// Substitute variables in the string with their values +func Substitute(template string, mapping Mapping) (result string, err *InvalidTemplateError) { + result = pattern.ReplaceAllStringFunc(template, func(substring string) string { + matches := pattern.FindStringSubmatch(substring) + groups := make(map[string]string) + for i, name := range pattern.SubexpNames() { + if i != 0 { + groups[name] = matches[i] + } + } + + substitution := groups["named"] + if substitution == "" { + substitution = groups["braced"] + } + if substitution != "" { + // Soft default (fall back if unset or empty) + if strings.Contains(substitution, ":-") { + name, defaultValue := partition(substitution, ":-") + value, ok := mapping(name) + if !ok || value == "" { + return defaultValue + } + return value + } + + // Hard default (fall back if-and-only-if empty) + if strings.Contains(substitution, "-") { + name, defaultValue := partition(substitution, "-") + value, ok := mapping(name) + if !ok { + return defaultValue + } + return value + } + + // No default (fall back to empty string) + value, ok := mapping(substitution) + if !ok { + return "" + } + return value + } + + if escaped := groups["escaped"]; escaped != "" { + return escaped + } + + err = &InvalidTemplateError{Template: template} + return "" + }) + + return result, err +} + +// Split the string at the first occurrence of sep, and return the part before the separator, +// and the part after the separator. +// +// If the separator is not found, return the string itself, followed by an empty string. +func partition(s, sep string) (string, string) { + if strings.Contains(s, sep) { + parts := strings.SplitN(s, sep, 2) + return parts[0], parts[1] + } + return s, "" +} diff --git a/components/cli/compose/template/template_test.go b/components/cli/compose/template/template_test.go new file mode 100644 index 0000000000..6b81bf0a39 --- /dev/null +++ b/components/cli/compose/template/template_test.go @@ -0,0 +1,83 @@ +package template + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +var defaults = map[string]string{ + "FOO": "first", + "BAR": "", +} + +func defaultMapping(name string) (string, bool) { + val, ok := defaults[name] + return val, ok +} + +func TestEscaped(t *testing.T) { + result, err := Substitute("$${foo}", defaultMapping) + assert.NoError(t, err) + assert.Equal(t, "${foo}", result) +} + +func TestInvalid(t *testing.T) { + invalidTemplates := []string{ + "${", + "$}", + "${}", + "${ }", + "${ foo}", + "${foo }", + "${foo!}", + } + + for _, template := range invalidTemplates { + _, err := Substitute(template, defaultMapping) + assert.Error(t, err) + assert.IsType(t, &InvalidTemplateError{}, err) + } +} + +func TestNoValueNoDefault(t *testing.T) { + for _, template := range []string{"This ${missing} var", "This ${BAR} var"} { + result, err := Substitute(template, defaultMapping) + assert.NoError(t, err) + assert.Equal(t, "This var", result) + } +} + +func TestValueNoDefault(t *testing.T) { + for _, template := range []string{"This $FOO var", "This ${FOO} var"} { + result, err := Substitute(template, defaultMapping) + assert.NoError(t, err) + assert.Equal(t, "This first var", result) + } +} + +func TestNoValueWithDefault(t *testing.T) { + for _, template := range []string{"ok ${missing:-def}", "ok ${missing-def}"} { + result, err := Substitute(template, defaultMapping) + assert.NoError(t, err) + assert.Equal(t, "ok def", result) + } +} + +func TestEmptyValueWithSoftDefault(t *testing.T) { + result, err := Substitute("ok ${BAR:-def}", defaultMapping) + assert.NoError(t, err) + assert.Equal(t, "ok def", result) +} + +func TestEmptyValueWithHardDefault(t *testing.T) { + result, err := Substitute("ok ${BAR-def}", defaultMapping) + assert.NoError(t, err) + assert.Equal(t, "ok ", result) +} + +func TestNonAlphanumericDefault(t *testing.T) { + result, err := Substitute("ok ${BAR:-/non:-alphanumeric}", defaultMapping) + assert.NoError(t, err) + assert.Equal(t, "ok /non:-alphanumeric", result) +} diff --git a/components/cli/compose/types/types.go b/components/cli/compose/types/types.go new file mode 100644 index 0000000000..45923b3460 --- /dev/null +++ b/components/cli/compose/types/types.go @@ -0,0 +1,232 @@ +package types + +import ( + "time" +) + +// UnsupportedProperties not yet supported by this implementation of the compose file +var UnsupportedProperties = []string{ + "build", + "cap_add", + "cap_drop", + "cgroup_parent", + "devices", + "dns", + "dns_search", + "domainname", + "external_links", + "ipc", + "links", + "mac_address", + "network_mode", + "privileged", + "read_only", + "restart", + "security_opt", + "shm_size", + "stop_signal", + "tmpfs", +} + +// DeprecatedProperties that were removed from the v3 format, but their +// use should not impact the behaviour of the application. +var DeprecatedProperties = map[string]string{ + "container_name": "Setting the container name is not supported.", + "expose": "Exposing ports is unnecessary - services on the same network can access each other's containers on any port.", +} + +// ForbiddenProperties that are not supported in this implementation of the +// compose file. +var ForbiddenProperties = map[string]string{ + "extends": "Support for `extends` is not implemented yet. Use `docker-compose config` to generate a configuration with all `extends` options resolved, and deploy from that.", + "volume_driver": "Instead of setting the volume driver on the service, define a volume using the top-level `volumes` option and specify the driver there.", + "volumes_from": "To share a volume between services, define it using the top-level `volumes` option and reference it from each service that shares it using the service-level `volumes` option.", + "cpu_quota": "Set resource limits using deploy.resources", + "cpu_shares": "Set resource limits using deploy.resources", + "cpuset": "Set resource limits using deploy.resources", + "mem_limit": "Set resource limits using deploy.resources", + "memswap_limit": "Set resource limits using deploy.resources", +} + +// Dict is a mapping of strings to interface{} +type Dict map[string]interface{} + +// ConfigFile is a filename and the contents of the file as a Dict +type ConfigFile struct { + Filename string + Config Dict +} + +// ConfigDetails are the details about a group of ConfigFiles +type ConfigDetails struct { + WorkingDir string + ConfigFiles []ConfigFile + Environment map[string]string +} + +// Config is a full compose file configuration +type Config struct { + Services []ServiceConfig + Networks map[string]NetworkConfig + Volumes map[string]VolumeConfig +} + +// ServiceConfig is the configuration of one service +type ServiceConfig struct { + Name string + + CapAdd []string `mapstructure:"cap_add"` + CapDrop []string `mapstructure:"cap_drop"` + CgroupParent string `mapstructure:"cgroup_parent"` + Command []string `compose:"shell_command"` + ContainerName string `mapstructure:"container_name"` + DependsOn []string `mapstructure:"depends_on"` + Deploy DeployConfig + Devices []string + DNS []string `compose:"string_or_list"` + DNSSearch []string `mapstructure:"dns_search" compose:"string_or_list"` + DomainName string `mapstructure:"domainname"` + Entrypoint []string `compose:"shell_command"` + Environment map[string]string `compose:"list_or_dict_equals"` + Expose []string `compose:"list_of_strings_or_numbers"` + ExternalLinks []string `mapstructure:"external_links"` + ExtraHosts map[string]string `mapstructure:"extra_hosts" compose:"list_or_dict_colon"` + Hostname string + HealthCheck *HealthCheckConfig + Image string + Ipc string + Labels map[string]string `compose:"list_or_dict_equals"` + Links []string + Logging *LoggingConfig + MacAddress string `mapstructure:"mac_address"` + NetworkMode string `mapstructure:"network_mode"` + Networks map[string]*ServiceNetworkConfig `compose:"list_or_struct_map"` + Pid string + Ports []string `compose:"list_of_strings_or_numbers"` + Privileged bool + ReadOnly bool `mapstructure:"read_only"` + Restart string + SecurityOpt []string `mapstructure:"security_opt"` + StdinOpen bool `mapstructure:"stdin_open"` + StopGracePeriod *time.Duration `mapstructure:"stop_grace_period"` + StopSignal string `mapstructure:"stop_signal"` + Tmpfs []string `compose:"string_or_list"` + Tty bool `mapstructure:"tty"` + Ulimits map[string]*UlimitsConfig + User string + Volumes []string + WorkingDir string `mapstructure:"working_dir"` +} + +// LoggingConfig the logging configuration for a service +type LoggingConfig struct { + Driver string + Options map[string]string +} + +// DeployConfig the deployment configuration for a service +type DeployConfig struct { + Mode string + Replicas *uint64 + Labels map[string]string `compose:"list_or_dict_equals"` + UpdateConfig *UpdateConfig `mapstructure:"update_config"` + Resources Resources + RestartPolicy *RestartPolicy `mapstructure:"restart_policy"` + Placement Placement +} + +// HealthCheckConfig the healthcheck configuration for a service +type HealthCheckConfig struct { + Test []string `compose:"healthcheck"` + Timeout string + Interval string + Retries *uint64 + Disable bool +} + +// UpdateConfig the service update configuration +type UpdateConfig struct { + Parallelism *uint64 + Delay time.Duration + FailureAction string `mapstructure:"failure_action"` + Monitor time.Duration + MaxFailureRatio float32 `mapstructure:"max_failure_ratio"` +} + +// Resources the resource limits and reservations +type Resources struct { + Limits *Resource + Reservations *Resource +} + +// Resource is a resource to be limited or reserved +type Resource struct { + // TODO: types to convert from units and ratios + NanoCPUs string `mapstructure:"cpus"` + MemoryBytes UnitBytes `mapstructure:"memory"` +} + +// UnitBytes is the bytes type +type UnitBytes int64 + +// RestartPolicy the service restart policy +type RestartPolicy struct { + Condition string + Delay *time.Duration + MaxAttempts *uint64 `mapstructure:"max_attempts"` + Window *time.Duration +} + +// Placement constraints for the service +type Placement struct { + Constraints []string +} + +// ServiceNetworkConfig is the network configuration for a service +type ServiceNetworkConfig struct { + Aliases []string + Ipv4Address string `mapstructure:"ipv4_address"` + Ipv6Address string `mapstructure:"ipv6_address"` +} + +// UlimitsConfig the ulimit configuration +type UlimitsConfig struct { + Single int + Soft int + Hard int +} + +// NetworkConfig for a network +type NetworkConfig struct { + Driver string + DriverOpts map[string]string `mapstructure:"driver_opts"` + Ipam IPAMConfig + External External + Labels map[string]string `compose:"list_or_dict_equals"` +} + +// IPAMConfig for a network +type IPAMConfig struct { + Driver string + Config []*IPAMPool +} + +// IPAMPool for a network +type IPAMPool struct { + Subnet string +} + +// VolumeConfig for a volume +type VolumeConfig struct { + Driver string + DriverOpts map[string]string `mapstructure:"driver_opts"` + External External + Labels map[string]string `compose:"list_or_dict_equals"` +} + +// External identifies a Volume or Network as a reference to a resource that is +// not managed, and should already exist. +type External struct { + Name string + External bool +} From 275438c0f75ddcd7bd544410f7392be15365de4b Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 22 Sep 2016 14:11:08 -0400 Subject: [PATCH 500/978] Read long description from a file. Signed-off-by: Daniel Nephin Upstream-commit: 48930c8bbf8ec4d6a022c1b204dd326dae9089bc Component: cli --- components/cli/command/volume/cmd.go | 19 ----------- components/cli/command/volume/create.go | 40 ------------------------ components/cli/command/volume/inspect.go | 9 ------ components/cli/command/volume/list.go | 17 ---------- 4 files changed, 85 deletions(-) diff --git a/components/cli/command/volume/cmd.go b/components/cli/command/volume/cmd.go index 40862f29d1..2bc7687750 100644 --- a/components/cli/command/volume/cmd.go +++ b/components/cli/command/volume/cmd.go @@ -12,7 +12,6 @@ func NewVolumeCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := &cobra.Command{ Use: "volume COMMAND", Short: "Manage volumes", - Long: volumeDescription, Args: cli.NoArgs, RunE: dockerCli.ShowHelp, } @@ -25,21 +24,3 @@ func NewVolumeCommand(dockerCli *command.DockerCli) *cobra.Command { ) return cmd } - -var volumeDescription = ` -The **docker volume** command has subcommands for managing data volumes. A data -volume is a specially-designated directory that by-passes storage driver -management. - -Data volumes persist data independent of a container's life cycle. When you -delete a container, the Docker daemon does not delete any data volumes. You can -share volumes across multiple containers. Moreover, you can share data volumes -with other computing resources in your system. - -To see help for a subcommand, use: - - docker volume COMMAND --help - -For full details on using docker volume visit Docker's online documentation. - -` diff --git a/components/cli/command/volume/create.go b/components/cli/command/volume/create.go index 7b2a7e3318..de45ce67e8 100644 --- a/components/cli/command/volume/create.go +++ b/components/cli/command/volume/create.go @@ -29,7 +29,6 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := &cobra.Command{ Use: "create [OPTIONS] [VOLUME]", Short: "Create a volume", - Long: createDescription, Args: cli.RequiresMaxArgs(1), RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 1 { @@ -70,42 +69,3 @@ func runCreate(dockerCli *command.DockerCli, opts createOptions) error { fmt.Fprintf(dockerCli.Out(), "%s\n", vol.Name) return nil } - -var createDescription = ` -Creates a new volume that containers can consume and store data in. If a name -is not specified, Docker generates a random name. You create a volume and then -configure the container to use it, for example: - - $ docker volume create hello - hello - $ docker run -d -v hello:/world busybox ls /world - -The mount is created inside the container's **/src** directory. Docker doesn't -not support relative paths for mount points inside the container. - -Multiple containers can use the same volume in the same time period. This is -useful if two containers need access to shared data. For example, if one -container writes and the other reads the data. - -## Driver specific options - -Some volume drivers may take options to customize the volume creation. Use the -**-o** or **--opt** flags to pass driver options: - - $ docker volume create --driver fake --opt tardis=blue --opt timey=wimey - -These options are passed directly to the volume driver. Options for different -volume drivers may do different things (or nothing at all). - -The built-in **local** driver on Windows does not support any options. - -The built-in **local** driver on Linux accepts options similar to the linux -**mount** command: - - $ docker volume create --driver local --opt type=tmpfs --opt device=tmpfs --opt o=size=100m,uid=1000 - -Another example: - - $ docker volume create --driver local --opt type=btrfs --opt device=/dev/sda2 - -` diff --git a/components/cli/command/volume/inspect.go b/components/cli/command/volume/inspect.go index 5eb8ad2516..f58b927ace 100644 --- a/components/cli/command/volume/inspect.go +++ b/components/cli/command/volume/inspect.go @@ -20,7 +20,6 @@ func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := &cobra.Command{ Use: "inspect [OPTIONS] VOLUME [VOLUME...]", Short: "Display detailed information on one or more volumes", - Long: inspectDescription, Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.names = args @@ -45,11 +44,3 @@ func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { return inspect.Inspect(dockerCli.Out(), opts.names, opts.format, getVolFunc) } - -var inspectDescription = ` -Returns information about one or more volumes. By default, this command renders -all results in a JSON array. You can specify an alternate format to execute a -given template is executed for each result. Go's https://golang.org/pkg/text/template/ -package describes all the details of the format. - -` diff --git a/components/cli/command/volume/list.go b/components/cli/command/volume/list.go index d76006a1b2..0de83aea4e 100644 --- a/components/cli/command/volume/list.go +++ b/components/cli/command/volume/list.go @@ -34,7 +34,6 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command { Use: "ls [OPTIONS]", Aliases: []string{"list"}, Short: "List volumes", - Long: listDescription, Args: cli.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { return runList(dockerCli, opts) @@ -73,19 +72,3 @@ func runList(dockerCli *command.DockerCli, opts listOptions) error { } return formatter.VolumeWrite(volumeCtx, volumes.Volumes) } - -var listDescription = ` - -Lists all the volumes Docker manages. You can filter using the **-f** or -**--filter** flag. The filtering format is a **key=value** pair. To specify -more than one filter, pass multiple flags (for example, -**--filter "foo=bar" --filter "bif=baz"**) - -The currently supported filters are: - -* **dangling** (boolean - **true** or **false**, **1** or **0**) -* **driver** (a volume driver's name) -* **label** (**label=** or **label==**) -* **name** (a volume's name) - -` From 800c425eab98f62da68dddb38e3a2a5483115c99 Mon Sep 17 00:00:00 2001 From: Xianglin Gao Date: Wed, 28 Dec 2016 16:28:32 +0800 Subject: [PATCH 501/978] exit collect when we get EOF Signed-off-by: Xianglin Gao Upstream-commit: 5860dd5f80b842fe026542118eb565c20189a7d2 Component: cli --- components/cli/command/container/stats_helpers.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/components/cli/command/container/stats_helpers.go b/components/cli/command/container/stats_helpers.go index 4b57e3fe05..4a58134a4e 100644 --- a/components/cli/command/container/stats_helpers.go +++ b/components/cli/command/container/stats_helpers.go @@ -155,11 +155,13 @@ func collect(ctx context.Context, s *formatter.ContainerStats, cli client.APICli waitFirst.Done() } case err := <-u: + s.SetError(err) + if err == io.EOF { + break + } if err != nil { - s.SetError(err) continue } - s.SetError(nil) // if this is the first stat you get, release WaitGroup if !getFirst { getFirst = true From 238778432e5f6f012378cf25cbefbefe6b545a68 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Fri, 30 Dec 2016 18:15:53 +0100 Subject: [PATCH 502/978] Update order of '--secret-rm' and '--secret-add' When using both `--secret-rm` and `--secret-add` on `docker service update`, `--secret-rm` was always performed last. This made it impossible to update a secret that was already in use on a service (for example, to change it's permissions, or mount-location inside the container). This patch changes the order in which `rm` and `add` are performed, allowing updating a secret in a single `docker service update`. Before this change, the `rm` was always performed "last", so the secret was always removed: $ echo "foo" | docker secret create foo -f - foo $ docker service create --name myservice --secret foo nginx:alpine 62xjcr9sr0c2hvepdzqrn3ssn $ docker service update --secret-rm foo --secret-add source=foo,target=foo2 myservice myservice $ docker service inspect --format '{{ json .Spec.TaskTemplate.ContainerSpec.Secrets }}' myservice | jq . null After this change, the `rm` is performed _first_, allowing users to update a secret without updating the service _twice_; $ echo "foo" | docker secret create foo -f - 1bllmvw3a1yaq3eixqw3f7bjl $ docker service create --name myservice --secret foo nginx:alpine lr6s3uoggli1x0hab78glpcxo $ docker service update --secret-rm foo --secret-add source=foo,target=foo2 myservice myservice $ docker service inspect --format '{{ json .Spec.TaskTemplate.ContainerSpec.Secrets }}' myservice | jq . [ { "File": { "Name": "foo2", "UID": "0", "GID": "0", "Mode": 292 }, "SecretID": "tn9qiblgnuuut11eufquw5dev", "SecretName": "foo" } ] Signed-off-by: Sebastiaan van Stijn Upstream-commit: edeb5b6e0d3ac2bc6db33735a503cecccc2828de Component: cli --- components/cli/command/service/parse.go | 2 +- components/cli/command/service/update.go | 23 ++++---- components/cli/command/service/update_test.go | 57 +++++++++++++++++++ 3 files changed, 70 insertions(+), 12 deletions(-) diff --git a/components/cli/command/service/parse.go b/components/cli/command/service/parse.go index ff3249e581..6af7e3bb8e 100644 --- a/components/cli/command/service/parse.go +++ b/components/cli/command/service/parse.go @@ -12,7 +12,7 @@ import ( // parseSecrets retrieves the secrets from the requested names and converts // them to secret references to use with the spec -func parseSecrets(client client.APIClient, requestedSecrets []*types.SecretRequestOption) ([]*swarmtypes.SecretReference, error) { +func parseSecrets(client client.SecretAPIClient, requestedSecrets []*types.SecretRequestOption) ([]*swarmtypes.SecretReference, error) { secretRefs := make(map[string]*swarmtypes.SecretReference) ctx := context.Background() diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index 514b1bd510..6d13927dae 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -6,8 +6,6 @@ import ( "strings" "time" - "golang.org/x/net/context" - "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" mounttypes "github.com/docker/docker/api/types/mount" @@ -21,6 +19,7 @@ import ( shlex "github.com/flynn-archive/go-shlex" "github.com/spf13/cobra" "github.com/spf13/pflag" + "golang.org/x/net/context" ) func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { @@ -431,7 +430,16 @@ func updateEnvironment(flags *pflag.FlagSet, field *[]string) { *field = removeItems(*field, toRemove, envKey) } -func getUpdatedSecrets(apiClient client.APIClient, flags *pflag.FlagSet, secrets []*swarm.SecretReference) ([]*swarm.SecretReference, error) { +func getUpdatedSecrets(apiClient client.SecretAPIClient, flags *pflag.FlagSet, secrets []*swarm.SecretReference) ([]*swarm.SecretReference, error) { + newSecrets := []*swarm.SecretReference{} + + toRemove := buildToRemoveSet(flags, flagSecretRemove) + for _, secret := range secrets { + if _, exists := toRemove[secret.SecretName]; !exists { + newSecrets = append(newSecrets, secret) + } + } + if flags.Changed(flagSecretAdd) { values := flags.Lookup(flagSecretAdd).Value.(*opts.SecretOpt).Value() @@ -439,14 +447,7 @@ func getUpdatedSecrets(apiClient client.APIClient, flags *pflag.FlagSet, secrets if err != nil { return nil, err } - secrets = append(secrets, addSecrets...) - } - toRemove := buildToRemoveSet(flags, flagSecretRemove) - newSecrets := []*swarm.SecretReference{} - for _, secret := range secrets { - if _, exists := toRemove[secret.SecretName]; !exists { - newSecrets = append(newSecrets, secret) - } + newSecrets = append(newSecrets, addSecrets...) } return newSecrets, nil diff --git a/components/cli/command/service/update_test.go b/components/cli/command/service/update_test.go index 08fe248769..a6df6b985e 100644 --- a/components/cli/command/service/update_test.go +++ b/components/cli/command/service/update_test.go @@ -6,10 +6,12 @@ import ( "testing" "time" + "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" mounttypes "github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/pkg/testutil/assert" + "golang.org/x/net/context" ) func TestUpdateServiceArgs(t *testing.T) { @@ -382,3 +384,58 @@ func TestValidatePort(t *testing.T) { assert.Error(t, err, e) } } + +type secretAPIClientMock struct { + listResult []swarm.Secret +} + +func (s secretAPIClientMock) SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) { + return s.listResult, nil +} +func (s secretAPIClientMock) SecretCreate(ctx context.Context, secret swarm.SecretSpec) (types.SecretCreateResponse, error) { + return types.SecretCreateResponse{}, nil +} +func (s secretAPIClientMock) SecretRemove(ctx context.Context, id string) error { + return nil +} +func (s secretAPIClientMock) SecretInspectWithRaw(ctx context.Context, name string) (swarm.Secret, []byte, error) { + return swarm.Secret{}, []byte{}, nil +} + +// TestUpdateSecretUpdateInPlace tests the ability to update the "target" of an secret with "docker service update" +// by combining "--secret-rm" and "--secret-add" for the same secret. +func TestUpdateSecretUpdateInPlace(t *testing.T) { + apiClient := secretAPIClientMock{ + listResult: []swarm.Secret{ + { + ID: "tn9qiblgnuuut11eufquw5dev", + Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "foo"}}, + }, + }, + } + + flags := newUpdateCommand(nil).Flags() + flags.Set("secret-add", "source=foo,target=foo2") + flags.Set("secret-rm", "foo") + + secrets := []*swarm.SecretReference{ + { + File: &swarm.SecretReferenceFileTarget{ + Name: "foo", + UID: "0", + GID: "0", + Mode: 292, + }, + SecretID: "tn9qiblgnuuut11eufquw5dev", + SecretName: "foo", + }, + } + + updatedSecrets, err := getUpdatedSecrets(apiClient, flags, secrets) + + assert.Equal(t, err, nil) + assert.Equal(t, len(updatedSecrets), 1) + assert.Equal(t, updatedSecrets[0].SecretID, "tn9qiblgnuuut11eufquw5dev") + assert.Equal(t, updatedSecrets[0].SecretName, "foo") + assert.Equal(t, updatedSecrets[0].File.Name, "foo2") +} From 9b0127d16739a6adf56f35fd0a9fce37b48e6bb1 Mon Sep 17 00:00:00 2001 From: Harald Albers Date: Sat, 31 Dec 2016 09:55:04 -0800 Subject: [PATCH 503/978] Fix usage message of `plugin inspect` Signed-off-by: Harald Albers Upstream-commit: 87fea846fc1223d9594c2e4e68d9cb9e2b1e72d0 Component: cli --- components/cli/command/plugin/inspect.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/plugin/inspect.go b/components/cli/command/plugin/inspect.go index 46ec7b229b..c2c7a0d6bc 100644 --- a/components/cli/command/plugin/inspect.go +++ b/components/cli/command/plugin/inspect.go @@ -17,7 +17,7 @@ func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { var opts inspectOptions cmd := &cobra.Command{ - Use: "inspect [OPTIONS] PLUGIN|ID [PLUGIN|ID...]", + Use: "inspect [OPTIONS] PLUGIN [PLUGIN...]", Short: "Display detailed information on one or more plugins", Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { From ce5e5fc46ffaa048a154f25fb9b6dcf45e8e6a50 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 2 Jan 2017 11:19:33 +0100 Subject: [PATCH 504/978] Remove deadcode from `service/opts.go`, `SecretOpt` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `SecretOpt` is in the `opts` package, this one is never used, so it's dead code, removing it 👼. Signed-off-by: Vincent Demeester Upstream-commit: b5bc69238cfdf6414e8757fc37606e9b561fcfae Component: cli --- components/cli/command/service/opts.go | 95 -------------------------- 1 file changed, 95 deletions(-) diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index 78c27eae2c..b794b07a30 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -1,10 +1,7 @@ package service import ( - "encoding/csv" "fmt" - "os" - "path/filepath" "strconv" "strings" "time" @@ -142,98 +139,6 @@ func (f *floatValue) Value() float32 { return float32(*f) } -// SecretRequestSpec is a type for requesting secrets -type SecretRequestSpec struct { - source string - target string - uid string - gid string - mode os.FileMode -} - -// SecretOpt is a Value type for parsing secrets -type SecretOpt struct { - values []*SecretRequestSpec -} - -// Set a new secret value -func (o *SecretOpt) Set(value string) error { - csvReader := csv.NewReader(strings.NewReader(value)) - fields, err := csvReader.Read() - if err != nil { - return err - } - - spec := &SecretRequestSpec{ - source: "", - target: "", - uid: "0", - gid: "0", - mode: 0444, - } - - for _, field := range fields { - parts := strings.SplitN(field, "=", 2) - key := strings.ToLower(parts[0]) - - if len(parts) != 2 { - return fmt.Errorf("invalid field '%s' must be a key=value pair", field) - } - - value := parts[1] - switch key { - case "source", "src": - spec.source = value - case "target": - tDir, _ := filepath.Split(value) - if tDir != "" { - return fmt.Errorf("target must not have a path") - } - spec.target = value - case "uid": - spec.uid = value - case "gid": - spec.gid = value - case "mode": - m, err := strconv.ParseUint(value, 0, 32) - if err != nil { - return fmt.Errorf("invalid mode specified: %v", err) - } - - spec.mode = os.FileMode(m) - default: - return fmt.Errorf("invalid field in secret request: %s", key) - } - } - - if spec.source == "" { - return fmt.Errorf("source is required") - } - - o.values = append(o.values, spec) - return nil -} - -// Type returns the type of this option -func (o *SecretOpt) Type() string { - return "secret" -} - -// String returns a string repr of this option -func (o *SecretOpt) String() string { - secrets := []string{} - for _, secret := range o.values { - repr := fmt.Sprintf("%s -> %s", secret.source, secret.target) - secrets = append(secrets, repr) - } - return strings.Join(secrets, ", ") -} - -// Value returns the secret requests -func (o *SecretOpt) Value() []*SecretRequestSpec { - return o.values -} - type updateOptions struct { parallelism uint64 delay time.Duration From bff364bed941a6de6216ba70af0ba6c896e623b6 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 9 Nov 2016 16:15:32 -0500 Subject: [PATCH 505/978] Generate ContainerChanges from swagger spec. Signed-off-by: Daniel Nephin Upstream-commit: 0a623b251f1c6a0b07d3b5f6d1f0e5c47c5400c5 Component: cli --- components/cli/container_diff.go | 6 +++--- components/cli/container_diff_test.go | 4 ++-- components/cli/interface.go | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/components/cli/container_diff.go b/components/cli/container_diff.go index 1e3e554fc5..884dc9feef 100644 --- a/components/cli/container_diff.go +++ b/components/cli/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/components/cli/container_diff_test.go b/components/cli/container_diff_test.go index 1ce1117684..57dd73e66d 100644 --- a/components/cli/container_diff_test.go +++ b/components/cli/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/components/cli/interface.go b/components/cli/interface.go index 00b9adea32..5e1b63b39d 100644 --- a/components/cli/interface.go +++ b/components/cli/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 c5e636a36d9200325947d560f715814f81a718f3 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 9 Nov 2016 16:32:53 -0500 Subject: [PATCH 506/978] Generate ImageHistory from swagger spec. Signed-off-by: Daniel Nephin Upstream-commit: 09bd6619791a4b67afa0c598bbc525406bd9f130 Component: cli --- components/cli/image_history.go | 6 +++--- components/cli/image_history_test.go | 4 ++-- components/cli/interface.go | 3 ++- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/components/cli/image_history.go b/components/cli/image_history.go index acb1ee9278..7b4babcba3 100644 --- a/components/cli/image_history.go +++ b/components/cli/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/components/cli/image_history_test.go b/components/cli/image_history_test.go index 729edb1ad5..101bffd0c3 100644 --- a/components/cli/image_history_test.go +++ b/components/cli/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/components/cli/interface.go b/components/cli/interface.go index 5e1b63b39d..742f9a6c17 100644 --- a/components/cli/interface.go +++ b/components/cli/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 b3c70d598cf1227ee6039b64e43d6b6097265b3e Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 10 Nov 2016 11:27:56 -0500 Subject: [PATCH 507/978] Generate ImageDeleteResponse from swagger spec. Signed-off-by: Daniel Nephin Upstream-commit: f7e58c8c9bcb9981fdc8fd9af068669a187cdf3e Component: cli --- components/cli/image_remove.go | 4 ++-- components/cli/image_remove_test.go | 2 +- components/cli/interface.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/components/cli/image_remove.go b/components/cli/image_remove.go index 839e5311c4..6921209ee1 100644 --- a/components/cli/image_remove.go +++ b/components/cli/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/components/cli/image_remove_test.go b/components/cli/image_remove_test.go index 7b004f70e6..9856311305 100644 --- a/components/cli/image_remove_test.go +++ b/components/cli/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/components/cli/interface.go b/components/cli/interface.go index 742f9a6c17..e3bcb19950 100644 --- a/components/cli/interface.go +++ b/components/cli/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 875d2b1b22ff164ff85e470afea6102ee041277c Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 14 Nov 2016 14:50:16 -0500 Subject: [PATCH 508/978] Convert ContainerTopOKResponse from swagger spec. Signed-off-by: Daniel Nephin Upstream-commit: 9eda7f4daf3caced7886be181f28c350584d68e0 Component: cli --- components/cli/container_top.go | 6 +++--- components/cli/container_top_test.go | 4 ++-- components/cli/interface.go | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/components/cli/container_top.go b/components/cli/container_top.go index 4e7270ea22..9689123a40 100644 --- a/components/cli/container_top.go +++ b/components/cli/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/components/cli/container_top_test.go b/components/cli/container_top_test.go index 7802be063e..68ccef505d 100644 --- a/components/cli/container_top_test.go +++ b/components/cli/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/components/cli/interface.go b/components/cli/interface.go index e3bcb19950..ef9b10bba3 100644 --- a/components/cli/interface.go +++ b/components/cli/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 27db371a9223e3fc6408a0aedf422fd4e72c40a1 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 3 Jan 2017 15:58:41 -0500 Subject: [PATCH 509/978] Trim quotes from TLS flags. Signed-off-by: Daniel Nephin Upstream-commit: fe181a18d5f818c15b0647f2a6d272480db71017 Component: cli --- components/cli/flags/common.go | 12 ++++++--- components/cli/flags/common_test.go | 42 +++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 components/cli/flags/common_test.go diff --git a/components/cli/flags/common.go b/components/cli/flags/common.go index 9d3245c99c..af2fe0603a 100644 --- a/components/cli/flags/common.go +++ b/components/cli/flags/common.go @@ -59,11 +59,15 @@ func (commonOpts *CommonOptions) InstallFlags(flags *pflag.FlagSet) { // TODO use flag flags.String("identity"}, "i", "", "Path to libtrust key file") - commonOpts.TLSOptions = &tlsconfig.Options{} + commonOpts.TLSOptions = &tlsconfig.Options{ + CAFile: filepath.Join(dockerCertPath, DefaultCaFile), + CertFile: filepath.Join(dockerCertPath, DefaultCertFile), + KeyFile: filepath.Join(dockerCertPath, DefaultKeyFile), + } tlsOptions := commonOpts.TLSOptions - flags.StringVar(&tlsOptions.CAFile, "tlscacert", filepath.Join(dockerCertPath, DefaultCaFile), "Trust certs signed only by this CA") - flags.StringVar(&tlsOptions.CertFile, "tlscert", filepath.Join(dockerCertPath, DefaultCertFile), "Path to TLS certificate file") - flags.StringVar(&tlsOptions.KeyFile, "tlskey", filepath.Join(dockerCertPath, DefaultKeyFile), "Path to TLS key file") + flags.Var(opts.NewQuotedString(&tlsOptions.CAFile), "tlscacert", "Trust certs signed only by this CA") + flags.Var(opts.NewQuotedString(&tlsOptions.CertFile), "tlscert", "Path to TLS certificate file") + flags.Var(opts.NewQuotedString(&tlsOptions.KeyFile), "tlskey", "Path to TLS key file") hostOpt := opts.NewNamedListOptsRef("hosts", &commonOpts.Hosts, opts.ValidateHost) flags.VarP(hostOpt, "host", "H", "Daemon socket(s) to connect to") diff --git a/components/cli/flags/common_test.go b/components/cli/flags/common_test.go new file mode 100644 index 0000000000..616d577f0b --- /dev/null +++ b/components/cli/flags/common_test.go @@ -0,0 +1,42 @@ +package flags + +import ( + "path/filepath" + "testing" + + cliconfig "github.com/docker/docker/cli/config" + "github.com/docker/docker/pkg/testutil/assert" + "github.com/spf13/pflag" +) + +func TestCommonOptionsInstallFlags(t *testing.T) { + flags := pflag.NewFlagSet("testing", pflag.ContinueOnError) + opts := NewCommonOptions() + opts.InstallFlags(flags) + + err := flags.Parse([]string{ + "--tlscacert=\"/foo/cafile\"", + "--tlscert=\"/foo/cert\"", + "--tlskey=\"/foo/key\"", + }) + assert.NilError(t, err) + assert.Equal(t, opts.TLSOptions.CAFile, "/foo/cafile") + assert.Equal(t, opts.TLSOptions.CertFile, "/foo/cert") + assert.Equal(t, opts.TLSOptions.KeyFile, "/foo/key") +} + +func defaultPath(filename string) string { + return filepath.Join(cliconfig.Dir(), filename) +} + +func TestCommonOptionsInstallFlagsWithDefaults(t *testing.T) { + flags := pflag.NewFlagSet("testing", pflag.ContinueOnError) + opts := NewCommonOptions() + opts.InstallFlags(flags) + + err := flags.Parse([]string{}) + assert.NilError(t, err) + assert.Equal(t, opts.TLSOptions.CAFile, defaultPath("ca.pem")) + assert.Equal(t, opts.TLSOptions.CertFile, defaultPath("cert.pem")) + assert.Equal(t, opts.TLSOptions.KeyFile, defaultPath("key.pem")) +} From 2fcf755a19166d7d36d3fbe2324e700e04ee8810 Mon Sep 17 00:00:00 2001 From: John Howard Date: Tue, 3 Jan 2017 11:40:44 -0800 Subject: [PATCH 510/978] Windows to Linux build warning to stdout Signed-off-by: John Howard When building a Dockerfile from a Windows client on a Linux daemon, a "security warning" is printed on stderr. Having this warning printed on stderr makes it difficult to distinguish a failed build from one that's succeeding, and the only way to suppress the warning is through the -q option, which also suppresses every output. This change prints the warning on stdout, instead of stderr, to resolve this situation. Upstream-commit: 9651fbd1974797bbf9ce447b78d6309c857be9e4 Component: cli --- components/cli/command/image/build.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go index 1e4e8a267f..5d6e611406 100644 --- a/components/cli/command/image/build.go +++ b/components/cli/command/image/build.go @@ -334,7 +334,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { // Windows: show error message about modified file permissions if the // daemon isn't running Windows. if response.OSType != "windows" && runtime.GOOS == "windows" && !options.quiet { - fmt.Fprintln(dockerCli.Err(), `SECURITY WARNING: You are building a Docker image from Windows against a non-Windows Docker host. All files and directories added to build context will have '-rwxr-xr-x' permissions. It is recommended to double check and reset permissions for sensitive files and directories.`) + fmt.Fprintln(dockerCli.Out(), `SECURITY WARNING: You are building a Docker image from Windows against a non-Windows Docker host. All files and directories added to build context will have '-rwxr-xr-x' permissions. It is recommended to double check and reset permissions for sensitive files and directories.`) } // Everything worked so if -q was provided the output from the daemon From a1174cb0594feb95940b922b1b57f6c66c17bfa0 Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Wed, 28 Dec 2016 19:34:32 +0800 Subject: [PATCH 511/978] keep network option consistent between network connect and run Signed-off-by: yuexiao-wang Upstream-commit: 567b5545401badbb18cdb6d692345372f9c3de84 Component: cli --- components/cli/command/container/opts.go | 6 +++--- components/cli/command/network/connect.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/components/cli/command/container/opts.go b/components/cli/command/container/opts.go index 0f41dd507c..c5fc152168 100644 --- a/components/cli/command/container/opts.go +++ b/components/cli/command/container/opts.go @@ -193,11 +193,11 @@ func addFlags(flags *pflag.FlagSet) *containerOptions { flags.MarkHidden("dns-opt") flags.Var(&copts.dnsSearch, "dns-search", "Set custom DNS search domains") flags.Var(&copts.expose, "expose", "Expose a port or a range of ports") - flags.StringVar(&copts.ipv4Address, "ip", "", "Container IPv4 address (e.g. 172.30.100.104)") - flags.StringVar(&copts.ipv6Address, "ip6", "", "Container IPv6 address (e.g. 2001:db8::33)") + flags.StringVar(&copts.ipv4Address, "ip", "", "IPv4 address (e.g., 172.30.100.104)") + flags.StringVar(&copts.ipv6Address, "ip6", "", "IPv6 address (e.g., 2001:db8::33)") flags.Var(&copts.links, "link", "Add link to another container") flags.Var(&copts.linkLocalIPs, "link-local-ip", "Container IPv4/IPv6 link-local addresses") - flags.StringVar(&copts.macAddress, "mac-address", "", "Container MAC address (e.g. 92:d0:c6:0a:29:33)") + flags.StringVar(&copts.macAddress, "mac-address", "", "Container MAC address (e.g., 92:d0:c6:0a:29:33)") flags.VarP(&copts.publish, "publish", "p", "Publish a container's port(s) to the host") flags.BoolVarP(&copts.publishAll, "publish-all", "P", false, "Publish all exposed ports to random ports") // We allow for both "--net" and "--network", although the latter is the recommended way. diff --git a/components/cli/command/network/connect.go b/components/cli/command/network/connect.go index 113c6c03f2..bc90ddaba7 100644 --- a/components/cli/command/network/connect.go +++ b/components/cli/command/network/connect.go @@ -37,8 +37,8 @@ func newConnectCommand(dockerCli *command.DockerCli) *cobra.Command { } flags := cmd.Flags() - flags.StringVar(&opts.ipaddress, "ip", "", "IP Address") - flags.StringVar(&opts.ipv6address, "ip6", "", "IPv6 Address") + flags.StringVar(&opts.ipaddress, "ip", "", "IPv4 address (e.g., 172.30.100.104)") + flags.StringVar(&opts.ipv6address, "ip6", "", "IPv6 address (e.g., 2001:db8::33)") flags.Var(&opts.links, "link", "Add link to another container") flags.StringSliceVar(&opts.aliases, "alias", []string{}, "Add network-scoped alias for the container") flags.StringSliceVar(&opts.linklocalips, "link-local-ip", []string{}, "Add a link-local address for the container") From c902dd671a963a4d43594fa362710c487c222f3c Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 7 Dec 2016 14:02:13 -0800 Subject: [PATCH 512/978] 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 Upstream-commit: e2416af0136b72d369de53ff7928d7cc603d4f46 Component: cli --- components/cli/command/container/prune.go | 16 ++++++++++------ components/cli/command/image/prune.go | 16 +++++++++------- components/cli/command/network/prune.go | 16 ++++++++++------ components/cli/command/prune/prune.go | 15 ++++++++------- components/cli/command/system/prune.go | 21 ++++++++++++--------- 5 files changed, 49 insertions(+), 35 deletions(-) diff --git a/components/cli/command/container/prune.go b/components/cli/command/container/prune.go index 0aad66e6ee..ca50e2e159 100644 --- a/components/cli/command/container/prune.go +++ b/components/cli/command/container/prune.go @@ -3,21 +3,22 @@ package container import ( "fmt" - "github.com/docker/docker/api/types/filters" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/opts" units "github.com/docker/go-units" "github.com/spf13/cobra" "golang.org/x/net/context" ) type pruneOptions struct { - force bool + force bool + filter opts.FilterOpt } // NewPruneCommand returns a new cobra prune command for containers func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts pruneOptions + opts := pruneOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ Use: "prune [OPTIONS]", @@ -39,6 +40,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation") + flags.Var(&opts.filter, "filter", "Provide filter values (e.g. 'until=')") return cmd } @@ -47,11 +49,13 @@ const warning = `WARNING! This will remove all stopped containers. Are you sure you want to continue?` func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed uint64, output string, err error) { + pruneFilters := opts.filter.Value() + if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) { return } - report, err := dockerCli.Client().ContainersPrune(context.Background(), filters.Args{}) + report, err := dockerCli.Client().ContainersPrune(context.Background(), pruneFilters) if err != nil { return } @@ -69,6 +73,6 @@ func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed u // RunPrune calls the Container Prune API // This returns the amount of space reclaimed and a detailed output string -func RunPrune(dockerCli *command.DockerCli) (uint64, string, error) { - return runPrune(dockerCli, pruneOptions{force: true}) +func RunPrune(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error) { + return runPrune(dockerCli, pruneOptions{force: true, filter: filter}) } diff --git a/components/cli/command/image/prune.go b/components/cli/command/image/prune.go index 82c28fcf49..f17aed7410 100644 --- a/components/cli/command/image/prune.go +++ b/components/cli/command/image/prune.go @@ -5,21 +5,22 @@ import ( "golang.org/x/net/context" - "github.com/docker/docker/api/types/filters" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/opts" units "github.com/docker/go-units" "github.com/spf13/cobra" ) type pruneOptions struct { - force bool - all bool + force bool + all bool + filter opts.FilterOpt } // NewPruneCommand returns a new cobra prune command for images func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts pruneOptions + opts := pruneOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ Use: "prune [OPTIONS]", @@ -42,6 +43,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation") flags.BoolVarP(&opts.all, "all", "a", false, "Remove all unused images, not just dangling ones") + flags.Var(&opts.filter, "filter", "Provide filter values (e.g. 'until=')") return cmd } @@ -54,7 +56,7 @@ Are you sure you want to continue?` ) func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed uint64, output string, err error) { - pruneFilters := filters.NewArgs() + pruneFilters := opts.filter.Value() pruneFilters.Add("dangling", fmt.Sprintf("%v", !opts.all)) warning := danglingWarning @@ -87,6 +89,6 @@ func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed u // RunPrune calls the Image Prune API // This returns the amount of space reclaimed and a detailed output string -func RunPrune(dockerCli *command.DockerCli, all bool) (uint64, string, error) { - return runPrune(dockerCli, pruneOptions{force: true, all: all}) +func RunPrune(dockerCli *command.DockerCli, all bool, filter opts.FilterOpt) (uint64, string, error) { + return runPrune(dockerCli, pruneOptions{force: true, all: all, filter: filter}) } diff --git a/components/cli/command/network/prune.go b/components/cli/command/network/prune.go index 9f1979e6b5..c5c5359926 100644 --- a/components/cli/command/network/prune.go +++ b/components/cli/command/network/prune.go @@ -5,19 +5,20 @@ import ( "golang.org/x/net/context" - "github.com/docker/docker/api/types/filters" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/opts" "github.com/spf13/cobra" ) type pruneOptions struct { - force bool + force bool + filter opts.FilterOpt } // NewPruneCommand returns a new cobra prune command for networks func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts pruneOptions + opts := pruneOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ Use: "prune [OPTIONS]", @@ -38,6 +39,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation") + flags.Var(&opts.filter, "filter", "Provide filter values (e.g. 'until=')") return cmd } @@ -46,11 +48,13 @@ const warning = `WARNING! This will remove all networks not used by at least one Are you sure you want to continue?` func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (output string, err error) { + pruneFilters := opts.filter.Value() + if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) { return } - report, err := dockerCli.Client().NetworksPrune(context.Background(), filters.Args{}) + report, err := dockerCli.Client().NetworksPrune(context.Background(), pruneFilters) if err != nil { return } @@ -67,7 +71,7 @@ func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (output string, e // RunPrune calls the Network Prune API // This returns the amount of space reclaimed and a detailed output string -func RunPrune(dockerCli *command.DockerCli) (uint64, string, error) { - output, err := runPrune(dockerCli, pruneOptions{force: true}) +func RunPrune(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error) { + output, err := runPrune(dockerCli, pruneOptions{force: true, filter: filter}) return 0, output, err } diff --git a/components/cli/command/prune/prune.go b/components/cli/command/prune/prune.go index a022487fd6..6314718c69 100644 --- a/components/cli/command/prune/prune.go +++ b/components/cli/command/prune/prune.go @@ -6,6 +6,7 @@ import ( "github.com/docker/docker/cli/command/image" "github.com/docker/docker/cli/command/network" "github.com/docker/docker/cli/command/volume" + "github.com/docker/docker/opts" "github.com/spf13/cobra" ) @@ -30,21 +31,21 @@ func NewNetworkPruneCommand(dockerCli *command.DockerCli) *cobra.Command { } // RunContainerPrune executes a prune command for containers -func RunContainerPrune(dockerCli *command.DockerCli) (uint64, string, error) { - return container.RunPrune(dockerCli) +func RunContainerPrune(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error) { + return container.RunPrune(dockerCli, filter) } // RunVolumePrune executes a prune command for volumes -func RunVolumePrune(dockerCli *command.DockerCli) (uint64, string, error) { +func RunVolumePrune(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error) { return volume.RunPrune(dockerCli) } // RunImagePrune executes a prune command for images -func RunImagePrune(dockerCli *command.DockerCli, all bool) (uint64, string, error) { - return image.RunPrune(dockerCli, all) +func RunImagePrune(dockerCli *command.DockerCli, all bool, filter opts.FilterOpt) (uint64, string, error) { + return image.RunPrune(dockerCli, all, filter) } // RunNetworkPrune executes a prune command for networks -func RunNetworkPrune(dockerCli *command.DockerCli) (uint64, string, error) { - return network.RunPrune(dockerCli) +func RunNetworkPrune(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error) { + return network.RunPrune(dockerCli, filter) } diff --git a/components/cli/command/system/prune.go b/components/cli/command/system/prune.go index 92dddbdca6..46e4316f4a 100644 --- a/components/cli/command/system/prune.go +++ b/components/cli/command/system/prune.go @@ -6,18 +6,20 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/prune" + "github.com/docker/docker/opts" units "github.com/docker/go-units" "github.com/spf13/cobra" ) type pruneOptions struct { - force bool - all bool + force bool + all bool + filter opts.FilterOpt } // NewPruneCommand creates a new cobra.Command for `docker prune` func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts pruneOptions + opts := pruneOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ Use: "prune [OPTIONS]", @@ -32,6 +34,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation") flags.BoolVarP(&opts.all, "all", "a", false, "Remove all unused images not just dangling ones") + flags.Var(&opts.filter, "filter", "Provide filter values (e.g. 'until=')") return cmd } @@ -48,27 +51,27 @@ Are you sure you want to continue?` allImageDesc = `- all images without at least one container associated to them` ) -func runPrune(dockerCli *command.DockerCli, opts pruneOptions) error { +func runPrune(dockerCli *command.DockerCli, options pruneOptions) error { var message string - if opts.all { + if options.all { message = fmt.Sprintf(warning, allImageDesc) } else { message = fmt.Sprintf(warning, danglingImageDesc) } - if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), message) { + if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), message) { return nil } var spaceReclaimed uint64 - for _, pruneFn := range []func(dockerCli *command.DockerCli) (uint64, string, error){ + for _, pruneFn := range []func(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error){ prune.RunContainerPrune, prune.RunVolumePrune, prune.RunNetworkPrune, } { - spc, output, err := pruneFn(dockerCli) + spc, output, err := pruneFn(dockerCli, options.filter) if err != nil { return err } @@ -78,7 +81,7 @@ func runPrune(dockerCli *command.DockerCli, opts pruneOptions) error { } } - spc, output, err := prune.RunImagePrune(dockerCli, opts.all) + spc, output, err := prune.RunImagePrune(dockerCli, options.all, options.filter) if err != nil { return err } From 27d4e010cca6b5ff8f00611905c9c44b8c516644 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 7 Dec 2016 14:02:13 -0800 Subject: [PATCH 513/978] 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 Upstream-commit: 337483496b355b61d3aa4fd4b4f4853e59be646a Component: cli --- components/cli/container_prune_test.go | 111 +++++++++++++++++++++++++ components/cli/image_prune_test.go | 106 +++++++++++++++++++++++ components/cli/network_prune_test.go | 99 ++++++++++++++++++++++ 3 files changed, 316 insertions(+) create mode 100644 components/cli/container_prune_test.go create mode 100644 components/cli/image_prune_test.go create mode 100644 components/cli/network_prune_test.go diff --git a/components/cli/container_prune_test.go b/components/cli/container_prune_test.go new file mode 100644 index 0000000000..5f06ea0664 --- /dev/null +++ b/components/cli/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/components/cli/image_prune_test.go b/components/cli/image_prune_test.go new file mode 100644 index 0000000000..61cf18ef35 --- /dev/null +++ b/components/cli/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/components/cli/network_prune_test.go b/components/cli/network_prune_test.go new file mode 100644 index 0000000000..07a5d41f20 --- /dev/null +++ b/components/cli/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 ec49b3a694d850c9f2cb87d5dc742e82278b6f09 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 4 Jan 2017 15:17:54 -0800 Subject: [PATCH 514/978] Add `.CreatedAt` placeholder for `docker network ls --format` This fix tries to add a placeholder `.CreatedAt` for Go format template in `docker network ls --format`. While working on 29226, I noticed that it is not possible to display network's creation time in `docker network ls`, with or without `--format`. We are able to find the timestamp through `docker network inspect` though. However, as we allows networks to be pruned based on the timestamp (see 29226), showing the timestamp in `docker network ls --format` would be much useful now. This fix adds the `.CreatedAt` placeholder for `docker network ls --format`. The default output was not changed for `docker network ls --format`. A test case for unit tests has been added. Signed-off-by: Yong Tang Upstream-commit: 4e89a50a31b5956cc5aa99de47193efafc6d5989 Component: cli --- components/cli/command/formatter/network.go | 5 +++++ .../cli/command/formatter/network_test.go | 19 +++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/components/cli/command/formatter/network.go b/components/cli/command/formatter/network.go index 7fbad7d2ab..c29be412aa 100644 --- a/components/cli/command/formatter/network.go +++ b/components/cli/command/formatter/network.go @@ -115,3 +115,8 @@ func (c *networkContext) Label(name string) string { } return c.n.Labels[name] } + +func (c *networkContext) CreatedAt() string { + c.AddHeader(createdAtHeader) + return c.n.Created.String() +} diff --git a/components/cli/command/formatter/network_test.go b/components/cli/command/formatter/network_test.go index b40a534eed..e105afbdf8 100644 --- a/components/cli/command/formatter/network_test.go +++ b/components/cli/command/formatter/network_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "strings" "testing" + "time" "github.com/docker/docker/api/types" "github.com/docker/docker/pkg/stringid" @@ -142,14 +143,24 @@ network_id: networkID2 Context{Format: NewNetworkFormat("{{.Name}}", false)}, `foobar_baz foobar_bar +`, + }, + // Custom Format with CreatedAt + { + Context{Format: NewNetworkFormat("{{.Name}} {{.CreatedAt}}", false)}, + `foobar_baz 2016-01-01 00:00:00 +0000 UTC +foobar_bar 2017-01-01 00:00:00 +0000 UTC `, }, } + timestamp1, _ := time.Parse("2006-01-02", "2016-01-01") + timestamp2, _ := time.Parse("2006-01-02", "2017-01-01") + for _, testcase := range cases { networks := []types.NetworkResource{ - {ID: "networkID1", Name: "foobar_baz", Driver: "foo", Scope: "local"}, - {ID: "networkID2", Name: "foobar_bar", Driver: "bar", Scope: "local"}, + {ID: "networkID1", Name: "foobar_baz", Driver: "foo", Scope: "local", Created: timestamp1}, + {ID: "networkID2", Name: "foobar_bar", Driver: "bar", Scope: "local", Created: timestamp2}, } out := bytes.NewBufferString("") testcase.context.Output = out @@ -168,8 +179,8 @@ func TestNetworkContextWriteJSON(t *testing.T) { {ID: "networkID2", Name: "foobar_bar"}, } expectedJSONs := []map[string]interface{}{ - {"Driver": "", "ID": "networkID1", "IPv6": "false", "Internal": "false", "Labels": "", "Name": "foobar_baz", "Scope": ""}, - {"Driver": "", "ID": "networkID2", "IPv6": "false", "Internal": "false", "Labels": "", "Name": "foobar_bar", "Scope": ""}, + {"Driver": "", "ID": "networkID1", "IPv6": "false", "Internal": "false", "Labels": "", "Name": "foobar_baz", "Scope": "", "CreatedAt": "0001-01-01 00:00:00 +0000 UTC"}, + {"Driver": "", "ID": "networkID2", "IPv6": "false", "Internal": "false", "Labels": "", "Name": "foobar_bar", "Scope": "", "CreatedAt": "0001-01-01 00:00:00 +0000 UTC"}, } out := bytes.NewBufferString("") From 9009e924cad38d113324220110146a47c0531f0f Mon Sep 17 00:00:00 2001 From: wefine Date: Tue, 3 Jan 2017 23:02:58 +0800 Subject: [PATCH 515/978] check both source_image_tag and target_image_tag for 'docker image tag' Signed-off-by: wefine Upstream-commit: b71687e054e711f8432b550cf431e3da81d61756 Component: cli --- components/cli/image_tag.go | 18 ++++++++++-------- components/cli/image_tag_test.go | 13 ++++++++++++- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/components/cli/image_tag.go b/components/cli/image_tag.go index bdbf94add2..dbcd078e1c 100644 --- a/components/cli/image_tag.go +++ b/components/cli/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/components/cli/image_tag_test.go b/components/cli/image_tag_test.go index 7925db9f1b..d37bd0e85e 100644 --- a/components/cli/image_tag_test.go +++ b/components/cli/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 57533e233547d7fcf96ff26b45199dc74b431dc7 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Fri, 6 Jan 2017 12:06:02 -0800 Subject: [PATCH 516/978] remove -f on secret create and unify usage with other commands Signed-off-by: Victor Vieux Upstream-commit: 62cbb25a176cc4f743127203f4663a99d78eace7 Component: cli --- components/cli/command/secret/create.go | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/components/cli/command/secret/create.go b/components/cli/command/secret/create.go index 6967fb51ee..a3248e5dfe 100644 --- a/components/cli/command/secret/create.go +++ b/components/cli/command/secret/create.go @@ -27,17 +27,17 @@ func newSecretCreateCommand(dockerCli *command.DockerCli) *cobra.Command { } cmd := &cobra.Command{ - Use: "create [OPTIONS] SECRET", + Use: "create [OPTIONS] SECRET file|-", Short: "Create a secret from a file or STDIN as content", - Args: cli.ExactArgs(1), + Args: cli.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { createOpts.name = args[0] + createOpts.file = args[1] return runSecretCreate(dockerCli, createOpts) }, } flags := cmd.Flags() flags.VarP(&createOpts.labels, "label", "l", "Secret labels") - flags.StringVarP(&createOpts.file, "file", "f", "", "Read from a file or STDIN ('-')") return cmd } @@ -46,10 +46,6 @@ func runSecretCreate(dockerCli *command.DockerCli, options createOptions) error client := dockerCli.Client() ctx := context.Background() - if options.file == "" { - return fmt.Errorf("Please specify either a file name or STDIN ('-') with --file") - } - var in io.Reader = dockerCli.In() if options.file != "-" { file, err := system.OpenSequential(options.file) From ec2f9bcbe4c1e2a1245a1717d611ca6a6ef30783 Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Fri, 6 Jan 2017 17:23:18 -0800 Subject: [PATCH 517/978] *: use opencontainers/go-digest package The `digest` data type, used throughout docker for image verification and identity, has been broken out into `opencontainers/go-digest`. This PR updates the dependencies and moves uses over to the new type. Signed-off-by: Stephen J Day Upstream-commit: 5d67ac20cbca18a852e2239cbdc9502e64167b68 Component: cli --- components/cli/command/image/trust.go | 4 ++-- components/cli/command/service/trust.go | 4 ++-- components/cli/compose/schema/bindata.go | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/components/cli/command/image/trust.go b/components/cli/command/image/trust.go index 948e002bf2..58e0574396 100644 --- a/components/cli/command/image/trust.go +++ b/components/cli/command/image/trust.go @@ -10,7 +10,6 @@ import ( "sort" "github.com/Sirupsen/logrus" - "github.com/docker/distribution/digest" "github.com/docker/docker/api/types" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/trust" @@ -19,6 +18,7 @@ import ( "github.com/docker/docker/registry" "github.com/docker/notary/client" "github.com/docker/notary/tuf/data" + "github.com/opencontainers/go-digest" "golang.org/x/net/context" ) @@ -58,7 +58,7 @@ func PushTrustedReference(cli *command.DockerCli, repoInfo *registry.RepositoryI var pushResult types.PushResult err := json.Unmarshal(*aux, &pushResult) if err == nil && pushResult.Tag != "" { - if dgst, err := digest.ParseDigest(pushResult.Digest); err == nil { + if dgst, err := digest.Parse(pushResult.Digest); err == nil { h, err := hex.DecodeString(dgst.Hex()) if err != nil { target = nil diff --git a/components/cli/command/service/trust.go b/components/cli/command/service/trust.go index 052d49c32a..15f8a708f0 100644 --- a/components/cli/command/service/trust.go +++ b/components/cli/command/service/trust.go @@ -5,7 +5,6 @@ import ( "fmt" "github.com/Sirupsen/logrus" - "github.com/docker/distribution/digest" distreference "github.com/docker/distribution/reference" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli/command" @@ -13,6 +12,7 @@ import ( "github.com/docker/docker/reference" "github.com/docker/docker/registry" "github.com/docker/notary/tuf/data" + "github.com/opencontainers/go-digest" "github.com/pkg/errors" "golang.org/x/net/context" ) @@ -30,7 +30,7 @@ func resolveServiceImageDigest(dockerCli *command.DockerCli, service *swarm.Serv // could be parsed as a digest reference. Specifying an image ID // is valid but not resolvable. There is no warning message for // an image ID because it's valid to use one. - if _, err := digest.ParseDigest(image); err == nil { + if _, err := digest.Parse(image); err == nil { return nil } diff --git a/components/cli/compose/schema/bindata.go b/components/cli/compose/schema/bindata.go index 2acc7d29f1..c3774130b3 100644 --- a/components/cli/compose/schema/bindata.go +++ b/components/cli/compose/schema/bindata.go @@ -182,6 +182,7 @@ type bintree struct { Func func() (*asset, error) Children map[string]*bintree } + var _bintree = &bintree{nil, map[string]*bintree{ "data": &bintree{nil, map[string]*bintree{ "config_schema_v3.0.json": &bintree{dataConfig_schema_v30Json, map[string]*bintree{}}, @@ -234,4 +235,3 @@ func _filePath(dir, name string) string { cannonicalName := strings.Replace(name, "\\", "/", -1) return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) } - From d4990707919491336f7e654cbacafb09150bf7f6 Mon Sep 17 00:00:00 2001 From: ttronicum Date: Sun, 25 Dec 2016 05:13:53 +0100 Subject: [PATCH 518/978] explain since format and give examples Signed-off-by: tronicum Upstream-commit: 6081f43bd1494377ff38415b09377c0fe469120b Component: cli --- components/cli/command/container/logs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/container/logs.go b/components/cli/command/container/logs.go index 3a37cedf43..f15a644924 100644 --- a/components/cli/command/container/logs.go +++ b/components/cli/command/container/logs.go @@ -44,7 +44,7 @@ func NewLogsCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output") - flags.StringVar(&opts.since, "since", "", "Show logs since timestamp") + flags.StringVar(&opts.since, "since", "", "Show logs since timestamp (e.g. 2013-01-02T13:23:37) or relative (e.g. 42m for 42 minutes)") flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps") flags.BoolVar(&opts.details, "details", false, "Show extra details provided to logs") flags.StringVar(&opts.tail, "tail", "all", "Number of lines to show from the end of the logs") From 99b5324db73d347c85fd7467e879c55854218de2 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Sun, 25 Dec 2016 22:23:35 +0100 Subject: [PATCH 519/978] Add some unit tests to the node and swarm cli code Start work on adding unit tests to our cli code in order to have to write less costly integration test. Signed-off-by: Vincent Demeester Upstream-commit: ee8f9e084af7aaf71db3b55e345475ab079fa923 Component: cli --- components/cli/command/cli.go | 10 +- components/cli/command/node/client_test.go | 68 ++++++ components/cli/command/node/demote.go | 4 +- components/cli/command/node/demote_test.go | 88 +++++++ components/cli/command/node/inspect.go | 4 +- components/cli/command/node/inspect_test.go | 122 ++++++++++ components/cli/command/node/list.go | 4 +- components/cli/command/node/list_test.go | 101 ++++++++ components/cli/command/node/opts.go | 36 --- components/cli/command/node/promote.go | 4 +- components/cli/command/node/promote_test.go | 88 +++++++ components/cli/command/node/ps.go | 4 +- components/cli/command/node/ps_test.go | 132 +++++++++++ components/cli/command/node/remove.go | 4 +- components/cli/command/node/remove_test.go | 47 ++++ .../node-inspect-pretty.manager-leader.golden | 25 ++ .../node-inspect-pretty.manager.golden | 25 ++ .../node-inspect-pretty.simple.golden | 23 ++ .../node/testdata/node-ps.simple.golden | 2 + .../node/testdata/node-ps.with-errors.golden | 4 + components/cli/command/node/update.go | 6 +- components/cli/command/node/update_test.go | 172 ++++++++++++++ components/cli/command/swarm/client_test.go | 84 +++++++ components/cli/command/swarm/init.go | 6 +- components/cli/command/swarm/init_test.go | 129 +++++++++++ components/cli/command/swarm/join.go | 4 +- components/cli/command/swarm/join_test.go | 102 +++++++++ components/cli/command/swarm/join_token.go | 6 +- .../cli/command/swarm/join_token_test.go | 215 ++++++++++++++++++ components/cli/command/swarm/leave.go | 4 +- components/cli/command/swarm/leave_test.go | 52 +++++ components/cli/command/swarm/opts_test.go | 73 ++++++ .../swarm/testdata/init-init-autolock.golden | 11 + .../command/swarm/testdata/init-init.golden | 4 + .../testdata/jointoken-manager-quiet.golden | 1 + .../testdata/jointoken-manager-rotate.golden | 8 + .../swarm/testdata/jointoken-manager.golden | 6 + .../testdata/jointoken-worker-quiet.golden | 1 + .../swarm/testdata/jointoken-worker.golden | 6 + .../unlockkeys-unlock-key-quiet.golden | 1 + .../unlockkeys-unlock-key-rotate-quiet.golden | 1 + .../unlockkeys-unlock-key-rotate.golden | 9 + .../testdata/unlockkeys-unlock-key.golden | 7 + .../testdata/update-all-flags-quiet.golden | 1 + .../update-autolock-unlock-key.golden | 8 + .../swarm/testdata/update-noargs.golden | 13 ++ components/cli/command/swarm/unlock.go | 4 +- components/cli/command/swarm/unlock_key.go | 9 +- .../cli/command/swarm/unlock_key_test.go | 175 ++++++++++++++ components/cli/command/swarm/unlock_test.go | 101 ++++++++ components/cli/command/swarm/update.go | 14 +- components/cli/command/swarm/update_test.go | 182 +++++++++++++++ components/cli/command/task/print.go | 4 +- components/cli/internal/test/builders/node.go | 117 ++++++++++ .../cli/internal/test/builders/swarm.go | 39 ++++ components/cli/internal/test/builders/task.go | 111 +++++++++ components/cli/internal/test/cli.go | 48 ++++ 57 files changed, 2451 insertions(+), 78 deletions(-) create mode 100644 components/cli/command/node/client_test.go create mode 100644 components/cli/command/node/demote_test.go create mode 100644 components/cli/command/node/inspect_test.go create mode 100644 components/cli/command/node/list_test.go create mode 100644 components/cli/command/node/promote_test.go create mode 100644 components/cli/command/node/ps_test.go create mode 100644 components/cli/command/node/remove_test.go create mode 100644 components/cli/command/node/testdata/node-inspect-pretty.manager-leader.golden create mode 100644 components/cli/command/node/testdata/node-inspect-pretty.manager.golden create mode 100644 components/cli/command/node/testdata/node-inspect-pretty.simple.golden create mode 100644 components/cli/command/node/testdata/node-ps.simple.golden create mode 100644 components/cli/command/node/testdata/node-ps.with-errors.golden create mode 100644 components/cli/command/node/update_test.go create mode 100644 components/cli/command/swarm/client_test.go create mode 100644 components/cli/command/swarm/init_test.go create mode 100644 components/cli/command/swarm/join_test.go create mode 100644 components/cli/command/swarm/join_token_test.go create mode 100644 components/cli/command/swarm/leave_test.go create mode 100644 components/cli/command/swarm/testdata/init-init-autolock.golden create mode 100644 components/cli/command/swarm/testdata/init-init.golden create mode 100644 components/cli/command/swarm/testdata/jointoken-manager-quiet.golden create mode 100644 components/cli/command/swarm/testdata/jointoken-manager-rotate.golden create mode 100644 components/cli/command/swarm/testdata/jointoken-manager.golden create mode 100644 components/cli/command/swarm/testdata/jointoken-worker-quiet.golden create mode 100644 components/cli/command/swarm/testdata/jointoken-worker.golden create mode 100644 components/cli/command/swarm/testdata/unlockkeys-unlock-key-quiet.golden create mode 100644 components/cli/command/swarm/testdata/unlockkeys-unlock-key-rotate-quiet.golden create mode 100644 components/cli/command/swarm/testdata/unlockkeys-unlock-key-rotate.golden create mode 100644 components/cli/command/swarm/testdata/unlockkeys-unlock-key.golden create mode 100644 components/cli/command/swarm/testdata/update-all-flags-quiet.golden create mode 100644 components/cli/command/swarm/testdata/update-autolock-unlock-key.golden create mode 100644 components/cli/command/swarm/testdata/update-noargs.golden create mode 100644 components/cli/command/swarm/unlock_key_test.go create mode 100644 components/cli/command/swarm/unlock_test.go create mode 100644 components/cli/command/swarm/update_test.go create mode 100644 components/cli/internal/test/builders/node.go create mode 100644 components/cli/internal/test/builders/swarm.go create mode 100644 components/cli/internal/test/builders/task.go create mode 100644 components/cli/internal/test/cli.go diff --git a/components/cli/command/cli.go b/components/cli/command/cli.go index c287ebcf77..bf9d554608 100644 --- a/components/cli/command/cli.go +++ b/components/cli/command/cli.go @@ -32,7 +32,15 @@ type Streams interface { Err() io.Writer } -// DockerCli represents the docker command line client. +// Cli represents the docker command line client. +type Cli interface { + Client() client.APIClient + Out() *OutStream + Err() io.Writer + In() *InStream +} + +// DockerCli is an instance the docker command line client. // Instances of the client can be returned from NewDockerCli. type DockerCli struct { configFile *configfile.ConfigFile diff --git a/components/cli/command/node/client_test.go b/components/cli/command/node/client_test.go new file mode 100644 index 0000000000..1f5cdc7cee --- /dev/null +++ b/components/cli/command/node/client_test.go @@ -0,0 +1,68 @@ +package node + +import ( + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/client" + "golang.org/x/net/context" +) + +type fakeClient struct { + client.Client + infoFunc func() (types.Info, error) + nodeInspectFunc func() (swarm.Node, []byte, error) + nodeListFunc func() ([]swarm.Node, error) + nodeRemoveFunc func() error + nodeUpdateFunc func(nodeID string, version swarm.Version, node swarm.NodeSpec) error + taskInspectFunc func(taskID string) (swarm.Task, []byte, error) + taskListFunc func(options types.TaskListOptions) ([]swarm.Task, error) +} + +func (cli *fakeClient) NodeInspectWithRaw(ctx context.Context, ref string) (swarm.Node, []byte, error) { + if cli.nodeInspectFunc != nil { + return cli.nodeInspectFunc() + } + return swarm.Node{}, []byte{}, nil +} + +func (cli *fakeClient) NodeList(ctx context.Context, options types.NodeListOptions) ([]swarm.Node, error) { + if cli.nodeListFunc != nil { + return cli.nodeListFunc() + } + return []swarm.Node{}, nil +} + +func (cli *fakeClient) NodeRemove(ctx context.Context, nodeID string, options types.NodeRemoveOptions) error { + if cli.nodeRemoveFunc != nil { + return cli.nodeRemoveFunc() + } + return nil +} + +func (cli *fakeClient) NodeUpdate(ctx context.Context, nodeID string, version swarm.Version, node swarm.NodeSpec) error { + if cli.nodeUpdateFunc != nil { + return cli.nodeUpdateFunc(nodeID, version, node) + } + return nil +} + +func (cli *fakeClient) Info(ctx context.Context) (types.Info, error) { + if cli.infoFunc != nil { + return cli.infoFunc() + } + return types.Info{}, nil +} + +func (cli *fakeClient) TaskInspectWithRaw(ctx context.Context, taskID string) (swarm.Task, []byte, error) { + if cli.taskInspectFunc != nil { + return cli.taskInspectFunc(taskID) + } + return swarm.Task{}, []byte{}, nil +} + +func (cli *fakeClient) TaskList(ctx context.Context, options types.TaskListOptions) ([]swarm.Task, error) { + if cli.taskListFunc != nil { + return cli.taskListFunc(options) + } + return []swarm.Task{}, nil +} diff --git a/components/cli/command/node/demote.go b/components/cli/command/node/demote.go index 33f86c6499..72ed3ea630 100644 --- a/components/cli/command/node/demote.go +++ b/components/cli/command/node/demote.go @@ -9,7 +9,7 @@ import ( "github.com/spf13/cobra" ) -func newDemoteCommand(dockerCli *command.DockerCli) *cobra.Command { +func newDemoteCommand(dockerCli command.Cli) *cobra.Command { return &cobra.Command{ Use: "demote NODE [NODE...]", Short: "Demote one or more nodes from manager in the swarm", @@ -20,7 +20,7 @@ func newDemoteCommand(dockerCli *command.DockerCli) *cobra.Command { } } -func runDemote(dockerCli *command.DockerCli, nodes []string) error { +func runDemote(dockerCli command.Cli, nodes []string) error { demote := func(node *swarm.Node) error { if node.Spec.Role == swarm.NodeRoleWorker { fmt.Fprintf(dockerCli.Out(), "Node %s is already a worker.\n", node.ID) diff --git a/components/cli/command/node/demote_test.go b/components/cli/command/node/demote_test.go new file mode 100644 index 0000000000..3ba88f41c8 --- /dev/null +++ b/components/cli/command/node/demote_test.go @@ -0,0 +1,88 @@ +package node + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/internal/test" + // Import builders to get the builder function as package function + . "github.com/docker/docker/cli/internal/test/builders" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestNodeDemoteErrors(t *testing.T) { + testCases := []struct { + args []string + nodeInspectFunc func() (swarm.Node, []byte, error) + nodeUpdateFunc func(nodeID string, version swarm.Version, node swarm.NodeSpec) error + expectedError string + }{ + { + expectedError: "requires at least 1 argument", + }, + { + args: []string{"nodeID"}, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return swarm.Node{}, []byte{}, fmt.Errorf("error inspecting the node") + }, + expectedError: "error inspecting the node", + }, + { + args: []string{"nodeID"}, + nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { + return fmt.Errorf("error updating the node") + }, + expectedError: "error updating the node", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newDemoteCommand( + test.NewFakeCli(&fakeClient{ + nodeInspectFunc: tc.nodeInspectFunc, + nodeUpdateFunc: tc.nodeUpdateFunc, + }, buf)) + cmd.SetArgs(tc.args) + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestNodeDemoteNoChange(t *testing.T) { + buf := new(bytes.Buffer) + cmd := newDemoteCommand( + test.NewFakeCli(&fakeClient{ + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(), []byte{}, nil + }, + nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { + if node.Role != swarm.NodeRoleWorker { + return fmt.Errorf("expected role worker, got %s", node.Role) + } + return nil + }, + }, buf)) + cmd.SetArgs([]string{"nodeID"}) + assert.NilError(t, cmd.Execute()) +} + +func TestNodeDemoteMultipleNode(t *testing.T) { + buf := new(bytes.Buffer) + cmd := newDemoteCommand( + test.NewFakeCli(&fakeClient{ + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(Manager()), []byte{}, nil + }, + nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { + if node.Role != swarm.NodeRoleWorker { + return fmt.Errorf("expected role worker, got %s", node.Role) + } + return nil + }, + }, buf)) + cmd.SetArgs([]string{"nodeID1", "nodeID2"}) + assert.NilError(t, cmd.Execute()) +} diff --git a/components/cli/command/node/inspect.go b/components/cli/command/node/inspect.go index fde70185f8..97a2717781 100644 --- a/components/cli/command/node/inspect.go +++ b/components/cli/command/node/inspect.go @@ -22,7 +22,7 @@ type inspectOptions struct { pretty bool } -func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { +func newInspectCommand(dockerCli command.Cli) *cobra.Command { var opts inspectOptions cmd := &cobra.Command{ @@ -41,7 +41,7 @@ func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { +func runInspect(dockerCli command.Cli, opts inspectOptions) error { client := dockerCli.Client() ctx := context.Background() getRef := func(ref string) (interface{}, []byte, error) { diff --git a/components/cli/command/node/inspect_test.go b/components/cli/command/node/inspect_test.go new file mode 100644 index 0000000000..91bd41e165 --- /dev/null +++ b/components/cli/command/node/inspect_test.go @@ -0,0 +1,122 @@ +package node + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/internal/test" + // Import builders to get the builder function as package function + . "github.com/docker/docker/cli/internal/test/builders" + "github.com/docker/docker/pkg/testutil/assert" + "github.com/docker/docker/pkg/testutil/golden" +) + +func TestNodeInspectErrors(t *testing.T) { + testCases := []struct { + args []string + flags map[string]string + nodeInspectFunc func() (swarm.Node, []byte, error) + infoFunc func() (types.Info, error) + expectedError string + }{ + { + expectedError: "requires at least 1 argument", + }, + { + args: []string{"self"}, + infoFunc: func() (types.Info, error) { + return types.Info{}, fmt.Errorf("error asking for node info") + }, + expectedError: "error asking for node info", + }, + { + args: []string{"nodeID"}, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return swarm.Node{}, []byte{}, fmt.Errorf("error inspecting the node") + }, + infoFunc: func() (types.Info, error) { + return types.Info{}, fmt.Errorf("error asking for node info") + }, + expectedError: "error inspecting the node", + }, + { + args: []string{"self"}, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return swarm.Node{}, []byte{}, fmt.Errorf("error inspecting the node") + }, + infoFunc: func() (types.Info, error) { + return types.Info{}, nil + }, + expectedError: "error inspecting the node", + }, + { + args: []string{"self"}, + flags: map[string]string{ + "pretty": "true", + }, + infoFunc: func() (types.Info, error) { + return types.Info{}, fmt.Errorf("error asking for node info") + }, + expectedError: "error asking for node info", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newInspectCommand( + test.NewFakeCli(&fakeClient{ + nodeInspectFunc: tc.nodeInspectFunc, + infoFunc: tc.infoFunc, + }, buf)) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestNodeInspectPretty(t *testing.T) { + testCases := []struct { + name string + nodeInspectFunc func() (swarm.Node, []byte, error) + }{ + { + name: "simple", + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(NodeLabels(map[string]string{ + "lbl1": "value1", + })), []byte{}, nil + }, + }, + { + name: "manager", + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(Manager()), []byte{}, nil + }, + }, + { + name: "manager-leader", + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(Manager(Leader())), []byte{}, nil + }, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newInspectCommand( + test.NewFakeCli(&fakeClient{ + nodeInspectFunc: tc.nodeInspectFunc, + }, buf)) + cmd.SetArgs([]string{"nodeID"}) + cmd.Flags().Set("pretty", "true") + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), fmt.Sprintf("node-inspect-pretty.%s.golden", tc.name)) + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) + } +} diff --git a/components/cli/command/node/list.go b/components/cli/command/node/list.go index 9cacdcf441..d166401ab7 100644 --- a/components/cli/command/node/list.go +++ b/components/cli/command/node/list.go @@ -24,7 +24,7 @@ type listOptions struct { filter opts.FilterOpt } -func newListCommand(dockerCli *command.DockerCli) *cobra.Command { +func newListCommand(dockerCli command.Cli) *cobra.Command { opts := listOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -43,7 +43,7 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runList(dockerCli *command.DockerCli, opts listOptions) error { +func runList(dockerCli command.Cli, opts listOptions) error { client := dockerCli.Client() out := dockerCli.Out() ctx := context.Background() diff --git a/components/cli/command/node/list_test.go b/components/cli/command/node/list_test.go new file mode 100644 index 0000000000..237c4be9ca --- /dev/null +++ b/components/cli/command/node/list_test.go @@ -0,0 +1,101 @@ +package node + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/internal/test" + // Import builders to get the builder function as package function + . "github.com/docker/docker/cli/internal/test/builders" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestNodeListErrorOnAPIFailure(t *testing.T) { + testCases := []struct { + nodeListFunc func() ([]swarm.Node, error) + infoFunc func() (types.Info, error) + expectedError string + }{ + { + nodeListFunc: func() ([]swarm.Node, error) { + return []swarm.Node{}, fmt.Errorf("error listing nodes") + }, + expectedError: "error listing nodes", + }, + { + nodeListFunc: func() ([]swarm.Node, error) { + return []swarm.Node{ + { + ID: "nodeID", + }, + }, nil + }, + infoFunc: func() (types.Info, error) { + return types.Info{}, fmt.Errorf("error asking for node info") + }, + expectedError: "error asking for node info", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newListCommand( + test.NewFakeCli(&fakeClient{ + nodeListFunc: tc.nodeListFunc, + infoFunc: tc.infoFunc, + }, buf)) + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestNodeList(t *testing.T) { + buf := new(bytes.Buffer) + cmd := newListCommand( + test.NewFakeCli(&fakeClient{ + nodeListFunc: func() ([]swarm.Node, error) { + return []swarm.Node{ + *Node(NodeID("nodeID1"), Hostname("nodeHostname1"), Manager(Leader())), + *Node(NodeID("nodeID2"), Hostname("nodeHostname2"), Manager()), + *Node(NodeID("nodeID3"), Hostname("nodeHostname3")), + }, nil + }, + infoFunc: func() (types.Info, error) { + return types.Info{ + Swarm: swarm.Info{ + NodeID: "nodeID1", + }, + }, nil + }, + }, buf)) + assert.NilError(t, cmd.Execute()) + assert.Contains(t, buf.String(), `nodeID1 * nodeHostname1 Ready Active Leader`) + assert.Contains(t, buf.String(), `nodeID2 nodeHostname2 Ready Active Reachable`) + assert.Contains(t, buf.String(), `nodeID3 nodeHostname3 Ready Active`) +} + +func TestNodeListQuietShouldOnlyPrintIDs(t *testing.T) { + buf := new(bytes.Buffer) + cmd := newListCommand( + test.NewFakeCli(&fakeClient{ + nodeListFunc: func() ([]swarm.Node, error) { + return []swarm.Node{ + *Node(), + }, nil + }, + }, buf)) + cmd.Flags().Set("quiet", "true") + assert.NilError(t, cmd.Execute()) + assert.Contains(t, buf.String(), "nodeID") +} + +// Test case for #24090 +func TestNodeListContainsHostname(t *testing.T) { + buf := new(bytes.Buffer) + cmd := newListCommand(test.NewFakeCli(&fakeClient{}, buf)) + assert.NilError(t, cmd.Execute()) + assert.Contains(t, buf.String(), "HOSTNAME") +} diff --git a/components/cli/command/node/opts.go b/components/cli/command/node/opts.go index 7e6c55d487..0ad365f0c6 100644 --- a/components/cli/command/node/opts.go +++ b/components/cli/command/node/opts.go @@ -1,12 +1,7 @@ package node import ( - "fmt" - "strings" - - "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/opts" - runconfigopts "github.com/docker/docker/runconfig/opts" ) type nodeOptions struct { @@ -27,34 +22,3 @@ func newNodeOptions() *nodeOptions { }, } } - -func (opts *nodeOptions) ToNodeSpec() (swarm.NodeSpec, error) { - var spec swarm.NodeSpec - - spec.Annotations.Name = opts.annotations.name - spec.Annotations.Labels = runconfigopts.ConvertKVStringsToMap(opts.annotations.labels.GetAll()) - - switch swarm.NodeRole(strings.ToLower(opts.role)) { - case swarm.NodeRoleWorker: - spec.Role = swarm.NodeRoleWorker - case swarm.NodeRoleManager: - spec.Role = swarm.NodeRoleManager - case "": - default: - return swarm.NodeSpec{}, fmt.Errorf("invalid role %q, only worker and manager are supported", opts.role) - } - - switch swarm.NodeAvailability(strings.ToLower(opts.availability)) { - case swarm.NodeAvailabilityActive: - spec.Availability = swarm.NodeAvailabilityActive - case swarm.NodeAvailabilityPause: - spec.Availability = swarm.NodeAvailabilityPause - case swarm.NodeAvailabilityDrain: - spec.Availability = swarm.NodeAvailabilityDrain - case "": - default: - return swarm.NodeSpec{}, fmt.Errorf("invalid availability %q, only active, pause and drain are supported", opts.availability) - } - - return spec, nil -} diff --git a/components/cli/command/node/promote.go b/components/cli/command/node/promote.go index f47d783f4c..94fff6400b 100644 --- a/components/cli/command/node/promote.go +++ b/components/cli/command/node/promote.go @@ -9,7 +9,7 @@ import ( "github.com/spf13/cobra" ) -func newPromoteCommand(dockerCli *command.DockerCli) *cobra.Command { +func newPromoteCommand(dockerCli command.Cli) *cobra.Command { return &cobra.Command{ Use: "promote NODE [NODE...]", Short: "Promote one or more nodes to manager in the swarm", @@ -20,7 +20,7 @@ func newPromoteCommand(dockerCli *command.DockerCli) *cobra.Command { } } -func runPromote(dockerCli *command.DockerCli, nodes []string) error { +func runPromote(dockerCli command.Cli, nodes []string) error { promote := func(node *swarm.Node) error { if node.Spec.Role == swarm.NodeRoleManager { fmt.Fprintf(dockerCli.Out(), "Node %s is already a manager.\n", node.ID) diff --git a/components/cli/command/node/promote_test.go b/components/cli/command/node/promote_test.go new file mode 100644 index 0000000000..ef4666321d --- /dev/null +++ b/components/cli/command/node/promote_test.go @@ -0,0 +1,88 @@ +package node + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/internal/test" + // Import builders to get the builder function as package function + . "github.com/docker/docker/cli/internal/test/builders" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestNodePromoteErrors(t *testing.T) { + testCases := []struct { + args []string + nodeInspectFunc func() (swarm.Node, []byte, error) + nodeUpdateFunc func(nodeID string, version swarm.Version, node swarm.NodeSpec) error + expectedError string + }{ + { + expectedError: "requires at least 1 argument", + }, + { + args: []string{"nodeID"}, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return swarm.Node{}, []byte{}, fmt.Errorf("error inspecting the node") + }, + expectedError: "error inspecting the node", + }, + { + args: []string{"nodeID"}, + nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { + return fmt.Errorf("error updating the node") + }, + expectedError: "error updating the node", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newPromoteCommand( + test.NewFakeCli(&fakeClient{ + nodeInspectFunc: tc.nodeInspectFunc, + nodeUpdateFunc: tc.nodeUpdateFunc, + }, buf)) + cmd.SetArgs(tc.args) + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestNodePromoteNoChange(t *testing.T) { + buf := new(bytes.Buffer) + cmd := newPromoteCommand( + test.NewFakeCli(&fakeClient{ + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(Manager()), []byte{}, nil + }, + nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { + if node.Role != swarm.NodeRoleManager { + return fmt.Errorf("expected role manager, got %s", node.Role) + } + return nil + }, + }, buf)) + cmd.SetArgs([]string{"nodeID"}) + assert.NilError(t, cmd.Execute()) +} + +func TestNodePromoteMultipleNode(t *testing.T) { + buf := new(bytes.Buffer) + cmd := newPromoteCommand( + test.NewFakeCli(&fakeClient{ + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(), []byte{}, nil + }, + nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { + if node.Role != swarm.NodeRoleManager { + return fmt.Errorf("expected role manager, got %s", node.Role) + } + return nil + }, + }, buf)) + cmd.SetArgs([]string{"nodeID1", "nodeID2"}) + assert.NilError(t, cmd.Execute()) +} diff --git a/components/cli/command/node/ps.go b/components/cli/command/node/ps.go index a034721d24..52ac36646e 100644 --- a/components/cli/command/node/ps.go +++ b/components/cli/command/node/ps.go @@ -22,7 +22,7 @@ type psOptions struct { filter opts.FilterOpt } -func newPsCommand(dockerCli *command.DockerCli) *cobra.Command { +func newPsCommand(dockerCli command.Cli) *cobra.Command { opts := psOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -47,7 +47,7 @@ func newPsCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runPs(dockerCli *command.DockerCli, opts psOptions) error { +func runPs(dockerCli command.Cli, opts psOptions) error { client := dockerCli.Client() ctx := context.Background() diff --git a/components/cli/command/node/ps_test.go b/components/cli/command/node/ps_test.go new file mode 100644 index 0000000000..1a1022d213 --- /dev/null +++ b/components/cli/command/node/ps_test.go @@ -0,0 +1,132 @@ +package node + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/internal/test" + // Import builders to get the builder function as package function + . "github.com/docker/docker/cli/internal/test/builders" + "github.com/docker/docker/pkg/testutil/assert" + "github.com/docker/docker/pkg/testutil/golden" +) + +func TestNodePsErrors(t *testing.T) { + testCases := []struct { + args []string + flags map[string]string + infoFunc func() (types.Info, error) + nodeInspectFunc func() (swarm.Node, []byte, error) + taskListFunc func(options types.TaskListOptions) ([]swarm.Task, error) + taskInspectFunc func(taskID string) (swarm.Task, []byte, error) + expectedError string + }{ + { + infoFunc: func() (types.Info, error) { + return types.Info{}, fmt.Errorf("error asking for node info") + }, + expectedError: "error asking for node info", + }, + { + args: []string{"nodeID"}, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return swarm.Node{}, []byte{}, fmt.Errorf("error inspecting the node") + }, + expectedError: "error inspecting the node", + }, + { + args: []string{"nodeID"}, + taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) { + return []swarm.Task{}, fmt.Errorf("error returning the task list") + }, + expectedError: "error returning the task list", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newPsCommand( + test.NewFakeCli(&fakeClient{ + infoFunc: tc.infoFunc, + nodeInspectFunc: tc.nodeInspectFunc, + taskInspectFunc: tc.taskInspectFunc, + taskListFunc: tc.taskListFunc, + }, buf)) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestNodePs(t *testing.T) { + testCases := []struct { + name string + args []string + flags map[string]string + infoFunc func() (types.Info, error) + nodeInspectFunc func() (swarm.Node, []byte, error) + taskListFunc func(options types.TaskListOptions) ([]swarm.Task, error) + taskInspectFunc func(taskID string) (swarm.Task, []byte, error) + }{ + { + name: "simple", + args: []string{"nodeID"}, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(), []byte{}, nil + }, + taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) { + return []swarm.Task{ + *Task(WithStatus(Timestamp(time.Now().Add(-2*time.Hour)), PortStatus([]swarm.PortConfig{ + { + TargetPort: 80, + PublishedPort: 80, + Protocol: "tcp", + }, + }))), + }, nil + }, + }, + { + name: "with-errors", + args: []string{"nodeID"}, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(), []byte{}, nil + }, + taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) { + return []swarm.Task{ + *Task(TaskID("taskID1"), ServiceID("failure"), + WithStatus(Timestamp(time.Now().Add(-2*time.Hour)), StatusErr("a task error"))), + *Task(TaskID("taskID2"), ServiceID("failure"), + WithStatus(Timestamp(time.Now().Add(-3*time.Hour)), StatusErr("a task error"))), + *Task(TaskID("taskID3"), ServiceID("failure"), + WithStatus(Timestamp(time.Now().Add(-4*time.Hour)), StatusErr("a task error"))), + }, nil + }, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newPsCommand( + test.NewFakeCli(&fakeClient{ + infoFunc: tc.infoFunc, + nodeInspectFunc: tc.nodeInspectFunc, + taskInspectFunc: tc.taskInspectFunc, + taskListFunc: tc.taskListFunc, + }, buf)) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), fmt.Sprintf("node-ps.%s.golden", tc.name)) + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) + } +} diff --git a/components/cli/command/node/remove.go b/components/cli/command/node/remove.go index 19b4a96631..0e4963aca4 100644 --- a/components/cli/command/node/remove.go +++ b/components/cli/command/node/remove.go @@ -16,7 +16,7 @@ type removeOptions struct { force bool } -func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { +func newRemoveCommand(dockerCli command.Cli) *cobra.Command { opts := removeOptions{} cmd := &cobra.Command{ @@ -33,7 +33,7 @@ func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runRemove(dockerCli *command.DockerCli, args []string, opts removeOptions) error { +func runRemove(dockerCli command.Cli, args []string, opts removeOptions) error { client := dockerCli.Client() ctx := context.Background() diff --git a/components/cli/command/node/remove_test.go b/components/cli/command/node/remove_test.go new file mode 100644 index 0000000000..54930a276c --- /dev/null +++ b/components/cli/command/node/remove_test.go @@ -0,0 +1,47 @@ +package node + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + + "github.com/docker/docker/cli/internal/test" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestNodeRemoveErrors(t *testing.T) { + testCases := []struct { + args []string + nodeRemoveFunc func() error + expectedError string + }{ + { + expectedError: "requires at least 1 argument", + }, + { + args: []string{"nodeID"}, + nodeRemoveFunc: func() error { + return fmt.Errorf("error removing the node") + }, + expectedError: "error removing the node", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newRemoveCommand( + test.NewFakeCli(&fakeClient{ + nodeRemoveFunc: tc.nodeRemoveFunc, + }, buf)) + cmd.SetArgs(tc.args) + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestNodeRemoveMultiple(t *testing.T) { + buf := new(bytes.Buffer) + cmd := newRemoveCommand(test.NewFakeCli(&fakeClient{}, buf)) + cmd.SetArgs([]string{"nodeID1", "nodeID2"}) + assert.NilError(t, cmd.Execute()) +} diff --git a/components/cli/command/node/testdata/node-inspect-pretty.manager-leader.golden b/components/cli/command/node/testdata/node-inspect-pretty.manager-leader.golden new file mode 100644 index 0000000000..461fc46ea2 --- /dev/null +++ b/components/cli/command/node/testdata/node-inspect-pretty.manager-leader.golden @@ -0,0 +1,25 @@ +ID: nodeID +Name: defaultNodeName +Hostname: defaultNodeHostname +Joined at: 2009-11-10 23:00:00 +0000 utc +Status: + State: Ready + Availability: Active + Address: 127.0.0.1 +Manager Status: + Address: 127.0.0.1 + Raft Status: Reachable + Leader: Yes +Platform: + Operating System: linux + Architecture: x86_64 +Resources: + CPUs: 0 + Memory: 20 MiB +Plugins: + Network: bridge, overlay + Volume: local +Engine Version: 1.13.0 +Engine Labels: + - engine = label + diff --git a/components/cli/command/node/testdata/node-inspect-pretty.manager.golden b/components/cli/command/node/testdata/node-inspect-pretty.manager.golden new file mode 100644 index 0000000000..2c660188d5 --- /dev/null +++ b/components/cli/command/node/testdata/node-inspect-pretty.manager.golden @@ -0,0 +1,25 @@ +ID: nodeID +Name: defaultNodeName +Hostname: defaultNodeHostname +Joined at: 2009-11-10 23:00:00 +0000 utc +Status: + State: Ready + Availability: Active + Address: 127.0.0.1 +Manager Status: + Address: 127.0.0.1 + Raft Status: Reachable + Leader: No +Platform: + Operating System: linux + Architecture: x86_64 +Resources: + CPUs: 0 + Memory: 20 MiB +Plugins: + Network: bridge, overlay + Volume: local +Engine Version: 1.13.0 +Engine Labels: + - engine = label + diff --git a/components/cli/command/node/testdata/node-inspect-pretty.simple.golden b/components/cli/command/node/testdata/node-inspect-pretty.simple.golden new file mode 100644 index 0000000000..e63bc12596 --- /dev/null +++ b/components/cli/command/node/testdata/node-inspect-pretty.simple.golden @@ -0,0 +1,23 @@ +ID: nodeID +Name: defaultNodeName +Labels: + - lbl1 = value1 +Hostname: defaultNodeHostname +Joined at: 2009-11-10 23:00:00 +0000 utc +Status: + State: Ready + Availability: Active + Address: 127.0.0.1 +Platform: + Operating System: linux + Architecture: x86_64 +Resources: + CPUs: 0 + Memory: 20 MiB +Plugins: + Network: bridge, overlay + Volume: local +Engine Version: 1.13.0 +Engine Labels: + - engine = label + diff --git a/components/cli/command/node/testdata/node-ps.simple.golden b/components/cli/command/node/testdata/node-ps.simple.golden new file mode 100644 index 0000000000..f9555d8792 --- /dev/null +++ b/components/cli/command/node/testdata/node-ps.simple.golden @@ -0,0 +1,2 @@ +ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS +taskID rl02d5gwz6chzu7il5fhtb8be.1 myimage:mytag defaultNodeName Ready Ready 2 hours ago *:80->80/tcp diff --git a/components/cli/command/node/testdata/node-ps.with-errors.golden b/components/cli/command/node/testdata/node-ps.with-errors.golden new file mode 100644 index 0000000000..273b30fa11 --- /dev/null +++ b/components/cli/command/node/testdata/node-ps.with-errors.golden @@ -0,0 +1,4 @@ +ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS +taskID1 failure.1 myimage:mytag defaultNodeName Ready Ready 2 hours ago "a task error" +taskID2 \_ failure.1 myimage:mytag defaultNodeName Ready Ready 3 hours ago "a task error" +taskID3 \_ failure.1 myimage:mytag defaultNodeName Ready Ready 4 hours ago "a task error" diff --git a/components/cli/command/node/update.go b/components/cli/command/node/update.go index 65339e138b..6ca2a7c1e3 100644 --- a/components/cli/command/node/update.go +++ b/components/cli/command/node/update.go @@ -18,7 +18,7 @@ var ( errNoRoleChange = errors.New("role was already set to the requested value") ) -func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { +func newUpdateCommand(dockerCli command.Cli) *cobra.Command { nodeOpts := newNodeOptions() cmd := &cobra.Command{ @@ -39,14 +39,14 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, nodeID string) error { +func runUpdate(dockerCli command.Cli, flags *pflag.FlagSet, nodeID string) error { success := func(_ string) { fmt.Fprintln(dockerCli.Out(), nodeID) } return updateNodes(dockerCli, []string{nodeID}, mergeNodeUpdate(flags), success) } -func updateNodes(dockerCli *command.DockerCli, nodes []string, mergeNode func(node *swarm.Node) error, success func(nodeID string)) error { +func updateNodes(dockerCli command.Cli, nodes []string, mergeNode func(node *swarm.Node) error, success func(nodeID string)) error { client := dockerCli.Client() ctx := context.Background() diff --git a/components/cli/command/node/update_test.go b/components/cli/command/node/update_test.go new file mode 100644 index 0000000000..439ba94436 --- /dev/null +++ b/components/cli/command/node/update_test.go @@ -0,0 +1,172 @@ +package node + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/internal/test" + // Import builders to get the builder function as package function + . "github.com/docker/docker/cli/internal/test/builders" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestNodeUpdateErrors(t *testing.T) { + testCases := []struct { + args []string + flags map[string]string + nodeInspectFunc func() (swarm.Node, []byte, error) + nodeUpdateFunc func(nodeID string, version swarm.Version, node swarm.NodeSpec) error + expectedError string + }{ + { + expectedError: "requires exactly 1 argument", + }, + { + args: []string{"node1", "node2"}, + expectedError: "requires exactly 1 argument", + }, + { + args: []string{"nodeID"}, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return swarm.Node{}, []byte{}, fmt.Errorf("error inspecting the node") + }, + expectedError: "error inspecting the node", + }, + { + args: []string{"nodeID"}, + nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { + return fmt.Errorf("error updating the node") + }, + expectedError: "error updating the node", + }, + { + args: []string{"nodeID"}, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(NodeLabels(map[string]string{ + "key": "value", + })), []byte{}, nil + }, + flags: map[string]string{ + "label-rm": "notpresent", + }, + expectedError: "key notpresent doesn't exist in node's labels", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newUpdateCommand( + test.NewFakeCli(&fakeClient{ + nodeInspectFunc: tc.nodeInspectFunc, + nodeUpdateFunc: tc.nodeUpdateFunc, + }, buf)) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestNodeUpdate(t *testing.T) { + testCases := []struct { + args []string + flags map[string]string + nodeInspectFunc func() (swarm.Node, []byte, error) + nodeUpdateFunc func(nodeID string, version swarm.Version, node swarm.NodeSpec) error + }{ + { + args: []string{"nodeID"}, + flags: map[string]string{ + "role": "manager", + }, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(), []byte{}, nil + }, + nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { + if node.Role != swarm.NodeRoleManager { + return fmt.Errorf("expected role manager, got %s", node.Role) + } + return nil + }, + }, + { + args: []string{"nodeID"}, + flags: map[string]string{ + "availability": "drain", + }, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(), []byte{}, nil + }, + nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { + if node.Availability != swarm.NodeAvailabilityDrain { + return fmt.Errorf("expected drain availability, got %s", node.Availability) + } + return nil + }, + }, + { + args: []string{"nodeID"}, + flags: map[string]string{ + "label-add": "lbl", + }, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(), []byte{}, nil + }, + nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { + if _, present := node.Annotations.Labels["lbl"]; !present { + return fmt.Errorf("expected 'lbl' label, got %v", node.Annotations.Labels) + } + return nil + }, + }, + { + args: []string{"nodeID"}, + flags: map[string]string{ + "label-add": "key=value", + }, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(), []byte{}, nil + }, + nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { + if value, present := node.Annotations.Labels["key"]; !present || value != "value" { + return fmt.Errorf("expected 'key' label to be 'value', got %v", node.Annotations.Labels) + } + return nil + }, + }, + { + args: []string{"nodeID"}, + flags: map[string]string{ + "label-rm": "key", + }, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(NodeLabels(map[string]string{ + "key": "value", + })), []byte{}, nil + }, + nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { + if len(node.Annotations.Labels) > 0 { + return fmt.Errorf("expected no labels, got %v", node.Annotations.Labels) + } + return nil + }, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newUpdateCommand( + test.NewFakeCli(&fakeClient{ + nodeInspectFunc: tc.nodeInspectFunc, + nodeUpdateFunc: tc.nodeUpdateFunc, + }, buf)) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + assert.NilError(t, cmd.Execute()) + } +} diff --git a/components/cli/command/swarm/client_test.go b/components/cli/command/swarm/client_test.go new file mode 100644 index 0000000000..1d42b9499c --- /dev/null +++ b/components/cli/command/swarm/client_test.go @@ -0,0 +1,84 @@ +package swarm + +import ( + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/client" + "golang.org/x/net/context" +) + +type fakeClient struct { + client.Client + infoFunc func() (types.Info, error) + swarmInitFunc func() (string, error) + swarmInspectFunc func() (swarm.Swarm, error) + nodeInspectFunc func() (swarm.Node, []byte, error) + swarmGetUnlockKeyFunc func() (types.SwarmUnlockKeyResponse, error) + swarmJoinFunc func() error + swarmLeaveFunc func() error + swarmUpdateFunc func(swarm swarm.Spec, flags swarm.UpdateFlags) error + swarmUnlockFunc func(req swarm.UnlockRequest) error +} + +func (cli *fakeClient) Info(ctx context.Context) (types.Info, error) { + if cli.infoFunc != nil { + return cli.infoFunc() + } + return types.Info{}, nil +} + +func (cli *fakeClient) NodeInspectWithRaw(ctx context.Context, ref string) (swarm.Node, []byte, error) { + if cli.nodeInspectFunc != nil { + return cli.nodeInspectFunc() + } + return swarm.Node{}, []byte{}, nil +} + +func (cli *fakeClient) SwarmInit(ctx context.Context, req swarm.InitRequest) (string, error) { + if cli.swarmInitFunc != nil { + return cli.swarmInitFunc() + } + return "", nil +} + +func (cli *fakeClient) SwarmInspect(ctx context.Context) (swarm.Swarm, error) { + if cli.swarmInspectFunc != nil { + return cli.swarmInspectFunc() + } + return swarm.Swarm{}, nil +} + +func (cli *fakeClient) SwarmGetUnlockKey(ctx context.Context) (types.SwarmUnlockKeyResponse, error) { + if cli.swarmGetUnlockKeyFunc != nil { + return cli.swarmGetUnlockKeyFunc() + } + return types.SwarmUnlockKeyResponse{}, nil +} + +func (cli *fakeClient) SwarmJoin(ctx context.Context, req swarm.JoinRequest) error { + if cli.swarmJoinFunc != nil { + return cli.swarmJoinFunc() + } + return nil +} + +func (cli *fakeClient) SwarmLeave(ctx context.Context, force bool) error { + if cli.swarmLeaveFunc != nil { + return cli.swarmLeaveFunc() + } + return nil +} + +func (cli *fakeClient) SwarmUpdate(ctx context.Context, version swarm.Version, swarm swarm.Spec, flags swarm.UpdateFlags) error { + if cli.swarmUpdateFunc != nil { + return cli.swarmUpdateFunc(swarm, flags) + } + return nil +} + +func (cli *fakeClient) SwarmUnlock(ctx context.Context, req swarm.UnlockRequest) error { + if cli.swarmUnlockFunc != nil { + return cli.swarmUnlockFunc(req) + } + return nil +} diff --git a/components/cli/command/swarm/init.go b/components/cli/command/swarm/init.go index 2550feeb47..e038ac62a5 100644 --- a/components/cli/command/swarm/init.go +++ b/components/cli/command/swarm/init.go @@ -22,7 +22,7 @@ type initOptions struct { forceNewCluster bool } -func newInitCommand(dockerCli *command.DockerCli) *cobra.Command { +func newInitCommand(dockerCli command.Cli) *cobra.Command { opts := initOptions{ listenAddr: NewListenAddrOption(), } @@ -45,7 +45,7 @@ func newInitCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runInit(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts initOptions) error { +func runInit(dockerCli command.Cli, flags *pflag.FlagSet, opts initOptions) error { client := dockerCli.Client() ctx := context.Background() @@ -67,7 +67,7 @@ func runInit(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts initOption fmt.Fprintf(dockerCli.Out(), "Swarm initialized: current node (%s) is now a manager.\n\n", nodeID) - if err := printJoinCommand(ctx, dockerCli, nodeID, true, false); err != nil { + if err := printJoinCommand(ctx, dockerCli, nodeID, false, true); err != nil { return err } diff --git a/components/cli/command/swarm/init_test.go b/components/cli/command/swarm/init_test.go new file mode 100644 index 0000000000..13de1cd550 --- /dev/null +++ b/components/cli/command/swarm/init_test.go @@ -0,0 +1,129 @@ +package swarm + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/internal/test" + "github.com/docker/docker/pkg/testutil/assert" + "github.com/docker/docker/pkg/testutil/golden" +) + +func TestSwarmInitErrorOnAPIFailure(t *testing.T) { + testCases := []struct { + name string + flags map[string]string + swarmInitFunc func() (string, error) + swarmInspectFunc func() (swarm.Swarm, error) + swarmGetUnlockKeyFunc func() (types.SwarmUnlockKeyResponse, error) + nodeInspectFunc func() (swarm.Node, []byte, error) + expectedError string + }{ + { + name: "init-failed", + swarmInitFunc: func() (string, error) { + return "", fmt.Errorf("error initializing the swarm") + }, + expectedError: "error initializing the swarm", + }, + { + name: "init-faild-with-ip-choice", + swarmInitFunc: func() (string, error) { + return "", fmt.Errorf("could not choose an IP address to advertise") + }, + expectedError: "could not choose an IP address to advertise - specify one with --advertise-addr", + }, + { + name: "swarm-inspect-after-init-failed", + swarmInspectFunc: func() (swarm.Swarm, error) { + return swarm.Swarm{}, fmt.Errorf("error inspecting the swarm") + }, + expectedError: "error inspecting the swarm", + }, + { + name: "node-inspect-after-init-failed", + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return swarm.Node{}, []byte{}, fmt.Errorf("error inspecting the node") + }, + expectedError: "error inspecting the node", + }, + { + name: "swarm-get-unlock-key-after-init-failed", + flags: map[string]string{ + flagAutolock: "true", + }, + swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { + return types.SwarmUnlockKeyResponse{}, fmt.Errorf("error getting swarm unlock key") + }, + expectedError: "could not fetch unlock key: error getting swarm unlock key", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newInitCommand( + test.NewFakeCli(&fakeClient{ + swarmInitFunc: tc.swarmInitFunc, + swarmInspectFunc: tc.swarmInspectFunc, + swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, + nodeInspectFunc: tc.nodeInspectFunc, + }, buf)) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestSwarmInit(t *testing.T) { + testCases := []struct { + name string + flags map[string]string + swarmInitFunc func() (string, error) + swarmInspectFunc func() (swarm.Swarm, error) + swarmGetUnlockKeyFunc func() (types.SwarmUnlockKeyResponse, error) + nodeInspectFunc func() (swarm.Node, []byte, error) + }{ + { + name: "init", + swarmInitFunc: func() (string, error) { + return "nodeID", nil + }, + }, + { + name: "init-autolock", + flags: map[string]string{ + flagAutolock: "true", + }, + swarmInitFunc: func() (string, error) { + return "nodeID", nil + }, + swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { + return types.SwarmUnlockKeyResponse{ + UnlockKey: "unlock-key", + }, nil + }, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newInitCommand( + test.NewFakeCli(&fakeClient{ + swarmInitFunc: tc.swarmInitFunc, + swarmInspectFunc: tc.swarmInspectFunc, + swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, + nodeInspectFunc: tc.nodeInspectFunc, + }, buf)) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), fmt.Sprintf("init-%s.golden", tc.name)) + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) + } +} diff --git a/components/cli/command/swarm/join.go b/components/cli/command/swarm/join.go index 004313b4c6..3ea1462df4 100644 --- a/components/cli/command/swarm/join.go +++ b/components/cli/command/swarm/join.go @@ -18,7 +18,7 @@ type joinOptions struct { token string } -func newJoinCommand(dockerCli *command.DockerCli) *cobra.Command { +func newJoinCommand(dockerCli command.Cli) *cobra.Command { opts := joinOptions{ listenAddr: NewListenAddrOption(), } @@ -40,7 +40,7 @@ func newJoinCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runJoin(dockerCli *command.DockerCli, opts joinOptions) error { +func runJoin(dockerCli command.Cli, opts joinOptions) error { client := dockerCli.Client() ctx := context.Background() diff --git a/components/cli/command/swarm/join_test.go b/components/cli/command/swarm/join_test.go new file mode 100644 index 0000000000..66dd6d66b6 --- /dev/null +++ b/components/cli/command/swarm/join_test.go @@ -0,0 +1,102 @@ +package swarm + +import ( + "bytes" + "fmt" + "io/ioutil" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/internal/test" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestSwarmJoinErrors(t *testing.T) { + testCases := []struct { + name string + args []string + swarmJoinFunc func() error + infoFunc func() (types.Info, error) + expectedError string + }{ + { + name: "not-enough-args", + expectedError: "requires exactly 1 argument", + }, + { + name: "too-many-args", + args: []string{"remote1", "remote2"}, + expectedError: "requires exactly 1 argument", + }, + { + name: "join-failed", + args: []string{"remote"}, + swarmJoinFunc: func() error { + return fmt.Errorf("error joining the swarm") + }, + expectedError: "error joining the swarm", + }, + { + name: "join-failed-on-init", + args: []string{"remote"}, + infoFunc: func() (types.Info, error) { + return types.Info{}, fmt.Errorf("error asking for node info") + }, + expectedError: "error asking for node info", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newJoinCommand( + test.NewFakeCli(&fakeClient{ + swarmJoinFunc: tc.swarmJoinFunc, + infoFunc: tc.infoFunc, + }, buf)) + cmd.SetArgs(tc.args) + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestSwarmJoin(t *testing.T) { + testCases := []struct { + name string + infoFunc func() (types.Info, error) + expected string + }{ + { + name: "join-as-manager", + infoFunc: func() (types.Info, error) { + return types.Info{ + Swarm: swarm.Info{ + ControlAvailable: true, + }, + }, nil + }, + expected: "This node joined a swarm as a manager.", + }, + { + name: "join-as-worker", + infoFunc: func() (types.Info, error) { + return types.Info{ + Swarm: swarm.Info{ + ControlAvailable: false, + }, + }, nil + }, + expected: "This node joined a swarm as a worker.", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newJoinCommand( + test.NewFakeCli(&fakeClient{ + infoFunc: tc.infoFunc, + }, buf)) + cmd.SetArgs([]string{"remote"}) + assert.NilError(t, cmd.Execute()) + assert.Equal(t, strings.TrimSpace(buf.String()), tc.expected) + } +} diff --git a/components/cli/command/swarm/join_token.go b/components/cli/command/swarm/join_token.go index d800b769ba..5c84c7a310 100644 --- a/components/cli/command/swarm/join_token.go +++ b/components/cli/command/swarm/join_token.go @@ -18,7 +18,7 @@ type joinTokenOptions struct { quiet bool } -func newJoinTokenCommand(dockerCli *command.DockerCli) *cobra.Command { +func newJoinTokenCommand(dockerCli command.Cli) *cobra.Command { opts := joinTokenOptions{} cmd := &cobra.Command{ @@ -38,7 +38,7 @@ func newJoinTokenCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runJoinToken(dockerCli *command.DockerCli, opts joinTokenOptions) error { +func runJoinToken(dockerCli command.Cli, opts joinTokenOptions) error { worker := opts.role == "worker" manager := opts.role == "manager" @@ -94,7 +94,7 @@ func runJoinToken(dockerCli *command.DockerCli, opts joinTokenOptions) error { return printJoinCommand(ctx, dockerCli, info.Swarm.NodeID, worker, manager) } -func printJoinCommand(ctx context.Context, dockerCli *command.DockerCli, nodeID string, worker bool, manager bool) error { +func printJoinCommand(ctx context.Context, dockerCli command.Cli, nodeID string, worker bool, manager bool) error { client := dockerCli.Client() node, _, err := client.NodeInspectWithRaw(ctx, nodeID) diff --git a/components/cli/command/swarm/join_token_test.go b/components/cli/command/swarm/join_token_test.go new file mode 100644 index 0000000000..6244016419 --- /dev/null +++ b/components/cli/command/swarm/join_token_test.go @@ -0,0 +1,215 @@ +package swarm + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/internal/test" + // Import builders to get the builder function as package function + . "github.com/docker/docker/cli/internal/test/builders" + "github.com/docker/docker/pkg/testutil/assert" + "github.com/docker/docker/pkg/testutil/golden" +) + +func TestSwarmJoinTokenErrors(t *testing.T) { + testCases := []struct { + name string + args []string + flags map[string]string + infoFunc func() (types.Info, error) + swarmInspectFunc func() (swarm.Swarm, error) + swarmUpdateFunc func(swarm swarm.Spec, flags swarm.UpdateFlags) error + nodeInspectFunc func() (swarm.Node, []byte, error) + expectedError string + }{ + { + name: "not-enough-args", + expectedError: "requires exactly 1 argument", + }, + { + name: "too-many-args", + args: []string{"worker", "manager"}, + expectedError: "requires exactly 1 argument", + }, + { + name: "invalid-args", + args: []string{"foo"}, + expectedError: "unknown role foo", + }, + { + name: "swarm-inspect-failed", + args: []string{"worker"}, + swarmInspectFunc: func() (swarm.Swarm, error) { + return swarm.Swarm{}, fmt.Errorf("error inspecting the swarm") + }, + expectedError: "error inspecting the swarm", + }, + { + name: "swarm-inspect-rotate-failed", + args: []string{"worker"}, + flags: map[string]string{ + flagRotate: "true", + }, + swarmInspectFunc: func() (swarm.Swarm, error) { + return swarm.Swarm{}, fmt.Errorf("error inspecting the swarm") + }, + expectedError: "error inspecting the swarm", + }, + { + name: "swarm-update-failed", + args: []string{"worker"}, + flags: map[string]string{ + flagRotate: "true", + }, + swarmUpdateFunc: func(swarm swarm.Spec, flags swarm.UpdateFlags) error { + return fmt.Errorf("error updating the swarm") + }, + expectedError: "error updating the swarm", + }, + { + name: "node-inspect-failed", + args: []string{"worker"}, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return swarm.Node{}, []byte{}, fmt.Errorf("error inspecting node") + }, + expectedError: "error inspecting node", + }, + { + name: "info-failed", + args: []string{"worker"}, + infoFunc: func() (types.Info, error) { + return types.Info{}, fmt.Errorf("error asking for node info") + }, + expectedError: "error asking for node info", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newJoinTokenCommand( + test.NewFakeCli(&fakeClient{ + swarmInspectFunc: tc.swarmInspectFunc, + swarmUpdateFunc: tc.swarmUpdateFunc, + infoFunc: tc.infoFunc, + nodeInspectFunc: tc.nodeInspectFunc, + }, buf)) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestSwarmJoinToken(t *testing.T) { + testCases := []struct { + name string + args []string + flags map[string]string + infoFunc func() (types.Info, error) + swarmInspectFunc func() (swarm.Swarm, error) + nodeInspectFunc func() (swarm.Node, []byte, error) + }{ + { + name: "worker", + args: []string{"worker"}, + infoFunc: func() (types.Info, error) { + return types.Info{ + Swarm: swarm.Info{ + NodeID: "nodeID", + }, + }, nil + }, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(Manager()), []byte{}, nil + }, + swarmInspectFunc: func() (swarm.Swarm, error) { + return *Swarm(), nil + }, + }, + { + name: "manager", + args: []string{"manager"}, + infoFunc: func() (types.Info, error) { + return types.Info{ + Swarm: swarm.Info{ + NodeID: "nodeID", + }, + }, nil + }, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(Manager()), []byte{}, nil + }, + swarmInspectFunc: func() (swarm.Swarm, error) { + return *Swarm(), nil + }, + }, + { + name: "manager-rotate", + args: []string{"manager"}, + flags: map[string]string{ + flagRotate: "true", + }, + infoFunc: func() (types.Info, error) { + return types.Info{ + Swarm: swarm.Info{ + NodeID: "nodeID", + }, + }, nil + }, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(Manager()), []byte{}, nil + }, + swarmInspectFunc: func() (swarm.Swarm, error) { + return *Swarm(), nil + }, + }, + { + name: "worker-quiet", + args: []string{"worker"}, + flags: map[string]string{ + flagQuiet: "true", + }, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(Manager()), []byte{}, nil + }, + swarmInspectFunc: func() (swarm.Swarm, error) { + return *Swarm(), nil + }, + }, + { + name: "manager-quiet", + args: []string{"manager"}, + flags: map[string]string{ + flagQuiet: "true", + }, + nodeInspectFunc: func() (swarm.Node, []byte, error) { + return *Node(Manager()), []byte{}, nil + }, + swarmInspectFunc: func() (swarm.Swarm, error) { + return *Swarm(), nil + }, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newJoinTokenCommand( + test.NewFakeCli(&fakeClient{ + swarmInspectFunc: tc.swarmInspectFunc, + infoFunc: tc.infoFunc, + nodeInspectFunc: tc.nodeInspectFunc, + }, buf)) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), fmt.Sprintf("jointoken-%s.golden", tc.name)) + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) + } +} diff --git a/components/cli/command/swarm/leave.go b/components/cli/command/swarm/leave.go index e2cfa0a045..128ed46d8a 100644 --- a/components/cli/command/swarm/leave.go +++ b/components/cli/command/swarm/leave.go @@ -14,7 +14,7 @@ type leaveOptions struct { force bool } -func newLeaveCommand(dockerCli *command.DockerCli) *cobra.Command { +func newLeaveCommand(dockerCli command.Cli) *cobra.Command { opts := leaveOptions{} cmd := &cobra.Command{ @@ -31,7 +31,7 @@ func newLeaveCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runLeave(dockerCli *command.DockerCli, opts leaveOptions) error { +func runLeave(dockerCli command.Cli, opts leaveOptions) error { client := dockerCli.Client() ctx := context.Background() diff --git a/components/cli/command/swarm/leave_test.go b/components/cli/command/swarm/leave_test.go new file mode 100644 index 0000000000..09b41b2511 --- /dev/null +++ b/components/cli/command/swarm/leave_test.go @@ -0,0 +1,52 @@ +package swarm + +import ( + "bytes" + "fmt" + "io/ioutil" + "strings" + "testing" + + "github.com/docker/docker/cli/internal/test" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestSwarmLeaveErrors(t *testing.T) { + testCases := []struct { + name string + args []string + swarmLeaveFunc func() error + expectedError string + }{ + { + name: "too-many-args", + args: []string{"foo"}, + expectedError: "accepts no argument(s)", + }, + { + name: "leave-failed", + swarmLeaveFunc: func() error { + return fmt.Errorf("error leaving the swarm") + }, + expectedError: "error leaving the swarm", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newLeaveCommand( + test.NewFakeCli(&fakeClient{ + swarmLeaveFunc: tc.swarmLeaveFunc, + }, buf)) + cmd.SetArgs(tc.args) + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestSwarmLeave(t *testing.T) { + buf := new(bytes.Buffer) + cmd := newLeaveCommand( + test.NewFakeCli(&fakeClient{}, buf)) + assert.NilError(t, cmd.Execute()) + assert.Equal(t, strings.TrimSpace(buf.String()), "Node left the swarm.") +} diff --git a/components/cli/command/swarm/opts_test.go b/components/cli/command/swarm/opts_test.go index 568dc87302..9a97e8bd2c 100644 --- a/components/cli/command/swarm/opts_test.go +++ b/components/cli/command/swarm/opts_test.go @@ -35,3 +35,76 @@ func TestNodeAddrOptionSetInvalidFormat(t *testing.T) { opt := NewListenAddrOption() assert.Error(t, opt.Set("http://localhost:4545"), "Invalid") } + +func TestExternalCAOptionErrors(t *testing.T) { + testCases := []struct { + externalCA string + expectedError string + }{ + { + externalCA: "", + expectedError: "EOF", + }, + { + externalCA: "anything", + expectedError: "invalid field 'anything' must be a key=value pair", + }, + { + externalCA: "foo=bar", + expectedError: "the external-ca option needs a protocol= parameter", + }, + { + externalCA: "protocol=baz", + expectedError: "unrecognized external CA protocol baz", + }, + { + externalCA: "protocol=cfssl", + expectedError: "the external-ca option needs a url= parameter", + }, + } + for _, tc := range testCases { + opt := &ExternalCAOption{} + assert.Error(t, opt.Set(tc.externalCA), tc.expectedError) + } +} + +func TestExternalCAOption(t *testing.T) { + testCases := []struct { + externalCA string + expected string + }{ + { + externalCA: "protocol=cfssl,url=anything", + expected: "cfssl: anything", + }, + { + externalCA: "protocol=CFSSL,url=anything", + expected: "cfssl: anything", + }, + { + externalCA: "protocol=Cfssl,url=https://example.com", + expected: "cfssl: https://example.com", + }, + { + externalCA: "protocol=Cfssl,url=https://example.com,foo=bar", + expected: "cfssl: https://example.com", + }, + { + externalCA: "protocol=Cfssl,url=https://example.com,foo=bar,foo=baz", + expected: "cfssl: https://example.com", + }, + } + for _, tc := range testCases { + opt := &ExternalCAOption{} + assert.NilError(t, opt.Set(tc.externalCA)) + assert.Equal(t, opt.String(), tc.expected) + } +} + +func TestExternalCAOptionMultiple(t *testing.T) { + opt := &ExternalCAOption{} + assert.NilError(t, opt.Set("protocol=cfssl,url=https://example.com")) + assert.NilError(t, opt.Set("protocol=CFSSL,url=anything")) + assert.Equal(t, len(opt.Value()), 2) + assert.Equal(t, opt.String(), "cfssl: https://example.com, cfssl: anything") +} diff --git a/components/cli/command/swarm/testdata/init-init-autolock.golden b/components/cli/command/swarm/testdata/init-init-autolock.golden new file mode 100644 index 0000000000..cdd3c666b6 --- /dev/null +++ b/components/cli/command/swarm/testdata/init-init-autolock.golden @@ -0,0 +1,11 @@ +Swarm initialized: current node (nodeID) is now a manager. + +To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions. + +To unlock a swarm manager after it restarts, run the `docker swarm unlock` +command and provide the following key: + + unlock-key + +Please remember to store this key in a password manager, since without it you +will not be able to restart the manager. diff --git a/components/cli/command/swarm/testdata/init-init.golden b/components/cli/command/swarm/testdata/init-init.golden new file mode 100644 index 0000000000..6e82be010e --- /dev/null +++ b/components/cli/command/swarm/testdata/init-init.golden @@ -0,0 +1,4 @@ +Swarm initialized: current node (nodeID) is now a manager. + +To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions. + diff --git a/components/cli/command/swarm/testdata/jointoken-manager-quiet.golden b/components/cli/command/swarm/testdata/jointoken-manager-quiet.golden new file mode 100644 index 0000000000..0c7cfc6088 --- /dev/null +++ b/components/cli/command/swarm/testdata/jointoken-manager-quiet.golden @@ -0,0 +1 @@ +manager-join-token diff --git a/components/cli/command/swarm/testdata/jointoken-manager-rotate.golden b/components/cli/command/swarm/testdata/jointoken-manager-rotate.golden new file mode 100644 index 0000000000..7ee455bec8 --- /dev/null +++ b/components/cli/command/swarm/testdata/jointoken-manager-rotate.golden @@ -0,0 +1,8 @@ +Successfully rotated manager join token. + +To add a manager to this swarm, run the following command: + + docker swarm join \ + --token manager-join-token \ + 127.0.0.1 + diff --git a/components/cli/command/swarm/testdata/jointoken-manager.golden b/components/cli/command/swarm/testdata/jointoken-manager.golden new file mode 100644 index 0000000000..d56527aa55 --- /dev/null +++ b/components/cli/command/swarm/testdata/jointoken-manager.golden @@ -0,0 +1,6 @@ +To add a manager to this swarm, run the following command: + + docker swarm join \ + --token manager-join-token \ + 127.0.0.1 + diff --git a/components/cli/command/swarm/testdata/jointoken-worker-quiet.golden b/components/cli/command/swarm/testdata/jointoken-worker-quiet.golden new file mode 100644 index 0000000000..b445e191e5 --- /dev/null +++ b/components/cli/command/swarm/testdata/jointoken-worker-quiet.golden @@ -0,0 +1 @@ +worker-join-token diff --git a/components/cli/command/swarm/testdata/jointoken-worker.golden b/components/cli/command/swarm/testdata/jointoken-worker.golden new file mode 100644 index 0000000000..5d44f3daee --- /dev/null +++ b/components/cli/command/swarm/testdata/jointoken-worker.golden @@ -0,0 +1,6 @@ +To add a worker to this swarm, run the following command: + + docker swarm join \ + --token worker-join-token \ + 127.0.0.1 + diff --git a/components/cli/command/swarm/testdata/unlockkeys-unlock-key-quiet.golden b/components/cli/command/swarm/testdata/unlockkeys-unlock-key-quiet.golden new file mode 100644 index 0000000000..ed53505e25 --- /dev/null +++ b/components/cli/command/swarm/testdata/unlockkeys-unlock-key-quiet.golden @@ -0,0 +1 @@ +unlock-key diff --git a/components/cli/command/swarm/testdata/unlockkeys-unlock-key-rotate-quiet.golden b/components/cli/command/swarm/testdata/unlockkeys-unlock-key-rotate-quiet.golden new file mode 100644 index 0000000000..ed53505e25 --- /dev/null +++ b/components/cli/command/swarm/testdata/unlockkeys-unlock-key-rotate-quiet.golden @@ -0,0 +1 @@ +unlock-key diff --git a/components/cli/command/swarm/testdata/unlockkeys-unlock-key-rotate.golden b/components/cli/command/swarm/testdata/unlockkeys-unlock-key-rotate.golden new file mode 100644 index 0000000000..89152b8643 --- /dev/null +++ b/components/cli/command/swarm/testdata/unlockkeys-unlock-key-rotate.golden @@ -0,0 +1,9 @@ +Successfully rotated manager unlock key. + +To unlock a swarm manager after it restarts, run the `docker swarm unlock` +command and provide the following key: + + unlock-key + +Please remember to store this key in a password manager, since without it you +will not be able to restart the manager. diff --git a/components/cli/command/swarm/testdata/unlockkeys-unlock-key.golden b/components/cli/command/swarm/testdata/unlockkeys-unlock-key.golden new file mode 100644 index 0000000000..8316df478c --- /dev/null +++ b/components/cli/command/swarm/testdata/unlockkeys-unlock-key.golden @@ -0,0 +1,7 @@ +To unlock a swarm manager after it restarts, run the `docker swarm unlock` +command and provide the following key: + + unlock-key + +Please remember to store this key in a password manager, since without it you +will not be able to restart the manager. diff --git a/components/cli/command/swarm/testdata/update-all-flags-quiet.golden b/components/cli/command/swarm/testdata/update-all-flags-quiet.golden new file mode 100644 index 0000000000..3d195a2586 --- /dev/null +++ b/components/cli/command/swarm/testdata/update-all-flags-quiet.golden @@ -0,0 +1 @@ +Swarm updated. diff --git a/components/cli/command/swarm/testdata/update-autolock-unlock-key.golden b/components/cli/command/swarm/testdata/update-autolock-unlock-key.golden new file mode 100644 index 0000000000..a077b9e167 --- /dev/null +++ b/components/cli/command/swarm/testdata/update-autolock-unlock-key.golden @@ -0,0 +1,8 @@ +Swarm updated. +To unlock a swarm manager after it restarts, run the `docker swarm unlock` +command and provide the following key: + + unlock-key + +Please remember to store this key in a password manager, since without it you +will not be able to restart the manager. diff --git a/components/cli/command/swarm/testdata/update-noargs.golden b/components/cli/command/swarm/testdata/update-noargs.golden new file mode 100644 index 0000000000..381c0ccf1f --- /dev/null +++ b/components/cli/command/swarm/testdata/update-noargs.golden @@ -0,0 +1,13 @@ +Update the swarm + +Usage: + update [OPTIONS] [flags] + +Flags: + --autolock Change manager autolocking setting (true|false) + --cert-expiry duration Validity period for node certificates (ns|us|ms|s|m|h) (default 2160h0m0s) + --dispatcher-heartbeat duration Dispatcher heartbeat period (ns|us|ms|s|m|h) (default 5s) + --external-ca external-ca Specifications of one or more certificate signing endpoints + --max-snapshots uint Number of additional Raft snapshots to retain + --snapshot-interval uint Number of log entries between Raft snapshots (default 10000) + --task-history-limit int Task history retention limit (default 5) diff --git a/components/cli/command/swarm/unlock.go b/components/cli/command/swarm/unlock.go index aa752e2148..45dd6e79e3 100644 --- a/components/cli/command/swarm/unlock.go +++ b/components/cli/command/swarm/unlock.go @@ -18,7 +18,7 @@ import ( type unlockOptions struct{} -func newUnlockCommand(dockerCli *command.DockerCli) *cobra.Command { +func newUnlockCommand(dockerCli command.Cli) *cobra.Command { opts := unlockOptions{} cmd := &cobra.Command{ @@ -33,7 +33,7 @@ func newUnlockCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runUnlock(dockerCli *command.DockerCli, opts unlockOptions) error { +func runUnlock(dockerCli command.Cli, opts unlockOptions) error { client := dockerCli.Client() ctx := context.Background() diff --git a/components/cli/command/swarm/unlock_key.go b/components/cli/command/swarm/unlock_key.go index e571e6645f..77c97d88ea 100644 --- a/components/cli/command/swarm/unlock_key.go +++ b/components/cli/command/swarm/unlock_key.go @@ -3,12 +3,11 @@ package swarm import ( "fmt" - "github.com/spf13/cobra" - "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/pkg/errors" + "github.com/spf13/cobra" "golang.org/x/net/context" ) @@ -17,7 +16,7 @@ type unlockKeyOptions struct { quiet bool } -func newUnlockKeyCommand(dockerCli *command.DockerCli) *cobra.Command { +func newUnlockKeyCommand(dockerCli command.Cli) *cobra.Command { opts := unlockKeyOptions{} cmd := &cobra.Command{ @@ -36,7 +35,7 @@ func newUnlockKeyCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runUnlockKey(dockerCli *command.DockerCli, opts unlockKeyOptions) error { +func runUnlockKey(dockerCli command.Cli, opts unlockKeyOptions) error { client := dockerCli.Client() ctx := context.Background() @@ -79,7 +78,7 @@ func runUnlockKey(dockerCli *command.DockerCli, opts unlockKeyOptions) error { return nil } -func printUnlockCommand(ctx context.Context, dockerCli *command.DockerCli, unlockKey string) { +func printUnlockCommand(ctx context.Context, dockerCli command.Cli, unlockKey string) { if len(unlockKey) > 0 { fmt.Fprintf(dockerCli.Out(), "To unlock a swarm manager after it restarts, run the `docker swarm unlock`\ncommand and provide the following key:\n\n %s\n\nPlease remember to store this key in a password manager, since without it you\nwill not be able to restart the manager.\n", unlockKey) } diff --git a/components/cli/command/swarm/unlock_key_test.go b/components/cli/command/swarm/unlock_key_test.go new file mode 100644 index 0000000000..17a07d3fb1 --- /dev/null +++ b/components/cli/command/swarm/unlock_key_test.go @@ -0,0 +1,175 @@ +package swarm + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/internal/test" + // Import builders to get the builder function as package function + . "github.com/docker/docker/cli/internal/test/builders" + "github.com/docker/docker/pkg/testutil/assert" + "github.com/docker/docker/pkg/testutil/golden" +) + +func TestSwarmUnlockKeyErrors(t *testing.T) { + testCases := []struct { + name string + args []string + flags map[string]string + swarmInspectFunc func() (swarm.Swarm, error) + swarmUpdateFunc func(swarm swarm.Spec, flags swarm.UpdateFlags) error + swarmGetUnlockKeyFunc func() (types.SwarmUnlockKeyResponse, error) + expectedError string + }{ + { + name: "too-many-args", + args: []string{"foo"}, + expectedError: "accepts no argument(s)", + }, + { + name: "swarm-inspect-rotate-failed", + flags: map[string]string{ + flagRotate: "true", + }, + swarmInspectFunc: func() (swarm.Swarm, error) { + return swarm.Swarm{}, fmt.Errorf("error inspecting the swarm") + }, + expectedError: "error inspecting the swarm", + }, + { + name: "swarm-rotate-no-autolock-failed", + flags: map[string]string{ + flagRotate: "true", + }, + swarmInspectFunc: func() (swarm.Swarm, error) { + return *Swarm(), nil + }, + expectedError: "cannot rotate because autolock is not turned on", + }, + { + name: "swarm-update-failed", + flags: map[string]string{ + flagRotate: "true", + }, + swarmInspectFunc: func() (swarm.Swarm, error) { + return *Swarm(Autolock()), nil + }, + swarmUpdateFunc: func(swarm swarm.Spec, flags swarm.UpdateFlags) error { + return fmt.Errorf("error updating the swarm") + }, + expectedError: "error updating the swarm", + }, + { + name: "swarm-get-unlock-key-failed", + swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { + return types.SwarmUnlockKeyResponse{}, fmt.Errorf("error getting unlock key") + }, + expectedError: "error getting unlock key", + }, + { + name: "swarm-no-unlock-key-failed", + swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { + return types.SwarmUnlockKeyResponse{ + UnlockKey: "", + }, nil + }, + expectedError: "no unlock key is set", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newUnlockKeyCommand( + test.NewFakeCli(&fakeClient{ + swarmInspectFunc: tc.swarmInspectFunc, + swarmUpdateFunc: tc.swarmUpdateFunc, + swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, + }, buf)) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestSwarmUnlockKey(t *testing.T) { + testCases := []struct { + name string + args []string + flags map[string]string + swarmInspectFunc func() (swarm.Swarm, error) + swarmUpdateFunc func(swarm swarm.Spec, flags swarm.UpdateFlags) error + swarmGetUnlockKeyFunc func() (types.SwarmUnlockKeyResponse, error) + }{ + { + name: "unlock-key", + swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { + return types.SwarmUnlockKeyResponse{ + UnlockKey: "unlock-key", + }, nil + }, + }, + { + name: "unlock-key-quiet", + flags: map[string]string{ + flagQuiet: "true", + }, + swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { + return types.SwarmUnlockKeyResponse{ + UnlockKey: "unlock-key", + }, nil + }, + }, + { + name: "unlock-key-rotate", + flags: map[string]string{ + flagRotate: "true", + }, + swarmInspectFunc: func() (swarm.Swarm, error) { + return *Swarm(Autolock()), nil + }, + swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { + return types.SwarmUnlockKeyResponse{ + UnlockKey: "unlock-key", + }, nil + }, + }, + { + name: "unlock-key-rotate-quiet", + flags: map[string]string{ + flagQuiet: "true", + flagRotate: "true", + }, + swarmInspectFunc: func() (swarm.Swarm, error) { + return *Swarm(Autolock()), nil + }, + swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { + return types.SwarmUnlockKeyResponse{ + UnlockKey: "unlock-key", + }, nil + }, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newUnlockKeyCommand( + test.NewFakeCli(&fakeClient{ + swarmInspectFunc: tc.swarmInspectFunc, + swarmUpdateFunc: tc.swarmUpdateFunc, + swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, + }, buf)) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), fmt.Sprintf("unlockkeys-%s.golden", tc.name)) + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) + } +} diff --git a/components/cli/command/swarm/unlock_test.go b/components/cli/command/swarm/unlock_test.go new file mode 100644 index 0000000000..abf858a289 --- /dev/null +++ b/components/cli/command/swarm/unlock_test.go @@ -0,0 +1,101 @@ +package swarm + +import ( + "bytes" + "fmt" + "io/ioutil" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/internal/test" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestSwarmUnlockErrors(t *testing.T) { + testCases := []struct { + name string + args []string + input string + swarmUnlockFunc func(req swarm.UnlockRequest) error + infoFunc func() (types.Info, error) + expectedError string + }{ + { + name: "too-many-args", + args: []string{"foo"}, + expectedError: "accepts no argument(s)", + }, + { + name: "is-not-part-of-a-swarm", + infoFunc: func() (types.Info, error) { + return types.Info{ + Swarm: swarm.Info{ + LocalNodeState: swarm.LocalNodeStateInactive, + }, + }, nil + }, + expectedError: "This node is not part of a swarm", + }, + { + name: "is-not-locked", + infoFunc: func() (types.Info, error) { + return types.Info{ + Swarm: swarm.Info{ + LocalNodeState: swarm.LocalNodeStateActive, + }, + }, nil + }, + expectedError: "Error: swarm is not locked", + }, + { + name: "unlockrequest-failed", + infoFunc: func() (types.Info, error) { + return types.Info{ + Swarm: swarm.Info{ + LocalNodeState: swarm.LocalNodeStateLocked, + }, + }, nil + }, + swarmUnlockFunc: func(req swarm.UnlockRequest) error { + return fmt.Errorf("error unlocking the swarm") + }, + expectedError: "error unlocking the swarm", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newUnlockCommand( + test.NewFakeCli(&fakeClient{ + infoFunc: tc.infoFunc, + swarmUnlockFunc: tc.swarmUnlockFunc, + }, buf)) + cmd.SetArgs(tc.args) + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestSwarmUnlock(t *testing.T) { + input := "unlockKey" + buf := new(bytes.Buffer) + dockerCli := test.NewFakeCli(&fakeClient{ + infoFunc: func() (types.Info, error) { + return types.Info{ + Swarm: swarm.Info{ + LocalNodeState: swarm.LocalNodeStateLocked, + }, + }, nil + }, + swarmUnlockFunc: func(req swarm.UnlockRequest) error { + if req.UnlockKey != input { + return fmt.Errorf("Invalid unlock key") + } + return nil + }, + }, buf) + dockerCli.SetIn(ioutil.NopCloser(strings.NewReader(input))) + cmd := newUnlockCommand(dockerCli) + assert.NilError(t, cmd.Execute()) +} diff --git a/components/cli/command/swarm/update.go b/components/cli/command/swarm/update.go index dbbd268725..1ccd268e74 100644 --- a/components/cli/command/swarm/update.go +++ b/components/cli/command/swarm/update.go @@ -13,7 +13,7 @@ import ( "github.com/spf13/pflag" ) -func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { +func newUpdateCommand(dockerCli command.Cli) *cobra.Command { opts := swarmOptions{} cmd := &cobra.Command{ @@ -36,24 +36,24 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts swarmOptions) error { +func runUpdate(dockerCli command.Cli, flags *pflag.FlagSet, opts swarmOptions) error { client := dockerCli.Client() ctx := context.Background() var updateFlags swarm.UpdateFlags - swarm, err := client.SwarmInspect(ctx) + swarmInspect, err := client.SwarmInspect(ctx) if err != nil { return err } - prevAutoLock := swarm.Spec.EncryptionConfig.AutoLockManagers + prevAutoLock := swarmInspect.Spec.EncryptionConfig.AutoLockManagers - opts.mergeSwarmSpec(&swarm.Spec, flags) + opts.mergeSwarmSpec(&swarmInspect.Spec, flags) - curAutoLock := swarm.Spec.EncryptionConfig.AutoLockManagers + curAutoLock := swarmInspect.Spec.EncryptionConfig.AutoLockManagers - err = client.SwarmUpdate(ctx, swarm.Version, swarm.Spec, updateFlags) + err = client.SwarmUpdate(ctx, swarmInspect.Version, swarmInspect.Spec, updateFlags) if err != nil { return err } diff --git a/components/cli/command/swarm/update_test.go b/components/cli/command/swarm/update_test.go new file mode 100644 index 0000000000..c8a2860a00 --- /dev/null +++ b/components/cli/command/swarm/update_test.go @@ -0,0 +1,182 @@ +package swarm + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/internal/test" + // Import builders to get the builder function as package function + . "github.com/docker/docker/cli/internal/test/builders" + "github.com/docker/docker/pkg/testutil/assert" + "github.com/docker/docker/pkg/testutil/golden" +) + +func TestSwarmUpdateErrors(t *testing.T) { + testCases := []struct { + name string + args []string + flags map[string]string + swarmInspectFunc func() (swarm.Swarm, error) + swarmUpdateFunc func(swarm swarm.Spec, flags swarm.UpdateFlags) error + swarmGetUnlockKeyFunc func() (types.SwarmUnlockKeyResponse, error) + expectedError string + }{ + { + name: "too-many-args", + args: []string{"foo"}, + expectedError: "accepts no argument(s)", + }, + { + name: "swarm-inspect-error", + flags: map[string]string{ + flagTaskHistoryLimit: "10", + }, + swarmInspectFunc: func() (swarm.Swarm, error) { + return swarm.Swarm{}, fmt.Errorf("error inspecting the swarm") + }, + expectedError: "error inspecting the swarm", + }, + { + name: "swarm-update-error", + flags: map[string]string{ + flagTaskHistoryLimit: "10", + }, + swarmUpdateFunc: func(swarm swarm.Spec, flags swarm.UpdateFlags) error { + return fmt.Errorf("error updating the swarm") + }, + expectedError: "error updating the swarm", + }, + { + name: "swarm-unlockkey-error", + flags: map[string]string{ + flagAutolock: "true", + }, + swarmInspectFunc: func() (swarm.Swarm, error) { + return *Swarm(), nil + }, + swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { + return types.SwarmUnlockKeyResponse{}, fmt.Errorf("error getting unlock key") + }, + expectedError: "error getting unlock key", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newUpdateCommand( + test.NewFakeCli(&fakeClient{ + swarmInspectFunc: tc.swarmInspectFunc, + swarmUpdateFunc: tc.swarmUpdateFunc, + swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, + }, buf)) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestSwarmUpdate(t *testing.T) { + testCases := []struct { + name string + args []string + flags map[string]string + swarmInspectFunc func() (swarm.Swarm, error) + swarmUpdateFunc func(swarm swarm.Spec, flags swarm.UpdateFlags) error + swarmGetUnlockKeyFunc func() (types.SwarmUnlockKeyResponse, error) + }{ + { + name: "noargs", + }, + { + name: "all-flags-quiet", + flags: map[string]string{ + flagTaskHistoryLimit: "10", + flagDispatcherHeartbeat: "10s", + flagCertExpiry: "20s", + flagExternalCA: "protocol=cfssl,url=https://example.com.", + flagMaxSnapshots: "10", + flagSnapshotInterval: "100", + flagAutolock: "true", + flagQuiet: "true", + }, + swarmUpdateFunc: func(swarm swarm.Spec, flags swarm.UpdateFlags) error { + if *swarm.Orchestration.TaskHistoryRetentionLimit != 10 { + return fmt.Errorf("historyLimit not correctly set") + } + heartbeatDuration, err := time.ParseDuration("10s") + if err != nil { + return err + } + if swarm.Dispatcher.HeartbeatPeriod != heartbeatDuration { + return fmt.Errorf("heartbeatPeriodLimit not correctly set") + } + certExpiryDuration, err := time.ParseDuration("20s") + if err != nil { + return err + } + if swarm.CAConfig.NodeCertExpiry != certExpiryDuration { + return fmt.Errorf("certExpiry not correctly set") + } + if len(swarm.CAConfig.ExternalCAs) != 1 { + return fmt.Errorf("externalCA not correctly set") + } + if *swarm.Raft.KeepOldSnapshots != 10 { + return fmt.Errorf("keepOldSnapshots not correctly set") + } + if swarm.Raft.SnapshotInterval != 100 { + return fmt.Errorf("snapshotInterval not correctly set") + } + if !swarm.EncryptionConfig.AutoLockManagers { + return fmt.Errorf("autolock not correctly set") + } + return nil + }, + }, + { + name: "autolock-unlock-key", + flags: map[string]string{ + flagTaskHistoryLimit: "10", + flagAutolock: "true", + }, + swarmUpdateFunc: func(swarm swarm.Spec, flags swarm.UpdateFlags) error { + if *swarm.Orchestration.TaskHistoryRetentionLimit != 10 { + return fmt.Errorf("historyLimit not correctly set") + } + return nil + }, + swarmInspectFunc: func() (swarm.Swarm, error) { + return *Swarm(), nil + }, + swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { + return types.SwarmUnlockKeyResponse{ + UnlockKey: "unlock-key", + }, nil + }, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newUpdateCommand( + test.NewFakeCli(&fakeClient{ + swarmInspectFunc: tc.swarmInspectFunc, + swarmUpdateFunc: tc.swarmUpdateFunc, + swarmGetUnlockKeyFunc: tc.swarmGetUnlockKeyFunc, + }, buf)) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + cmd.SetOutput(buf) + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), fmt.Sprintf("update-%s.golden", tc.name)) + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) + } +} diff --git a/components/cli/command/task/print.go b/components/cli/command/task/print.go index 57c4e0c8c8..60a2bca85b 100644 --- a/components/cli/command/task/print.go +++ b/components/cli/command/task/print.go @@ -61,7 +61,7 @@ func (t tasksBySlot) Less(i, j int) bool { // Print task information in a table format. // Besides this, command `docker node ps ` // and `docker stack ps` will call this, too. -func Print(dockerCli *command.DockerCli, ctx context.Context, tasks []swarm.Task, resolver *idresolver.IDResolver, noTrunc bool) error { +func Print(dockerCli command.Cli, ctx context.Context, tasks []swarm.Task, resolver *idresolver.IDResolver, noTrunc bool) error { sort.Stable(tasksBySlot(tasks)) writer := tabwriter.NewWriter(dockerCli.Out(), 0, 4, 2, ' ', 0) @@ -74,7 +74,7 @@ func Print(dockerCli *command.DockerCli, ctx context.Context, tasks []swarm.Task } // PrintQuiet shows task list in a quiet way. -func PrintQuiet(dockerCli *command.DockerCli, tasks []swarm.Task) error { +func PrintQuiet(dockerCli command.Cli, tasks []swarm.Task) error { sort.Stable(tasksBySlot(tasks)) out := dockerCli.Out() diff --git a/components/cli/internal/test/builders/node.go b/components/cli/internal/test/builders/node.go new file mode 100644 index 0000000000..63fdebba12 --- /dev/null +++ b/components/cli/internal/test/builders/node.go @@ -0,0 +1,117 @@ +package builders + +import ( + "time" + + "github.com/docker/docker/api/types/swarm" +) + +// Node creates a node with default values. +// Any number of node function builder can be pass to augment it. +func Node(builders ...func(*swarm.Node)) *swarm.Node { + t1 := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) + node := &swarm.Node{ + ID: "nodeID", + Meta: swarm.Meta{ + CreatedAt: t1, + }, + Description: swarm.NodeDescription{ + Hostname: "defaultNodeHostname", + Platform: swarm.Platform{ + Architecture: "x86_64", + OS: "linux", + }, + Resources: swarm.Resources{ + NanoCPUs: 4, + MemoryBytes: 20 * 1024 * 1024, + }, + Engine: swarm.EngineDescription{ + EngineVersion: "1.13.0", + Labels: map[string]string{ + "engine": "label", + }, + Plugins: []swarm.PluginDescription{ + { + Type: "Volume", + Name: "local", + }, + { + Type: "Network", + Name: "bridge", + }, + { + Type: "Network", + Name: "overlay", + }, + }, + }, + }, + Status: swarm.NodeStatus{ + State: swarm.NodeStateReady, + Addr: "127.0.0.1", + }, + Spec: swarm.NodeSpec{ + Annotations: swarm.Annotations{ + Name: "defaultNodeName", + }, + Role: swarm.NodeRoleWorker, + Availability: swarm.NodeAvailabilityActive, + }, + } + + for _, builder := range builders { + builder(node) + } + + return node +} + +// NodeID sets the node id +func NodeID(id string) func(*swarm.Node) { + return func(node *swarm.Node) { + node.ID = id + } +} + +// NodeLabels sets the node labels +func NodeLabels(labels map[string]string) func(*swarm.Node) { + return func(node *swarm.Node) { + node.Spec.Labels = labels + } +} + +// Hostname sets the node hostname +func Hostname(hostname string) func(*swarm.Node) { + return func(node *swarm.Node) { + node.Description.Hostname = hostname + } +} + +// Leader sets the current node as a leader +func Leader() func(*swarm.ManagerStatus) { + return func(managerStatus *swarm.ManagerStatus) { + managerStatus.Leader = true + } +} + +// Manager set the current node as a manager +func Manager(managerStatusBuilders ...func(*swarm.ManagerStatus)) func(*swarm.Node) { + return func(node *swarm.Node) { + node.Spec.Role = swarm.NodeRoleManager + node.ManagerStatus = ManagerStatus(managerStatusBuilders...) + } +} + +// ManagerStatus create a ManageStatus with default values. +func ManagerStatus(managerStatusBuilders ...func(*swarm.ManagerStatus)) *swarm.ManagerStatus { + managerStatus := &swarm.ManagerStatus{ + Reachability: swarm.ReachabilityReachable, + Addr: "127.0.0.1", + } + + for _, builder := range managerStatusBuilders { + builder(managerStatus) + } + + return managerStatus +} diff --git a/components/cli/internal/test/builders/swarm.go b/components/cli/internal/test/builders/swarm.go new file mode 100644 index 0000000000..ab1a930628 --- /dev/null +++ b/components/cli/internal/test/builders/swarm.go @@ -0,0 +1,39 @@ +package builders + +import ( + "time" + + "github.com/docker/docker/api/types/swarm" +) + +// Swarm creates a swarm with default values. +// Any number of swarm function builder can be pass to augment it. +func Swarm(swarmBuilders ...func(*swarm.Swarm)) *swarm.Swarm { + t1 := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) + swarm := &swarm.Swarm{ + ClusterInfo: swarm.ClusterInfo{ + ID: "swarm", + Meta: swarm.Meta{ + CreatedAt: t1, + }, + Spec: swarm.Spec{}, + }, + JoinTokens: swarm.JoinTokens{ + Worker: "worker-join-token", + Manager: "manager-join-token", + }, + } + + for _, builder := range swarmBuilders { + builder(swarm) + } + + return swarm +} + +// Autolock set the swarm into autolock mode +func Autolock() func(*swarm.Swarm) { + return func(swarm *swarm.Swarm) { + swarm.Spec.EncryptionConfig.AutoLockManagers = true + } +} diff --git a/components/cli/internal/test/builders/task.go b/components/cli/internal/test/builders/task.go new file mode 100644 index 0000000000..688c62a3a8 --- /dev/null +++ b/components/cli/internal/test/builders/task.go @@ -0,0 +1,111 @@ +package builders + +import ( + "time" + + "github.com/docker/docker/api/types/swarm" +) + +var ( + defaultTime = time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) +) + +// Task creates a task with default values . +// Any number of task function builder can be pass to augment it. +func Task(taskBuilders ...func(*swarm.Task)) *swarm.Task { + task := &swarm.Task{ + ID: "taskID", + Meta: swarm.Meta{ + CreatedAt: defaultTime, + }, + Annotations: swarm.Annotations{ + Name: "defaultTaskName", + }, + Spec: *TaskSpec(), + ServiceID: "rl02d5gwz6chzu7il5fhtb8be", + Slot: 1, + Status: *TaskStatus(), + DesiredState: swarm.TaskStateReady, + } + + for _, builder := range taskBuilders { + builder(task) + } + + return task +} + +// TaskID sets the task ID +func TaskID(id string) func(*swarm.Task) { + return func(task *swarm.Task) { + task.ID = id + } +} + +// ServiceID sets the task service's ID +func ServiceID(id string) func(*swarm.Task) { + return func(task *swarm.Task) { + task.ServiceID = id + } +} + +// WithStatus sets the task status +func WithStatus(statusBuilders ...func(*swarm.TaskStatus)) func(*swarm.Task) { + return func(task *swarm.Task) { + task.Status = *TaskStatus(statusBuilders...) + } +} + +// TaskStatus creates a task status with default values . +// Any number of taskStatus function builder can be pass to augment it. +func TaskStatus(statusBuilders ...func(*swarm.TaskStatus)) *swarm.TaskStatus { + timestamp := defaultTime.Add(1 * time.Hour) + taskStatus := &swarm.TaskStatus{ + State: swarm.TaskStateReady, + Timestamp: timestamp, + } + + for _, builder := range statusBuilders { + builder(taskStatus) + } + + return taskStatus +} + +// Timestamp sets the task status timestamp +func Timestamp(t time.Time) func(*swarm.TaskStatus) { + return func(taskStatus *swarm.TaskStatus) { + taskStatus.Timestamp = t + } +} + +// StatusErr sets the tasks status error +func StatusErr(err string) func(*swarm.TaskStatus) { + return func(taskStatus *swarm.TaskStatus) { + taskStatus.Err = err + } +} + +// PortStatus sets the tasks port config status +// FIXME(vdemeester) should be a sub builder 👼 +func PortStatus(portConfigs []swarm.PortConfig) func(*swarm.TaskStatus) { + return func(taskStatus *swarm.TaskStatus) { + taskStatus.PortStatus.Ports = portConfigs + } +} + +// TaskSpec creates a task spec with default values . +// Any number of taskSpec function builder can be pass to augment it. +func TaskSpec(specBuilders ...func(*swarm.TaskSpec)) *swarm.TaskSpec { + taskSpec := &swarm.TaskSpec{ + ContainerSpec: swarm.ContainerSpec{ + Image: "myimage:mytag", + }, + } + + for _, builder := range specBuilders { + builder(taskSpec) + } + + return taskSpec +} diff --git a/components/cli/internal/test/cli.go b/components/cli/internal/test/cli.go new file mode 100644 index 0000000000..06ab053e98 --- /dev/null +++ b/components/cli/internal/test/cli.go @@ -0,0 +1,48 @@ +// Package test is a test-only package that can be used by other cli package to write unit test +package test + +import ( + "io" + "io/ioutil" + + "github.com/docker/docker/cli/command" + "github.com/docker/docker/client" + "strings" +) + +// FakeCli emulates the default DockerCli +type FakeCli struct { + command.DockerCli + client client.APIClient + out io.Writer + in io.ReadCloser +} + +// NewFakeCli returns a Cli backed by the fakeCli +func NewFakeCli(client client.APIClient, out io.Writer) *FakeCli { + return &FakeCli{ + client: client, + out: out, + in: ioutil.NopCloser(strings.NewReader("")), + } +} + +// SetIn sets the input of the cli to the specified ReadCloser +func (c *FakeCli) SetIn(in io.ReadCloser) { + c.in = in +} + +// Client returns a docker API client +func (c *FakeCli) Client() client.APIClient { + return c.client +} + +// Out returns the output stream the cli should write on +func (c *FakeCli) Out() *command.OutStream { + return command.NewOutStream(c.out) +} + +// In returns thi input stream the cli will use +func (c *FakeCli) In() *command.InStream { + return command.NewInStream(c.in) +} From f3fc9391c51635d45a3cb713d16446f2ba97d06e Mon Sep 17 00:00:00 2001 From: Dong Chen Date: Thu, 5 Jan 2017 11:21:22 -0800 Subject: [PATCH 520/978] add port PublishMode to service inspect --pretty output Signed-off-by: Dong Chen Upstream-commit: e8ad538d90190d81ecef623de92d089409f26fcf Component: cli --- components/cli/command/formatter/service.go | 1 + components/cli/command/service/update.go | 6 +----- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/components/cli/command/formatter/service.go b/components/cli/command/formatter/service.go index 2690029ce4..8242e1cb9e 100644 --- a/components/cli/command/formatter/service.go +++ b/components/cli/command/formatter/service.go @@ -103,6 +103,7 @@ Ports: PublishedPort {{ $port.PublishedPort }} Protocol = {{ $port.Protocol }} TargetPort = {{ $port.TargetPort }} + PublishMode = {{ $port.PublishMode }} {{- end }} {{ end -}} ` diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index 514b1bd510..f1e41c5cdb 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -691,11 +691,7 @@ portLoop: ports := flags.Lookup(flagPublishAdd).Value.(*opts.PortOpt).Value() for _, port := range ports { - if v, ok := portSet[portConfigToString(&port)]; ok { - if v != port { - fmt.Println("v", v) - return fmt.Errorf("conflicting port mapping between %v:%v/%s and %v:%v/%s", port.PublishedPort, port.TargetPort, port.Protocol, v.PublishedPort, v.TargetPort, v.Protocol) - } + if _, ok := portSet[portConfigToString(&port)]; ok { continue } //portSet[portConfigToString(&port)] = port From 0f057e3c8dad243481c8a1f738b8a36cfe110547 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 9 Jan 2017 14:22:02 -0500 Subject: [PATCH 521/978] Fix parsing resources from compose file for stack deploy. Signed-off-by: Daniel Nephin Upstream-commit: c2f0402f4d8b4f676931c9c670449f0629bc8ea7 Component: cli --- components/cli/compose/convert/service.go | 19 ++++++++++----- .../cli/compose/convert/service_test.go | 23 +++++++++++++++++++ 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/components/cli/compose/convert/service.go b/components/cli/compose/convert/service.go index 2a8ed8288d..37f3ece403 100644 --- a/components/cli/compose/convert/service.go +++ b/components/cli/compose/convert/service.go @@ -263,10 +263,14 @@ func convertUpdateConfig(source *composetypes.UpdateConfig) *swarm.UpdateConfig func convertResources(source composetypes.Resources) (*swarm.ResourceRequirements, error) { resources := &swarm.ResourceRequirements{} + var err error if source.Limits != nil { - cpus, err := opts.ParseCPUs(source.Limits.NanoCPUs) - if err != nil { - return nil, err + var cpus int64 + if source.Limits.NanoCPUs != "" { + cpus, err = opts.ParseCPUs(source.Limits.NanoCPUs) + if err != nil { + return nil, err + } } resources.Limits = &swarm.Resources{ NanoCPUs: cpus, @@ -274,9 +278,12 @@ func convertResources(source composetypes.Resources) (*swarm.ResourceRequirement } } if source.Reservations != nil { - cpus, err := opts.ParseCPUs(source.Reservations.NanoCPUs) - if err != nil { - return nil, err + var cpus int64 + if source.Reservations.NanoCPUs != "" { + cpus, err = opts.ParseCPUs(source.Reservations.NanoCPUs) + if err != nil { + return nil, err + } } resources.Reservations = &swarm.Resources{ NanoCPUs: cpus, diff --git a/components/cli/compose/convert/service_test.go b/components/cli/compose/convert/service_test.go index 45da764325..2e614d730c 100644 --- a/components/cli/compose/convert/service_test.go +++ b/components/cli/compose/convert/service_test.go @@ -80,6 +80,29 @@ func TestConvertResourcesFull(t *testing.T) { assert.DeepEqual(t, resources, expected) } +func TestConvertResourcesOnlyMemory(t *testing.T) { + source := composetypes.Resources{ + Limits: &composetypes.Resource{ + MemoryBytes: composetypes.UnitBytes(300000000), + }, + Reservations: &composetypes.Resource{ + MemoryBytes: composetypes.UnitBytes(200000000), + }, + } + resources, err := convertResources(source) + assert.NilError(t, err) + + expected := &swarm.ResourceRequirements{ + Limits: &swarm.Resources{ + MemoryBytes: 300000000, + }, + Reservations: &swarm.Resources{ + MemoryBytes: 200000000, + }, + } + assert.DeepEqual(t, resources, expected) +} + func TestConvertHealthcheck(t *testing.T) { retries := uint64(10) source := &composetypes.HealthCheckConfig{ From 11ca819f5a6f98ea1fb02f5b631d4246cf3f46ce Mon Sep 17 00:00:00 2001 From: Boaz Shuster Date: Tue, 6 Dec 2016 23:15:27 +0200 Subject: [PATCH 522/978] 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 Upstream-commit: 0247b1509c94c6597886be4616b203758f94a712 Component: cli --- components/cli/image_build.go | 2 +- components/cli/image_build_test.go | 4 ++-- components/cli/plugin_create.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/components/cli/image_build.go b/components/cli/image_build.go index 6fde75dcfd..411d5493ea 100644 --- a/components/cli/image_build.go +++ b/components/cli/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/components/cli/image_build_test.go b/components/cli/image_build_test.go index b9d04f817a..1e18b7bda8 100644 --- a/components/cli/image_build_test.go +++ b/components/cli/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/components/cli/plugin_create.go b/components/cli/plugin_create.go index a660ba5733..27954aa573 100644 --- a/components/cli/plugin_create.go +++ b/components/cli/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 4b2d2c595b6130be33fcd44c23ab91ffdab3d97d Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Tue, 10 Jan 2017 09:57:36 +0100 Subject: [PATCH 523/978] Few stack deploy network fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Make sure we use the correct network name for external ones. - Make the default network overridable and only creates networks that are used by services — so that default network is only created if a service doesn't declare a network. Signed-off-by: Vincent Demeester Upstream-commit: 70643ad005145a10af049a5ffb705b682ae1454e Component: cli --- components/cli/command/stack/deploy.go | 18 +++++++++++++++++- components/cli/compose/convert/compose.go | 11 +++-------- components/cli/compose/convert/compose_test.go | 7 ++++++- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index f4730db556..306a583e1e 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -117,7 +117,9 @@ func deployCompose(ctx context.Context, dockerCli *command.DockerCli, opts deplo namespace := convert.NewNamespace(opts.namespace) - networks, externalNetworks := convert.Networks(namespace, config.Networks) + serviceNetworks := getServicesDeclaredNetworks(config.Services) + + networks, externalNetworks := convert.Networks(namespace, config.Networks, serviceNetworks) if err := validateExternalNetworks(ctx, dockerCli, externalNetworks); err != nil { return err } @@ -131,6 +133,20 @@ func deployCompose(ctx context.Context, dockerCli *command.DockerCli, opts deplo return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth) } +func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} { + serviceNetworks := map[string]struct{}{} + for _, serviceConfig := range serviceConfigs { + if len(serviceConfig.Networks) == 0 { + serviceNetworks["default"] = struct{}{} + continue + } + for network := range serviceConfig.Networks { + serviceNetworks[network] = struct{}{} + } + } + return serviceNetworks +} + func propertyWarnings(properties map[string]string) string { var msgs []string for name, description := range properties { diff --git a/components/cli/compose/convert/compose.go b/components/cli/compose/convert/compose.go index 7c410844c7..70c1762a48 100644 --- a/components/cli/compose/convert/compose.go +++ b/components/cli/compose/convert/compose.go @@ -43,20 +43,15 @@ func AddStackLabel(namespace Namespace, labels map[string]string) map[string]str type networkMap map[string]composetypes.NetworkConfig // Networks from the compose-file type to the engine API type -func Networks(namespace Namespace, networks networkMap) (map[string]types.NetworkCreate, []string) { +func Networks(namespace Namespace, networks networkMap, servicesNetworks map[string]struct{}) (map[string]types.NetworkCreate, []string) { if networks == nil { networks = make(map[string]composetypes.NetworkConfig) } - // TODO: only add default network if it's used - if _, ok := networks["default"]; !ok { - networks["default"] = composetypes.NetworkConfig{} - } - externalNetworks := []string{} result := make(map[string]types.NetworkCreate) - - for internalName, network := range networks { + for internalName := range servicesNetworks { + network := networks[internalName] if network.External.External { externalNetworks = append(externalNetworks, network.External.Name) continue diff --git a/components/cli/compose/convert/compose_test.go b/components/cli/compose/convert/compose_test.go index 27a67047d8..d88ac7f7c4 100644 --- a/components/cli/compose/convert/compose_test.go +++ b/components/cli/compose/convert/compose_test.go @@ -28,6 +28,11 @@ func TestAddStackLabel(t *testing.T) { func TestNetworks(t *testing.T) { namespace := Namespace{name: "foo"} + serviceNetworks := map[string]struct{}{ + "normal": {}, + "outside": {}, + "default": {}, + } source := networkMap{ "normal": composetypes.NetworkConfig{ Driver: "overlay", @@ -79,7 +84,7 @@ func TestNetworks(t *testing.T) { }, } - networks, externals := Networks(namespace, source) + networks, externals := Networks(namespace, source, serviceNetworks) assert.DeepEqual(t, networks, expected) assert.DeepEqual(t, externals, []string{"special"}) } From 7d950520e5467c2d12d0aa7686063a01072afd5b Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 21 Dec 2016 18:06:16 -0800 Subject: [PATCH 524/978] Allow swarm join with `--availability=drain` This fix tries to address the issue raised in 24596 where it was not possible to join as manager only (`--availability=drain`). This fix adds a new flag `--availability` to `swarm join`. Related documentation has been updated. An integration test has been added. NOTE: Additional pull request for swarmkit and engine-api will be created separately. This fix fixes 24596. Signed-off-by: Yong Tang Upstream-commit: ab2635ead050ea2f283da637ed5115d7cfb7f694 Component: cli --- components/cli/command/swarm/join.go | 21 ++++++++++++++++++--- components/cli/command/swarm/opts.go | 1 + 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/components/cli/command/swarm/join.go b/components/cli/command/swarm/join.go index 3ea1462df4..40fc5c192f 100644 --- a/components/cli/command/swarm/join.go +++ b/components/cli/command/swarm/join.go @@ -2,12 +2,15 @@ package swarm import ( "fmt" + "strings" + + "golang.org/x/net/context" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/spf13/cobra" - "golang.org/x/net/context" + "github.com/spf13/pflag" ) type joinOptions struct { @@ -16,6 +19,7 @@ type joinOptions struct { // Not a NodeAddrOption because it has no default port. advertiseAddr string token string + availability string } func newJoinCommand(dockerCli command.Cli) *cobra.Command { @@ -29,7 +33,7 @@ func newJoinCommand(dockerCli command.Cli) *cobra.Command { Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.remote = args[0] - return runJoin(dockerCli, opts) + return runJoin(dockerCli, cmd.Flags(), opts) }, } @@ -37,10 +41,11 @@ func newJoinCommand(dockerCli command.Cli) *cobra.Command { flags.Var(&opts.listenAddr, flagListenAddr, "Listen address (format: [:port])") flags.StringVar(&opts.advertiseAddr, flagAdvertiseAddr, "", "Advertised address (format: [:port])") flags.StringVar(&opts.token, flagToken, "", "Token for entry into the swarm") + flags.StringVar(&opts.availability, flagAvailability, "active", "Availability of the node (active/pause/drain)") return cmd } -func runJoin(dockerCli command.Cli, opts joinOptions) error { +func runJoin(dockerCli command.Cli, flags *pflag.FlagSet, opts joinOptions) error { client := dockerCli.Client() ctx := context.Background() @@ -50,6 +55,16 @@ func runJoin(dockerCli command.Cli, opts joinOptions) error { AdvertiseAddr: opts.advertiseAddr, RemoteAddrs: []string{opts.remote}, } + if flags.Changed(flagAvailability) { + availability := swarm.NodeAvailability(strings.ToLower(opts.availability)) + switch availability { + case swarm.NodeAvailabilityActive, swarm.NodeAvailabilityPause, swarm.NodeAvailabilityDrain: + req.Availability = availability + default: + return fmt.Errorf("invalid availability %q, only active, pause and drain are supported", opts.availability) + } + } + err := client.SwarmJoin(ctx, req) if err != nil { return err diff --git a/components/cli/command/swarm/opts.go b/components/cli/command/swarm/opts.go index 9db46dcf55..40f88a4412 100644 --- a/components/cli/command/swarm/opts.go +++ b/components/cli/command/swarm/opts.go @@ -28,6 +28,7 @@ const ( flagSnapshotInterval = "snapshot-interval" flagLockKey = "lock-key" flagAutolock = "autolock" + flagAvailability = "availability" ) type swarmOptions struct { From c4a2b1cead108826ab4897de83e8595c162967f1 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 21 Dec 2016 18:13:31 -0800 Subject: [PATCH 525/978] Allow swarm init with `--availability=drain` This fix adds a new flag `--availability` to `swarm join`. Related documentation has been updated. An integration test has been added. Signed-off-by: Yong Tang Upstream-commit: 3c47987838fc272a493237b8e11301a99f4412ad Component: cli --- components/cli/command/swarm/init.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/components/cli/command/swarm/init.go b/components/cli/command/swarm/init.go index e038ac62a5..b796022672 100644 --- a/components/cli/command/swarm/init.go +++ b/components/cli/command/swarm/init.go @@ -20,6 +20,7 @@ type initOptions struct { // Not a NodeAddrOption because it has no default port. advertiseAddr string forceNewCluster bool + availability string } func newInitCommand(dockerCli command.Cli) *cobra.Command { @@ -41,6 +42,7 @@ func newInitCommand(dockerCli command.Cli) *cobra.Command { flags.StringVar(&opts.advertiseAddr, flagAdvertiseAddr, "", "Advertised address (format: [:port])") flags.BoolVar(&opts.forceNewCluster, "force-new-cluster", false, "Force create a new cluster from current state") flags.BoolVar(&opts.autolock, flagAutolock, false, "Enable manager autolocking (requiring an unlock key to start a stopped manager)") + flags.StringVar(&opts.availability, flagAvailability, "active", "Availability of the node (active/pause/drain)") addSwarmFlags(flags, &opts.swarmOptions) return cmd } @@ -56,6 +58,15 @@ func runInit(dockerCli command.Cli, flags *pflag.FlagSet, opts initOptions) erro Spec: opts.swarmOptions.ToSpec(flags), AutoLockManagers: opts.swarmOptions.autolock, } + if flags.Changed(flagAvailability) { + availability := swarm.NodeAvailability(strings.ToLower(opts.availability)) + switch availability { + case swarm.NodeAvailabilityActive, swarm.NodeAvailabilityPause, swarm.NodeAvailabilityDrain: + req.Availability = availability + default: + return fmt.Errorf("invalid availability %q, only active, pause and drain are supported", opts.availability) + } + } nodeID, err := client.SwarmInit(ctx, req) if err != nil { From 711f69b565cfb4a3f7276ac87ca9af37b3d95182 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 11 Jan 2017 11:57:24 -0500 Subject: [PATCH 526/978] Improve the error message for extends in stack deploy. Signed-off-by: Daniel Nephin Upstream-commit: 384611596b47e6c8f958dd3ca644b6d10db9a1db Component: cli --- components/cli/compose/types/types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/compose/types/types.go b/components/cli/compose/types/types.go index 45923b3460..5244bd1163 100644 --- a/components/cli/compose/types/types.go +++ b/components/cli/compose/types/types.go @@ -38,7 +38,7 @@ var DeprecatedProperties = map[string]string{ // ForbiddenProperties that are not supported in this implementation of the // compose file. var ForbiddenProperties = map[string]string{ - "extends": "Support for `extends` is not implemented yet. Use `docker-compose config` to generate a configuration with all `extends` options resolved, and deploy from that.", + "extends": "Support for `extends` is not implemented yet.", "volume_driver": "Instead of setting the volume driver on the service, define a volume using the top-level `volumes` option and specify the driver there.", "volumes_from": "To share a volume between services, define it using the top-level `volumes` option and reference it from each service that shares it using the service-level `volumes` option.", "cpu_quota": "Set resource limits using deploy.resources", From 5c20fde4a3f91c33c856542b7e8a50f8bd1f0440 Mon Sep 17 00:00:00 2001 From: Kenfe-Mickael Laventure Date: Wed, 11 Jan 2017 13:42:49 -0800 Subject: [PATCH 527/978] Fix ImageSummary.Size value The prune PR changed the meaning of the file to mean "space on disk only unique to this image", this PR revert this change. Signed-off-by: Kenfe-Mickael Laventure Upstream-commit: ce209504224356839fc42e62171f45b405f9a35b Component: cli --- components/cli/command/formatter/image.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/components/cli/command/formatter/image.go b/components/cli/command/formatter/image.go index 594b2f3926..5c7de826f0 100644 --- a/components/cli/command/formatter/image.go +++ b/components/cli/command/formatter/image.go @@ -226,8 +226,7 @@ func (c *imageContext) CreatedAt() string { func (c *imageContext) Size() string { c.AddHeader(sizeHeader) - //NOTE: For backward compatibility we need to return VirtualSize - return units.HumanSizeWithPrecision(float64(c.i.VirtualSize), 3) + return units.HumanSizeWithPrecision(float64(c.i.Size), 3) } func (c *imageContext) Containers() string { @@ -253,8 +252,8 @@ func (c *imageContext) SharedSize() string { func (c *imageContext) UniqueSize() string { c.AddHeader(uniqueSizeHeader) - if c.i.Size == -1 { + if c.i.VirtualSize == -1 || c.i.SharedSize == -1 { return "N/A" } - return units.HumanSize(float64(c.i.Size)) + return units.HumanSize(float64(c.i.VirtualSize - c.i.SharedSize)) } From df1baac4fc366fa941d3fcacc8ed3316eaecc8dc Mon Sep 17 00:00:00 2001 From: Daehyeok Mun Date: Tue, 29 Nov 2016 01:17:35 -0700 Subject: [PATCH 528/978] Refactoring ineffectual assignments This patch fixed below 4 types of code line 1. Remove unnecessary variable assignment 2. Use variables declaration instead of explicit initial zero value 3. Change variable name to underbar when variable not used 4. Add erro check and return for ignored error Signed-off-by: Daehyeok Mun Upstream-commit: 266900235c3ef24ca92b719c1057f1a81e0c7265 Component: cli --- components/cli/command/container/stats_helpers.go | 13 +++++-------- components/cli/command/plugin/create.go | 4 +++- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/components/cli/command/container/stats_helpers.go b/components/cli/command/container/stats_helpers.go index 4b57e3fe05..8eb7da0fd7 100644 --- a/components/cli/command/container/stats_helpers.go +++ b/components/cli/command/container/stats_helpers.go @@ -81,14 +81,11 @@ func collect(ctx context.Context, s *formatter.ContainerStats, cli client.APICli go func() { for { var ( - v *types.StatsJSON - memPercent = 0.0 - cpuPercent = 0.0 - blkRead, blkWrite uint64 // Only used on Linux - mem = 0.0 - memLimit = 0.0 - memPerc = 0.0 - pidsStatsCurrent uint64 + v *types.StatsJSON + memPercent, cpuPercent float64 + blkRead, blkWrite uint64 // Only used on Linux + mem, memLimit, memPerc float64 + pidsStatsCurrent uint64 ) if err := dec.Decode(&v); err != nil { diff --git a/components/cli/command/plugin/create.go b/components/cli/command/plugin/create.go index 2aab1e9e4a..82d17af48c 100644 --- a/components/cli/command/plugin/create.go +++ b/components/cli/command/plugin/create.go @@ -41,7 +41,9 @@ func validateConfig(path string) error { // validateContextDir validates the given dir and returns abs path on success. func validateContextDir(contextDir string) (string, error) { absContextDir, err := filepath.Abs(contextDir) - + if err != nil { + return "", err + } stat, err := os.Lstat(absContextDir) if err != nil { return "", err From 4588b337cdd97c612503c78b375139abb8e06e72 Mon Sep 17 00:00:00 2001 From: Josh Hawn Date: Tue, 22 Nov 2016 11:03:23 -0800 Subject: [PATCH 529/978] 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) Upstream-commit: acda56d47c83d896abca1a0d41f7f7473949f181 Component: cli --- components/cli/command/service/update_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/components/cli/command/service/update_test.go b/components/cli/command/service/update_test.go index a6df6b985e..bb931929c0 100644 --- a/components/cli/command/service/update_test.go +++ b/components/cli/command/service/update_test.go @@ -401,6 +401,9 @@ func (s secretAPIClientMock) SecretRemove(ctx context.Context, id string) error func (s secretAPIClientMock) SecretInspectWithRaw(ctx context.Context, name string) (swarm.Secret, []byte, error) { return swarm.Secret{}, []byte{}, nil } +func (s secretAPIClientMock) SecretUpdate(ctx context.Context, id string, version swarm.Version, secret swarm.SecretSpec) error { + return nil +} // TestUpdateSecretUpdateInPlace tests the ability to update the "target" of an secret with "docker service update" // by combining "--secret-rm" and "--secret-add" for the same secret. From 755844d0cfae2f3bad7178754bf605b1a058ef71 Mon Sep 17 00:00:00 2001 From: Josh Hawn Date: Tue, 22 Nov 2016 11:03:23 -0800 Subject: [PATCH 530/978] 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) Upstream-commit: 266db2ecda45a51c0b63673d4b227b352ad98607 Component: cli --- components/cli/interface.go | 1 + components/cli/secret_update.go | 19 +++++++++++ components/cli/secret_update_test.go | 49 ++++++++++++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 components/cli/secret_update.go create mode 100644 components/cli/secret_update_test.go diff --git a/components/cli/interface.go b/components/cli/interface.go index 00b9adea32..924b22bc04 100644 --- a/components/cli/interface.go +++ b/components/cli/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/components/cli/secret_update.go b/components/cli/secret_update.go new file mode 100644 index 0000000000..b94e24aab0 --- /dev/null +++ b/components/cli/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/components/cli/secret_update_test.go b/components/cli/secret_update_test.go new file mode 100644 index 0000000000..c620985bd5 --- /dev/null +++ b/components/cli/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 152258c49f86031440e5e7c6451307ac8e9ae31c Mon Sep 17 00:00:00 2001 From: Tony Abboud Date: Thu, 12 Jan 2017 12:01:29 -0500 Subject: [PATCH 531/978] Add error checking for hostPort range This fix catches the case where there is a single container port and a dynamic host port and will fail out gracefully Example docker-compose.yml snippet: port: ports: - "8091-8093:8091" - "80:8080" Signed-off-by: Tony Abboud Upstream-commit: 74c29fde046149401a6b572b06c465b4d565a315 Component: cli --- components/cli/compose/convert/service.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/components/cli/compose/convert/service.go b/components/cli/compose/convert/service.go index 37f3ece403..a245987c8f 100644 --- a/components/cli/compose/convert/service.go +++ b/components/cli/compose/convert/service.go @@ -301,9 +301,11 @@ func convertEndpointSpec(source []string) (*swarm.EndpointSpec, error) { } for port := range ports { - portConfigs = append( - portConfigs, - opts.ConvertPortToPortConfig(port, portBindings)...) + portConfig, err := opts.ConvertPortToPortConfig(port, portBindings) + if err != nil { + return nil, err + } + portConfigs = append(portConfigs, portConfig...) } return &swarm.EndpointSpec{Ports: portConfigs}, nil From 79ea6e48d776973f9aaced8d56e6390570f4532b Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Fri, 13 Jan 2017 01:05:39 +0100 Subject: [PATCH 532/978] 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 Upstream-commit: 35de37289bbf17265b1e6a8ccd7d91eff4087878 Component: cli --- components/cli/command/container/opts.go | 4 ++++ components/cli/command/container/opts_test.go | 8 ++++++++ components/cli/command/container/run.go | 12 ++++-------- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/components/cli/command/container/opts.go b/components/cli/command/container/opts.go index c5fc152168..55cc3c3b29 100644 --- a/components/cli/command/container/opts.go +++ b/components/cli/command/container/opts.go @@ -622,6 +622,10 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*container.Config, *c Runtime: copts.runtime, } + if copts.autoRemove && !hostConfig.RestartPolicy.IsNone() { + return nil, nil, nil, fmt.Errorf("Conflicting options: --restart and --rm") + } + // only set this value if the user provided the flag, else it should default to nil if flags.Changed("init") { hostConfig.Init = &copts.init diff --git a/components/cli/command/container/opts_test.go b/components/cli/command/container/opts_test.go index d02a0f7bfc..ce3bb21b41 100644 --- a/components/cli/command/container/opts_test.go +++ b/components/cli/command/container/opts_test.go @@ -456,6 +456,14 @@ func TestParseRestartPolicy(t *testing.T) { } } +func TestParseRestartPolicyAutoRemove(t *testing.T) { + expected := "Conflicting options: --restart and --rm" + _, _, _, err := parseRun([]string{"--rm", "--restart=always", "img", "cmd"}) + if err == nil || err.Error() != expected { + t.Fatalf("Expected error %v, but got none", expected) + } +} + func TestParseHealth(t *testing.T) { checkOk := func(args ...string) *container.HealthConfig { config, _, _, err := parseRun(args) diff --git a/components/cli/command/container/run.go b/components/cli/command/container/run.go index 0f8da3fa4e..4d85ee77ac 100644 --- a/components/cli/command/container/run.go +++ b/components/cli/command/container/run.go @@ -73,9 +73,8 @@ func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions cmdPath := "run" var ( - flAttach *opttypes.ListOpts - ErrConflictAttachDetach = errors.New("Conflicting options: -a and -d") - ErrConflictRestartPolicyAndAutoRemove = errors.New("Conflicting options: --restart and --rm") + flAttach *opttypes.ListOpts + ErrConflictAttachDetach = errors.New("Conflicting options: -a and -d") ) config, hostConfig, networkingConfig, err := parse(flags, copts) @@ -86,9 +85,6 @@ func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions return cli.StatusError{StatusCode: 125} } - if hostConfig.AutoRemove && !hostConfig.RestartPolicy.IsNone() { - return ErrConflictRestartPolicyAndAutoRemove - } if hostConfig.OomKillDisable != nil && *hostConfig.OomKillDisable && hostConfig.Memory == 0 { fmt.Fprintln(stderr, "WARNING: Disabling the OOM killer on containers without setting a '-m/--memory' limit may be dangerous.") } @@ -209,7 +205,7 @@ func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions }) } - statusChan := waitExitOrRemoved(ctx, dockerCli, createResponse.ID, hostConfig.AutoRemove) + statusChan := waitExitOrRemoved(ctx, dockerCli, createResponse.ID, copts.autoRemove) //start the container if err := client.ContainerStart(ctx, createResponse.ID, types.ContainerStartOptions{}); err != nil { @@ -222,7 +218,7 @@ func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions } reportError(stderr, cmdPath, err.Error(), false) - if hostConfig.AutoRemove { + if copts.autoRemove { // wait container to be removed <-statusChan } From 90947b7653e96bca76c8310ef6b263a03aa07c89 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Fri, 13 Jan 2017 01:05:39 +0100 Subject: [PATCH 533/978] 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 Upstream-commit: 36315fa14b72a6e51ee072271f78d22f790b0d25 Component: cli --- components/cli/container_create.go | 6 ++++ components/cli/container_create_test.go | 42 +++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/components/cli/container_create.go b/components/cli/container_create.go index 9f627aafa6..6841b0b282 100644 --- a/components/cli/container_create.go +++ b/components/cli/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/components/cli/container_create_test.go b/components/cli/container_create_test.go index 15dbd5ea01..73474cf56f 100644 --- a/components/cli/container_create_test.go +++ b/components/cli/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 1daa5daff5d809d272f0006a34ee7ec3ad500fc8 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 16 Jan 2017 17:57:26 +0100 Subject: [PATCH 534/978] Add version annotation to various flags added in 1.13 Pull request https://github.com/docker/docker/pull/27745 added support for the client to talk to older versions of the daemon. Various flags were added to docker 1.13 that are not compatible with older daemons. This PR adds annotations to those flags, so that they are automatically hidden if the daemon is older than docker 1.13 (API 1.25). Not all new flags affect the API (some are client-side only). The following PR's added new flags to docker 1.13 that affect the API; - https://github.com/docker/docker/pull/23430 added `--cpu-rt-period`and `--cpu-rt-runtime` - https://github.com/docker/docker/pull/27800 / https://github.com/docker/docker/pull/25317 added `--group` / `--group-add` / `--group-rm` - https://github.com/docker/docker/pull/27702 added `--network` to `docker build` - https://github.com/docker/docker/pull/25962 added `--attachable` to `docker network create` - https://github.com/docker/docker/pull/27998 added `--compose-file` to `docker stack deploy` - https://github.com/docker/docker/pull/22566 added `--stop-timeout` to `docker run` and `docker create` - https://github.com/docker/docker/pull/26061 added `--init` to `docker run` and `docker create` - https://github.com/docker/docker/pull/26941 added `--init-path` to `docker run` and `docker create` - https://github.com/docker/docker/pull/27958 added `--cpus` on `docker run` / `docker create` - https://github.com/docker/docker/pull/27567 added `--dns`, `--dns-opt`, and `--dns-search` to `docker service create` - https://github.com/docker/docker/pull/27596 added `--force` to `docker service update` - https://github.com/docker/docker/pull/27857 added `--hostname` to `docker service create` - https://github.com/docker/docker/pull/28031 added `--hosts`, `--host-add` / `--host-rm` to `docker service create` and `docker service update` - https://github.com/docker/docker/pull/28076 added `--tty` on `docker service create` / `docker service update` - https://github.com/docker/docker/pull/26421 added `--update-max-failure-ratio`, `--update-monitor` and `--rollback` on `docker service update` - https://github.com/docker/docker/pull/27369 added `--health-cmd`, `--health-interval`, `--health-retries`, `--health-timeout` and `--no-healthcheck` options to `docker service create` and `docker service update` Signed-off-by: Sebastiaan van Stijn Upstream-commit: 5d2722f83db9e301c6dcbe1c562c2051a52905db Component: cli --- components/cli/command/container/opts.go | 5 +++++ components/cli/command/container/update.go | 2 ++ components/cli/command/image/build.go | 1 + components/cli/command/network/create.go | 1 + components/cli/command/service/create.go | 6 ++++++ components/cli/command/service/opts.go | 9 +++++++++ components/cli/command/service/update.go | 14 ++++++++++++++ components/cli/command/stack/opts.go | 1 + components/cli/command/swarm/opts.go | 2 ++ 9 files changed, 41 insertions(+) diff --git a/components/cli/command/container/opts.go b/components/cli/command/container/opts.go index 55cc3c3b29..9896323bef 100644 --- a/components/cli/command/container/opts.go +++ b/components/cli/command/container/opts.go @@ -236,9 +236,12 @@ func addFlags(flags *pflag.FlagSet) *containerOptions { flags.Int64Var(&copts.cpuPeriod, "cpu-period", 0, "Limit CPU CFS (Completely Fair Scheduler) period") flags.Int64Var(&copts.cpuQuota, "cpu-quota", 0, "Limit CPU CFS (Completely Fair Scheduler) quota") flags.Int64Var(&copts.cpuRealtimePeriod, "cpu-rt-period", 0, "Limit CPU real-time period in microseconds") + flags.SetAnnotation("cpu-rt-period", "version", []string{"1.25"}) flags.Int64Var(&copts.cpuRealtimeRuntime, "cpu-rt-runtime", 0, "Limit CPU real-time runtime in microseconds") + flags.SetAnnotation("cpu-rt-runtime", "version", []string{"1.25"}) flags.Int64VarP(&copts.cpuShares, "cpu-shares", "c", 0, "CPU shares (relative weight)") flags.Var(&copts.cpus, "cpus", "Number of CPUs") + flags.SetAnnotation("cpus", "version", []string{"1.25"}) flags.Var(&copts.deviceReadBps, "device-read-bps", "Limit read rate (bytes per second) from a device") flags.Var(&copts.deviceReadIOps, "device-read-iops", "Limit read rate (IO per second) from a device") flags.Var(&copts.deviceWriteBps, "device-write-bps", "Limit write rate (bytes per second) to a device") @@ -264,7 +267,9 @@ func addFlags(flags *pflag.FlagSet) *containerOptions { flags.StringVar(&copts.runtime, "runtime", "", "Runtime to use for this container") flags.BoolVar(&copts.init, "init", false, "Run an init inside the container that forwards signals and reaps processes") + flags.SetAnnotation("init", "version", []string{"1.25"}) flags.StringVar(&copts.initPath, "init-path", "", "Path to the docker-init binary") + flags.SetAnnotation("init-path", "version", []string{"1.25"}) return copts } diff --git a/components/cli/command/container/update.go b/components/cli/command/container/update.go index 6a7cc820e9..4a1220a262 100644 --- a/components/cli/command/container/update.go +++ b/components/cli/command/container/update.go @@ -54,7 +54,9 @@ func NewUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Int64Var(&opts.cpuPeriod, "cpu-period", 0, "Limit CPU CFS (Completely Fair Scheduler) period") flags.Int64Var(&opts.cpuQuota, "cpu-quota", 0, "Limit CPU CFS (Completely Fair Scheduler) quota") flags.Int64Var(&opts.cpuRealtimePeriod, "cpu-rt-period", 0, "Limit the CPU real-time period in microseconds") + flags.SetAnnotation("cpu-rt-period", "version", []string{"1.25"}) flags.Int64Var(&opts.cpuRealtimeRuntime, "cpu-rt-runtime", 0, "Limit the CPU real-time runtime in microseconds") + flags.SetAnnotation("cpu-rt-runtime", "version", []string{"1.25"}) flags.StringVar(&opts.cpusetCpus, "cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)") flags.StringVar(&opts.cpusetMems, "cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)") flags.Int64VarP(&opts.cpuShares, "cpu-shares", "c", 0, "CPU shares (relative weight)") diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go index 5d6e611406..2bead42ec2 100644 --- a/components/cli/command/image/build.go +++ b/components/cli/command/image/build.go @@ -107,6 +107,7 @@ func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command { flags.BoolVar(&options.compress, "compress", false, "Compress the build context using gzip") flags.StringSliceVar(&options.securityOpt, "security-opt", []string{}, "Security options") flags.StringVar(&options.networkMode, "network", "default", "Set the networking mode for the RUN instructions during build") + flags.SetAnnotation("network", "version", []string{"1.25"}) command.AddTrustedFlags(flags, true) diff --git a/components/cli/command/network/create.go b/components/cli/command/network/create.go index dd5e94ea25..57c59ed053 100644 --- a/components/cli/command/network/create.go +++ b/components/cli/command/network/create.go @@ -58,6 +58,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.BoolVar(&opts.internal, "internal", false, "Restrict external access to the network") flags.BoolVar(&opts.ipv6, "ipv6", false, "Enable IPv6 networking") flags.BoolVar(&opts.attachable, "attachable", false, "Enable manual container attachment") + flags.SetAnnotation("attachable", "version", []string{"1.25"}) flags.StringVar(&opts.ipamDriver, "ipam-driver", "default", "IP Address Management Driver") flags.StringSliceVar(&opts.ipamSubnet, "subnet", []string{}, "Subnet in CIDR format that represents a network segment") diff --git a/components/cli/command/service/create.go b/components/cli/command/service/create.go index ca2bb089fd..3c82b78bc6 100644 --- a/components/cli/command/service/create.go +++ b/components/cli/command/service/create.go @@ -39,12 +39,18 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Var(&opts.constraints, flagConstraint, "Placement constraints") flags.Var(&opts.networks, flagNetwork, "Network attachments") flags.Var(&opts.secrets, flagSecret, "Specify secrets to expose to the service") + flags.SetAnnotation(flagSecret, "version", []string{"1.25"}) flags.VarP(&opts.endpoint.publishPorts, flagPublish, "p", "Publish a port as a node port") flags.Var(&opts.groups, flagGroup, "Set one or more supplementary user groups for the container") + flags.SetAnnotation(flagGroup, "version", []string{"1.25"}) flags.Var(&opts.dns, flagDNS, "Set custom DNS servers") + flags.SetAnnotation(flagDNS, "version", []string{"1.25"}) flags.Var(&opts.dnsOption, flagDNSOption, "Set DNS options") + flags.SetAnnotation(flagDNSOption, "version", []string{"1.25"}) flags.Var(&opts.dnsSearch, flagDNSSearch, "Set custom DNS search domains") + flags.SetAnnotation(flagDNSSearch, "version", []string{"1.25"}) flags.Var(&opts.hosts, flagHost, "Set one or more custom host-to-IP mappings (host:ip)") + flags.SetAnnotation(flagHost, "version", []string{"1.25"}) flags.SetInterspersed(false) return cmd diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index b794b07a30..2218890aa3 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -445,6 +445,7 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { flags.StringVarP(&opts.workdir, flagWorkdir, "w", "", "Working directory inside the container") flags.StringVarP(&opts.user, flagUser, "u", "", "Username or UID (format: [:])") flags.StringVar(&opts.hostname, flagHostname, "", "Container hostname") + flags.SetAnnotation(flagHostname, "version", []string{"1.25"}) flags.Var(&opts.resources.limitCPU, flagLimitCPU, "Limit CPUs") flags.Var(&opts.resources.limitMemBytes, flagLimitMemory, "Limit Memory") @@ -462,8 +463,10 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { flags.Uint64Var(&opts.update.parallelism, flagUpdateParallelism, 1, "Maximum number of tasks updated simultaneously (0 to update all at once)") flags.DurationVar(&opts.update.delay, flagUpdateDelay, time.Duration(0), "Delay between updates (ns|us|ms|s|m|h) (default 0s)") flags.DurationVar(&opts.update.monitor, flagUpdateMonitor, time.Duration(0), "Duration after each task update to monitor for failure (ns|us|ms|s|m|h) (default 0s)") + flags.SetAnnotation(flagUpdateMonitor, "version", []string{"1.25"}) flags.StringVar(&opts.update.onFailure, flagUpdateFailureAction, "pause", "Action on update failure (pause|continue)") flags.Var(&opts.update.maxFailureRatio, flagUpdateMaxFailureRatio, "Failure rate to tolerate during an update") + flags.SetAnnotation(flagUpdateMaxFailureRatio, "version", []string{"1.25"}) flags.StringVar(&opts.endpoint.mode, flagEndpointMode, "", "Endpoint mode (vip or dnsrr)") @@ -473,12 +476,18 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { flags.Var(&opts.logDriver.opts, flagLogOpt, "Logging driver options") flags.StringVar(&opts.healthcheck.cmd, flagHealthCmd, "", "Command to run to check health") + flags.SetAnnotation(flagHealthCmd, "version", []string{"1.25"}) flags.Var(&opts.healthcheck.interval, flagHealthInterval, "Time between running the check (ns|us|ms|s|m|h)") + flags.SetAnnotation(flagHealthInterval, "version", []string{"1.25"}) flags.Var(&opts.healthcheck.timeout, flagHealthTimeout, "Maximum time to allow one check to run (ns|us|ms|s|m|h)") + flags.SetAnnotation(flagHealthTimeout, "version", []string{"1.25"}) flags.IntVar(&opts.healthcheck.retries, flagHealthRetries, 0, "Consecutive failures needed to report unhealthy") + flags.SetAnnotation(flagHealthRetries, "version", []string{"1.25"}) flags.BoolVar(&opts.healthcheck.noHealthcheck, flagNoHealthcheck, false, "Disable any container-specified HEALTHCHECK") + flags.SetAnnotation(flagNoHealthcheck, "version", []string{"1.25"}) flags.BoolVarP(&opts.tty, flagTTY, "t", false, "Allocate a pseudo-TTY") + flags.SetAnnotation(flagTTY, "version", []string{"1.25"}) } const ( diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index df0977d86d..a33d599b69 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -38,11 +38,14 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.String("image", "", "Service image tag") flags.String("args", "", "Service command args") flags.Bool("rollback", false, "Rollback to previous specification") + flags.SetAnnotation("rollback", "version", []string{"1.25"}) flags.Bool("force", false, "Force update even if no changes require it") + flags.SetAnnotation("force", "version", []string{"1.25"}) addServiceFlags(cmd, serviceOpts) flags.Var(newListOptsVar(), flagEnvRemove, "Remove an environment variable") flags.Var(newListOptsVar(), flagGroupRemove, "Remove a previously added supplementary user group from the container") + flags.SetAnnotation(flagGroupRemove, "version", []string{"1.25"}) flags.Var(newListOptsVar(), flagLabelRemove, "Remove a label by its key") flags.Var(newListOptsVar(), flagContainerLabelRemove, "Remove a container label by its key") flags.Var(newListOptsVar(), flagMountRemove, "Remove a mount by its target path") @@ -50,22 +53,33 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Var(&opts.PortOpt{}, flagPublishRemove, "Remove a published port by its target port") flags.Var(newListOptsVar(), flagConstraintRemove, "Remove a constraint") flags.Var(newListOptsVar(), flagDNSRemove, "Remove a custom DNS server") + flags.SetAnnotation(flagDNSRemove, "version", []string{"1.25"}) flags.Var(newListOptsVar(), flagDNSOptionRemove, "Remove a DNS option") + flags.SetAnnotation(flagDNSOptionRemove, "version", []string{"1.25"}) flags.Var(newListOptsVar(), flagDNSSearchRemove, "Remove a DNS search domain") + flags.SetAnnotation(flagDNSSearchRemove, "version", []string{"1.25"}) flags.Var(newListOptsVar(), flagHostRemove, "Remove a custom host-to-IP mapping (host:ip)") + flags.SetAnnotation(flagHostRemove, "version", []string{"1.25"}) flags.Var(&serviceOpts.labels, flagLabelAdd, "Add or update a service label") flags.Var(&serviceOpts.containerLabels, flagContainerLabelAdd, "Add or update a container label") flags.Var(&serviceOpts.env, flagEnvAdd, "Add or update an environment variable") flags.Var(newListOptsVar(), flagSecretRemove, "Remove a secret") + flags.SetAnnotation(flagSecretRemove, "version", []string{"1.25"}) flags.Var(&serviceOpts.secrets, flagSecretAdd, "Add or update a secret on a service") + flags.SetAnnotation(flagSecretAdd, "version", []string{"1.25"}) flags.Var(&serviceOpts.mounts, flagMountAdd, "Add or update a mount on a service") flags.Var(&serviceOpts.constraints, flagConstraintAdd, "Add or update a placement constraint") flags.Var(&serviceOpts.endpoint.publishPorts, flagPublishAdd, "Add or update a published port") flags.Var(&serviceOpts.groups, flagGroupAdd, "Add an additional supplementary user group to the container") + flags.SetAnnotation(flagGroupAdd, "version", []string{"1.25"}) flags.Var(&serviceOpts.dns, flagDNSAdd, "Add or update a custom DNS server") + flags.SetAnnotation(flagDNSAdd, "version", []string{"1.25"}) flags.Var(&serviceOpts.dnsOption, flagDNSOptionAdd, "Add or update a DNS option") + flags.SetAnnotation(flagDNSOptionAdd, "version", []string{"1.25"}) flags.Var(&serviceOpts.dnsSearch, flagDNSSearchAdd, "Add or update a custom DNS search domain") + flags.SetAnnotation(flagDNSSearchAdd, "version", []string{"1.25"}) flags.Var(&serviceOpts.hosts, flagHostAdd, "Add or update a custom host-to-IP mapping (host:ip)") + flags.SetAnnotation(flagHostAdd, "version", []string{"1.25"}) return cmd } diff --git a/components/cli/command/stack/opts.go b/components/cli/command/stack/opts.go index 74fe4f5343..996ff68f23 100644 --- a/components/cli/command/stack/opts.go +++ b/components/cli/command/stack/opts.go @@ -11,6 +11,7 @@ import ( func addComposefileFlag(opt *string, flags *pflag.FlagSet) { flags.StringVarP(opt, "compose-file", "c", "", "Path to a Compose file") + flags.SetAnnotation("compose-file", "version", []string{"1.25"}) } func addBundlefileFlag(opt *string, flags *pflag.FlagSet) { diff --git a/components/cli/command/swarm/opts.go b/components/cli/command/swarm/opts.go index 40f88a4412..b32cc92106 100644 --- a/components/cli/command/swarm/opts.go +++ b/components/cli/command/swarm/opts.go @@ -176,7 +176,9 @@ func addSwarmFlags(flags *pflag.FlagSet, opts *swarmOptions) { flags.DurationVar(&opts.nodeCertExpiry, flagCertExpiry, time.Duration(90*24*time.Hour), "Validity period for node certificates (ns|us|ms|s|m|h)") flags.Var(&opts.externalCA, flagExternalCA, "Specifications of one or more certificate signing endpoints") flags.Uint64Var(&opts.maxSnapshots, flagMaxSnapshots, 0, "Number of additional Raft snapshots to retain") + flags.SetAnnotation(flagMaxSnapshots, "version", []string{"1.25"}) flags.Uint64Var(&opts.snapshotInterval, flagSnapshotInterval, 10000, "Number of log entries between Raft snapshots") + flags.SetAnnotation(flagSnapshotInterval, "version", []string{"1.25"}) } func (opts *swarmOptions) mergeSwarmSpec(spec *swarm.Spec, flags *pflag.FlagSet) { From 159f204ae92f7e89ddd8a045c1cb126541e8fad9 Mon Sep 17 00:00:00 2001 From: allencloud Date: Thu, 29 Dec 2016 01:09:25 +0800 Subject: [PATCH 535/978] purify error message in cli for create and run command Signed-off-by: allencloud Upstream-commit: b554a5f62572da7eac6fb5a6947001dc129393c2 Component: cli --- components/cli/command/container/run.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/components/cli/command/container/run.go b/components/cli/command/container/run.go index 4d85ee77ac..cbe64548ea 100644 --- a/components/cli/command/container/run.go +++ b/components/cli/command/container/run.go @@ -255,10 +255,11 @@ func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions // reportError is a utility method that prints a user-friendly message // containing the error that occurred during parsing and a suggestion to get help func reportError(stderr io.Writer, name string, str string, withHelp bool) { + str = strings.TrimSuffix(str, ".") + "." if withHelp { - str += ".\nSee '" + os.Args[0] + " " + name + " --help'" + str += "\nSee '" + os.Args[0] + " " + name + " --help'." } - fmt.Fprintf(stderr, "%s: %s.\n", os.Args[0], str) + fmt.Fprintf(stderr, "%s: %s\n", os.Args[0], str) } // if container start fails with 'not found'/'no such' error, return 127 From 5b6dccd76a50b771aec8b17b89d144b816422218 Mon Sep 17 00:00:00 2001 From: kaiwentan Date: Wed, 18 Jan 2017 00:26:37 +0800 Subject: [PATCH 536/978] correct all the formate to formatter Signed-off-by: kaiwentan Upstream-commit: 182bccefbe2be037846f05fab9aa36ef3f6ede0f Component: cli --- components/cli/command/formatter/disk_usage.go | 2 +- components/cli/command/formatter/image.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/command/formatter/disk_usage.go b/components/cli/command/formatter/disk_usage.go index 6f97d3b0f9..f4abac59e6 100644 --- a/components/cli/command/formatter/disk_usage.go +++ b/components/cli/command/formatter/disk_usage.go @@ -26,7 +26,7 @@ const ( uniqueSizeHeader = "UNIQUE SiZE" ) -// DiskUsageContext contains disk usage specific information required by the formater, encapsulate a Context struct. +// DiskUsageContext contains disk usage specific information required by the formatter, encapsulate a Context struct. type DiskUsageContext struct { Context Verbose bool diff --git a/components/cli/command/formatter/image.go b/components/cli/command/formatter/image.go index 5c7de826f0..9187dfb2e2 100644 --- a/components/cli/command/formatter/image.go +++ b/components/cli/command/formatter/image.go @@ -20,7 +20,7 @@ const ( digestHeader = "DIGEST" ) -// ImageContext contains image specific information required by the formater, encapsulate a Context struct. +// ImageContext contains image specific information required by the formatter, encapsulate a Context struct. type ImageContext struct { Context Digest bool From eca2ee10e8469832a9ac64e8ef651227ee4b3d6c Mon Sep 17 00:00:00 2001 From: allencloud Date: Sun, 8 Jan 2017 22:23:36 +0800 Subject: [PATCH 537/978] return error when listNode fails Signed-off-by: allencloud Upstream-commit: ca5bd1c10677c572509d88dbdf63a98beba09952 Component: cli --- components/cli/command/system/info.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/system/info.go b/components/cli/command/system/info.go index ec1cf47de2..d9fafd1aa1 100644 --- a/components/cli/command/system/info.go +++ b/components/cli/command/system/info.go @@ -103,7 +103,7 @@ func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error { fmt.Fprintf(dockerCli.Out(), " Error: %v\n", info.Swarm.Error) } fmt.Fprintf(dockerCli.Out(), " Is Manager: %v\n", info.Swarm.ControlAvailable) - if info.Swarm.ControlAvailable { + if info.Swarm.ControlAvailable && info.Swarm.Error == "" && info.Swarm.LocalNodeState != swarm.LocalNodeStateError { fmt.Fprintf(dockerCli.Out(), " ClusterID: %s\n", info.Swarm.Cluster.ID) fmt.Fprintf(dockerCli.Out(), " Managers: %d\n", info.Swarm.Managers) fmt.Fprintf(dockerCli.Out(), " Nodes: %d\n", info.Swarm.Nodes) From c02ff6e41e768dea24d45d325a948fff1d7301dc Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 19 Jan 2017 09:50:28 +0000 Subject: [PATCH 538/978] Fix broken JSON support in cli/command/formatter How to test (it should not print `{}`, and just returns JSON with the actual data): $ docker images --format '{{json .}}' $ docker container stats --format '{{json .}}' Signed-off-by: Akihiro Suda Upstream-commit: 775d9759c6b669de7a841a8497dcc7e26ab77e8a Component: cli --- components/cli/command/formatter/disk_usage.go | 12 ++++++++++++ components/cli/command/formatter/image.go | 4 ++++ components/cli/command/formatter/stats.go | 4 ++++ 3 files changed, 20 insertions(+) diff --git a/components/cli/command/formatter/disk_usage.go b/components/cli/command/formatter/disk_usage.go index f4abac59e6..ff1ab768c0 100644 --- a/components/cli/command/formatter/disk_usage.go +++ b/components/cli/command/formatter/disk_usage.go @@ -158,6 +158,10 @@ type diskUsageImagesContext struct { images []*types.ImageSummary } +func (c *diskUsageImagesContext) MarshalJSON() ([]byte, error) { + return marshalJSON(c) +} + func (c *diskUsageImagesContext) Type() string { c.AddHeader(typeHeader) return "Images" @@ -209,6 +213,10 @@ type diskUsageContainersContext struct { containers []*types.Container } +func (c *diskUsageContainersContext) MarshalJSON() ([]byte, error) { + return marshalJSON(c) +} + func (c *diskUsageContainersContext) Type() string { c.AddHeader(typeHeader) return "Containers" @@ -273,6 +281,10 @@ type diskUsageVolumesContext struct { volumes []*types.Volume } +func (c *diskUsageVolumesContext) MarshalJSON() ([]byte, error) { + return marshalJSON(c) +} + func (c *diskUsageVolumesContext) Type() string { c.AddHeader(typeHeader) return "Local Volumes" diff --git a/components/cli/command/formatter/image.go b/components/cli/command/formatter/image.go index 9187dfb2e2..3dbb1b9642 100644 --- a/components/cli/command/formatter/image.go +++ b/components/cli/command/formatter/image.go @@ -190,6 +190,10 @@ type imageContext struct { digest string } +func (c *imageContext) MarshalJSON() ([]byte, error) { + return marshalJSON(c) +} + func (c *imageContext) ID() string { c.AddHeader(imageIDHeader) if c.trunc { diff --git a/components/cli/command/formatter/stats.go b/components/cli/command/formatter/stats.go index 7997f996d8..a37e9d7923 100644 --- a/components/cli/command/formatter/stats.go +++ b/components/cli/command/formatter/stats.go @@ -138,6 +138,10 @@ type containerStatsContext struct { s StatsEntry } +func (c *containerStatsContext) MarshalJSON() ([]byte, error) { + return marshalJSON(c) +} + func (c *containerStatsContext) Container() string { c.AddHeader(containerHeader) return c.s.Container From 82428da3c9861acdfa685f211817c39579f32172 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Wed, 11 Jan 2017 13:54:52 -0800 Subject: [PATCH 539/978] Remove use of forked reference package for cli Use resolving to repo info as the split point between the legitimate reference package and forked reference package. Signed-off-by: Derek McGowan (github: dmcgowan) Upstream-commit: bbc4ac69fa3f5de90e3e8602fbfd60fbce4f2ca6 Component: cli --- components/cli/command/container/create.go | 37 ++++++++------- components/cli/command/formatter/image.go | 12 ++--- components/cli/command/image/build.go | 13 +++--- components/cli/command/image/pull.go | 12 ++--- components/cli/command/image/push.go | 6 +-- components/cli/command/image/trust.go | 39 ++++++++-------- components/cli/command/plugin/create.go | 4 +- components/cli/command/plugin/install.go | 52 +++++++++------------- components/cli/command/plugin/push.go | 16 ++++--- components/cli/command/registry.go | 4 +- components/cli/command/service/trust.go | 48 ++++++++------------ 11 files changed, 119 insertions(+), 124 deletions(-) diff --git a/components/cli/command/container/create.go b/components/cli/command/container/create.go index 13890d9ef5..01d7815c92 100644 --- a/components/cli/command/container/create.go +++ b/components/cli/command/container/create.go @@ -5,6 +5,7 @@ import ( "io" "os" + "github.com/docker/distribution/reference" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" networktypes "github.com/docker/docker/api/types/network" @@ -13,8 +14,6 @@ import ( "github.com/docker/docker/cli/command/image" apiclient "github.com/docker/docker/client" "github.com/docker/docker/pkg/jsonmessage" - // FIXME migrate to docker/distribution/reference - "github.com/docker/docker/reference" "github.com/docker/docker/registry" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -72,7 +71,7 @@ func runCreate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *createO } func pullImage(ctx context.Context, dockerCli *command.DockerCli, image string, out io.Writer) error { - ref, err := reference.ParseNamed(image) + ref, err := reference.ParseNormalizedNamed(image) if err != nil { return err } @@ -150,7 +149,12 @@ func newCIDFile(path string) (*cidFile, error) { func createContainer(ctx context.Context, dockerCli *command.DockerCli, config *container.Config, hostConfig *container.HostConfig, networkingConfig *networktypes.NetworkingConfig, cidfile, name string) (*container.ContainerCreateCreatedBody, error) { stderr := dockerCli.Err() - var containerIDFile *cidFile + var ( + containerIDFile *cidFile + trustedRef reference.Canonical + namedRef reference.Named + ) + if cidfile != "" { var err error if containerIDFile, err = newCIDFile(cidfile); err != nil { @@ -159,21 +163,24 @@ func createContainer(ctx context.Context, dockerCli *command.DockerCli, config * defer containerIDFile.Close() } - var trustedRef reference.Canonical - _, ref, err := reference.ParseIDOrReference(config.Image) + ref, err := reference.ParseAnyReference(config.Image) if err != nil { return nil, err } - if ref != nil { - ref = reference.WithDefaultTag(ref) + if named, ok := ref.(reference.Named); ok { + if reference.IsNameOnly(named) { + namedRef = reference.EnsureTagged(named) + } else { + namedRef = named + } - if ref, ok := ref.(reference.NamedTagged); ok && command.IsTrusted() { + if taggedRef, ok := namedRef.(reference.NamedTagged); ok && command.IsTrusted() { var err error - trustedRef, err = image.TrustedReference(ctx, dockerCli, ref, nil) + trustedRef, err = image.TrustedReference(ctx, dockerCli, taggedRef, nil) if err != nil { return nil, err } - config.Image = trustedRef.String() + config.Image = reference.FamiliarString(trustedRef) } } @@ -182,15 +189,15 @@ func createContainer(ctx context.Context, dockerCli *command.DockerCli, config * //if image not found try to pull it if err != nil { - if apiclient.IsErrImageNotFound(err) && ref != nil { - fmt.Fprintf(stderr, "Unable to find image '%s' locally\n", ref.String()) + if apiclient.IsErrImageNotFound(err) && namedRef != nil { + fmt.Fprintf(stderr, "Unable to find image '%s' locally\n", reference.FamiliarString(namedRef)) // we don't want to write to stdout anything apart from container.ID if err = pullImage(ctx, dockerCli, config.Image, stderr); err != nil { return nil, err } - if ref, ok := ref.(reference.NamedTagged); ok && trustedRef != nil { - if err := image.TagTrusted(ctx, dockerCli, trustedRef, ref); err != nil { + if taggedRef, ok := namedRef.(reference.NamedTagged); ok && trustedRef != nil { + if err := image.TagTrusted(ctx, dockerCli, trustedRef, taggedRef); err != nil { return nil, err } } diff --git a/components/cli/command/formatter/image.go b/components/cli/command/formatter/image.go index 9187dfb2e2..fc0168cf72 100644 --- a/components/cli/command/formatter/image.go +++ b/components/cli/command/formatter/image.go @@ -4,9 +4,9 @@ import ( "fmt" "time" + "github.com/docker/distribution/reference" "github.com/docker/docker/api/types" "github.com/docker/docker/pkg/stringid" - "github.com/docker/docker/reference" units "github.com/docker/go-units" ) @@ -95,21 +95,23 @@ func imageFormat(ctx ImageContext, images []types.ImageSummary, format func(subC repoDigests := map[string][]string{} for _, refString := range append(image.RepoTags) { - ref, err := reference.ParseNamed(refString) + ref, err := reference.ParseNormalizedNamed(refString) if err != nil { continue } if nt, ok := ref.(reference.NamedTagged); ok { - repoTags[ref.Name()] = append(repoTags[ref.Name()], nt.Tag()) + familiarRef := reference.FamiliarName(ref) + repoTags[familiarRef] = append(repoTags[familiarRef], nt.Tag()) } } for _, refString := range append(image.RepoDigests) { - ref, err := reference.ParseNamed(refString) + ref, err := reference.ParseNormalizedNamed(refString) if err != nil { continue } if c, ok := ref.(reference.Canonical); ok { - repoDigests[ref.Name()] = append(repoDigests[ref.Name()], c.Digest().String()) + familiarRef := reference.FamiliarName(ref) + repoDigests[familiarRef] = append(repoDigests[familiarRef], c.Digest().String()) } } diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go index 5d6e611406..ecc6861701 100644 --- a/components/cli/command/image/build.go +++ b/components/cli/command/image/build.go @@ -11,6 +11,7 @@ import ( "regexp" "runtime" + "github.com/docker/distribution/reference" "github.com/docker/docker/api" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" @@ -25,7 +26,6 @@ import ( "github.com/docker/docker/pkg/progress" "github.com/docker/docker/pkg/streamformatter" "github.com/docker/docker/pkg/urlutil" - "github.com/docker/docker/reference" runconfigopts "github.com/docker/docker/runconfig/opts" units "github.com/docker/go-units" "github.com/spf13/cobra" @@ -360,7 +360,7 @@ type translatorFunc func(context.Context, reference.NamedTagged) (reference.Cano // validateTag checks if the given image name can be resolved. func validateTag(rawRepo string) (string, error) { - _, err := reference.ParseNamed(rawRepo) + _, err := reference.ParseNormalizedNamed(rawRepo) if err != nil { return "", err } @@ -392,18 +392,21 @@ func rewriteDockerfileFrom(ctx context.Context, dockerfile io.Reader, translator matches := dockerfileFromLinePattern.FindStringSubmatch(line) if matches != nil && matches[1] != api.NoBaseImageSpecifier { // Replace the line with a resolved "FROM repo@digest" - ref, err := reference.ParseNamed(matches[1]) + var ref reference.Named + ref, err = reference.ParseNormalizedNamed(matches[1]) if err != nil { return nil, nil, err } - ref = reference.WithDefaultTag(ref) + if reference.IsNameOnly(ref) { + ref = reference.EnsureTagged(ref) + } if ref, ok := ref.(reference.NamedTagged); ok && command.IsTrusted() { trustedRef, err := translator(ctx, ref) if err != nil { return nil, nil, err } - line = dockerfileFromLinePattern.ReplaceAllLiteralString(line, fmt.Sprintf("FROM %s", trustedRef.String())) + line = dockerfileFromLinePattern.ReplaceAllLiteralString(line, fmt.Sprintf("FROM %s", reference.FamiliarString(trustedRef))) resolvedTags = append(resolvedTags, &resolvedTag{ digestRef: trustedRef, tagRef: ref, diff --git a/components/cli/command/image/pull.go b/components/cli/command/image/pull.go index 24933fe846..d5aa3eefbd 100644 --- a/components/cli/command/image/pull.go +++ b/components/cli/command/image/pull.go @@ -7,9 +7,9 @@ import ( "golang.org/x/net/context" + "github.com/docker/distribution/reference" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" - "github.com/docker/docker/reference" "github.com/docker/docker/registry" "github.com/spf13/cobra" ) @@ -42,7 +42,8 @@ func NewPullCommand(dockerCli *command.DockerCli) *cobra.Command { } func runPull(dockerCli *command.DockerCli, opts pullOptions) error { - distributionRef, err := reference.ParseNamed(opts.remote) + var distributionRef reference.Named + distributionRef, err := reference.ParseNormalizedNamed(opts.remote) if err != nil { return err } @@ -51,8 +52,9 @@ func runPull(dockerCli *command.DockerCli, opts pullOptions) error { } if !opts.all && reference.IsNameOnly(distributionRef) { - distributionRef = reference.WithDefaultTag(distributionRef) - fmt.Fprintf(dockerCli.Out(), "Using default tag: %s\n", reference.DefaultTag) + taggedRef := reference.EnsureTagged(distributionRef) + fmt.Fprintf(dockerCli.Out(), "Using default tag: %s\n", taggedRef.Tag()) + distributionRef = taggedRef } // Resolve the Repository name from fqn to RepositoryInfo @@ -71,7 +73,7 @@ func runPull(dockerCli *command.DockerCli, opts pullOptions) error { if command.IsTrusted() && !isCanonical { err = trustedPull(ctx, dockerCli, repoInfo, distributionRef, authConfig, requestPrivilege) } else { - err = imagePullPrivileged(ctx, dockerCli, authConfig, distributionRef.String(), requestPrivilege, opts.all) + err = imagePullPrivileged(ctx, dockerCli, authConfig, reference.FamiliarString(distributionRef), requestPrivilege, opts.all) } if err != nil { if strings.Contains(err.Error(), "target is plugin") { diff --git a/components/cli/command/image/push.go b/components/cli/command/image/push.go index a8ce4945ec..7972718e61 100644 --- a/components/cli/command/image/push.go +++ b/components/cli/command/image/push.go @@ -3,10 +3,10 @@ package image import ( "golang.org/x/net/context" + "github.com/docker/distribution/reference" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/pkg/jsonmessage" - "github.com/docker/docker/reference" "github.com/docker/docker/registry" "github.com/spf13/cobra" ) @@ -30,7 +30,7 @@ func NewPushCommand(dockerCli *command.DockerCli) *cobra.Command { } func runPush(dockerCli *command.DockerCli, remote string) error { - ref, err := reference.ParseNamed(remote) + ref, err := reference.ParseNormalizedNamed(remote) if err != nil { return err } @@ -51,7 +51,7 @@ func runPush(dockerCli *command.DockerCli, remote string) error { return trustedPush(ctx, dockerCli, repoInfo, ref, authConfig, requestPrivilege) } - responseBody, err := imagePushPrivileged(ctx, dockerCli, authConfig, ref.String(), requestPrivilege) + responseBody, err := imagePushPrivileged(ctx, dockerCli, authConfig, ref, requestPrivilege) if err != nil { return err } diff --git a/components/cli/command/image/trust.go b/components/cli/command/image/trust.go index 58e0574396..2ff9b463d5 100644 --- a/components/cli/command/image/trust.go +++ b/components/cli/command/image/trust.go @@ -10,11 +10,11 @@ import ( "sort" "github.com/Sirupsen/logrus" + "github.com/docker/distribution/reference" "github.com/docker/docker/api/types" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/trust" "github.com/docker/docker/pkg/jsonmessage" - "github.com/docker/docker/reference" "github.com/docker/docker/registry" "github.com/docker/notary/client" "github.com/docker/notary/tuf/data" @@ -30,7 +30,7 @@ type target struct { // trustedPush handles content trust pushing of an image func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error { - responseBody, err := imagePushPrivileged(ctx, cli, authConfig, ref.String(), requestPrivilege) + responseBody, err := imagePushPrivileged(ctx, cli, authConfig, ref, requestPrivilege) if err != nil { return err } @@ -202,7 +202,7 @@ func addTargetToAllSignableRoles(repo *client.NotaryRepository, target *client.T } // imagePushPrivileged push the image -func imagePushPrivileged(ctx context.Context, cli *command.DockerCli, authConfig types.AuthConfig, ref string, requestPrivilege types.RequestPrivilegeFunc) (io.ReadCloser, error) { +func imagePushPrivileged(ctx context.Context, cli *command.DockerCli, authConfig types.AuthConfig, ref reference.Named, requestPrivilege types.RequestPrivilegeFunc) (io.ReadCloser, error) { encodedAuth, err := command.EncodeAuthToBase64(authConfig) if err != nil { return nil, err @@ -212,7 +212,7 @@ func imagePushPrivileged(ctx context.Context, cli *command.DockerCli, authConfig PrivilegeFunc: requestPrivilege, } - return cli.Client().ImagePush(ctx, ref, options) + return cli.Client().ImagePush(ctx, reference.FamiliarString(ref), options) } // trustedPull handles content trust pulling of an image @@ -229,12 +229,12 @@ func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry // List all targets targets, err := notaryRepo.ListTargets(trust.ReleasesRole, data.CanonicalTargetsRole) if err != nil { - return trust.NotaryError(repoInfo.FullName(), err) + return trust.NotaryError(ref.Name(), err) } for _, tgt := range targets { t, err := convertTarget(tgt.Target) if err != nil { - fmt.Fprintf(cli.Out(), "Skipping target for %q\n", repoInfo.Name()) + fmt.Fprintf(cli.Out(), "Skipping target for %q\n", reference.FamiliarName(ref)) continue } // Only list tags in the top level targets role or the releases delegation role - ignore @@ -245,17 +245,17 @@ func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry refs = append(refs, t) } if len(refs) == 0 { - return trust.NotaryError(repoInfo.FullName(), fmt.Errorf("No trusted tags for %s", repoInfo.FullName())) + return trust.NotaryError(ref.Name(), fmt.Errorf("No trusted tags for %s", ref.Name())) } } else { t, err := notaryRepo.GetTargetByName(tagged.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole) if err != nil { - return trust.NotaryError(repoInfo.FullName(), err) + return trust.NotaryError(ref.Name(), err) } // Only get the tag if it's in the top level targets role or the releases delegation role // ignore it if it's in any other delegation roles if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole { - return trust.NotaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", tagged.Tag())) + return trust.NotaryError(ref.Name(), fmt.Errorf("No trust data for %s", tagged.Tag())) } logrus.Debugf("retrieving target for %s role\n", t.Role) @@ -272,24 +272,21 @@ func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry if displayTag != "" { displayTag = ":" + displayTag } - fmt.Fprintf(cli.Out(), "Pull (%d of %d): %s%s@%s\n", i+1, len(refs), repoInfo.Name(), displayTag, r.digest) + fmt.Fprintf(cli.Out(), "Pull (%d of %d): %s%s@%s\n", i+1, len(refs), reference.FamiliarName(ref), displayTag, r.digest) - ref, err := reference.WithDigest(reference.TrimNamed(repoInfo), r.digest) + trustedRef, err := reference.WithDigest(reference.TrimNamed(ref), r.digest) if err != nil { return err } - if err := imagePullPrivileged(ctx, cli, authConfig, ref.String(), requestPrivilege, false); err != nil { + if err := imagePullPrivileged(ctx, cli, authConfig, reference.FamiliarString(trustedRef), requestPrivilege, false); err != nil { return err } - tagged, err := reference.WithTag(repoInfo, r.name) - if err != nil { - return err - } - trustedRef, err := reference.WithDigest(reference.TrimNamed(repoInfo), r.digest) + tagged, err := reference.WithTag(reference.TrimNamed(ref), r.name) if err != nil { return err } + if err := TagTrusted(ctx, cli, trustedRef, tagged); err != nil { return err } @@ -375,7 +372,11 @@ func convertTarget(t client.Target) (target, error) { // TagTrusted tags a trusted ref func TagTrusted(ctx context.Context, cli *command.DockerCli, trustedRef reference.Canonical, ref reference.NamedTagged) error { - fmt.Fprintf(cli.Out(), "Tagging %s as %s\n", trustedRef.String(), ref.String()) + // Use familiar references when interacting with client and output + familiarRef := reference.FamiliarString(ref) + trustedFamiliarRef := reference.FamiliarString(trustedRef) - return cli.Client().ImageTag(ctx, trustedRef.String(), ref.String()) + fmt.Fprintf(cli.Out(), "Tagging %s as %s\n", trustedFamiliarRef, familiarRef) + + return cli.Client().ImageTag(ctx, trustedFamiliarRef, familiarRef) } diff --git a/components/cli/command/plugin/create.go b/components/cli/command/plugin/create.go index 82d17af48c..e1e6f74ee3 100644 --- a/components/cli/command/plugin/create.go +++ b/components/cli/command/plugin/create.go @@ -8,18 +8,18 @@ import ( "path/filepath" "github.com/Sirupsen/logrus" + "github.com/docker/distribution/reference" "github.com/docker/docker/api/types" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/pkg/archive" - "github.com/docker/docker/reference" "github.com/spf13/cobra" "golang.org/x/net/context" ) // validateTag checks if the given repoName can be resolved. func validateTag(rawRepo string) error { - _, err := reference.ParseNamed(rawRepo) + _, err := reference.ParseNormalizedNamed(rawRepo) return err } diff --git a/components/cli/command/plugin/install.go b/components/cli/command/plugin/install.go index a64dc2525a..39b8c15ec7 100644 --- a/components/cli/command/plugin/install.go +++ b/components/cli/command/plugin/install.go @@ -6,14 +6,13 @@ import ( "fmt" "strings" - distreference "github.com/docker/distribution/reference" + "github.com/docker/distribution/reference" "github.com/docker/docker/api/types" registrytypes "github.com/docker/docker/api/types/registry" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/image" "github.com/docker/docker/pkg/jsonmessage" - "github.com/docker/docker/reference" "github.com/docker/docker/registry" "github.com/spf13/cobra" "golang.org/x/net/context" @@ -52,8 +51,8 @@ func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func getRepoIndexFromUnnormalizedRef(ref distreference.Named) (*registrytypes.IndexInfo, error) { - named, err := reference.ParseNamed(ref.Name()) +func getRepoIndexFromUnnormalizedRef(ref reference.Named) (*registrytypes.IndexInfo, error) { + named, err := reference.ParseNormalizedNamed(ref.Name()) if err != nil { return nil, err } @@ -85,71 +84,60 @@ func newRegistryService() registry.Service { } func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error { - // Parse name using distribution reference package to support name - // containing both tag and digest. Names with both tag and digest - // will be treated by the daemon as a pull by digest with - // an alias for the tag (if no alias is provided). - ref, err := distreference.ParseNamed(opts.name) + // Names with both tag and digest will be treated by the daemon + // as a pull by digest with an alias for the tag + // (if no alias is provided). + ref, err := reference.ParseNormalizedNamed(opts.name) if err != nil { return err } alias := "" if opts.alias != "" { - aref, err := reference.ParseNamed(opts.alias) + aref, err := reference.ParseNormalizedNamed(opts.alias) if err != nil { return err } - aref = reference.WithDefaultTag(aref) - if _, ok := aref.(reference.NamedTagged); !ok { + if _, ok := aref.(reference.Canonical); ok { return fmt.Errorf("invalid name: %s", opts.alias) } - alias = aref.String() + alias = reference.FamiliarString(reference.EnsureTagged(aref)) } ctx := context.Background() - index, err := getRepoIndexFromUnnormalizedRef(ref) + repoInfo, err := registry.ParseRepositoryInfo(ref) if err != nil { return err } remote := ref.String() - _, isCanonical := ref.(distreference.Canonical) + _, isCanonical := ref.(reference.Canonical) if command.IsTrusted() && !isCanonical { if alias == "" { - alias = ref.String() + alias = reference.FamiliarString(ref) } - var nt reference.NamedTagged - named, err := reference.ParseNamed(ref.Name()) - if err != nil { - return err - } - if tagged, ok := ref.(distreference.Tagged); ok { - nt, err = reference.WithTag(named, tagged.Tag()) - if err != nil { - return err - } - } else { - named = reference.WithDefaultTag(named) - nt = named.(reference.NamedTagged) + + nt, ok := ref.(reference.NamedTagged) + if !ok { + nt = reference.EnsureTagged(ref) } trusted, err := image.TrustedReference(ctx, dockerCli, nt, newRegistryService()) if err != nil { return err } - remote = trusted.String() + remote = reference.FamiliarString(trusted) } - authConfig := command.ResolveAuthConfig(ctx, dockerCli, index) + authConfig := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index) encodedAuth, err := command.EncodeAuthToBase64(authConfig) if err != nil { return err } - registryAuthFunc := command.RegistryAuthenticationPrivilegedFunc(dockerCli, index, "plugin install") + registryAuthFunc := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, "plugin install") options := types.PluginInstallOptions{ RegistryAuth: encodedAuth, diff --git a/components/cli/command/plugin/push.go b/components/cli/command/plugin/push.go index b0766307f3..b0ddad939e 100644 --- a/components/cli/command/plugin/push.go +++ b/components/cli/command/plugin/push.go @@ -5,11 +5,11 @@ import ( "golang.org/x/net/context" + "github.com/docker/distribution/reference" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/image" "github.com/docker/docker/pkg/jsonmessage" - "github.com/docker/docker/reference" "github.com/docker/docker/registry" "github.com/spf13/cobra" ) @@ -32,16 +32,17 @@ func newPushCommand(dockerCli *command.DockerCli) *cobra.Command { } func runPush(dockerCli *command.DockerCli, name string) error { - named, err := reference.ParseNamed(name) // FIXME: validate + named, err := reference.ParseNormalizedNamed(name) if err != nil { return err } - if reference.IsNameOnly(named) { - named = reference.WithDefaultTag(named) + if _, ok := named.(reference.Canonical); ok { + return fmt.Errorf("invalid name: %s", name) } - ref, ok := named.(reference.NamedTagged) + + taggedRef, ok := named.(reference.NamedTagged) if !ok { - return fmt.Errorf("invalid name: %s", named.String()) + taggedRef = reference.EnsureTagged(named) } ctx := context.Background() @@ -56,7 +57,8 @@ func runPush(dockerCli *command.DockerCli, name string) error { if err != nil { return err } - responseBody, err := dockerCli.Client().PluginPush(ctx, ref.String(), encodedAuth) + + responseBody, err := dockerCli.Client().PluginPush(ctx, reference.FamiliarString(taggedRef), encodedAuth) if err != nil { return err } diff --git a/components/cli/command/registry.go b/components/cli/command/registry.go index 65f6b3309e..411310fa34 100644 --- a/components/cli/command/registry.go +++ b/components/cli/command/registry.go @@ -12,10 +12,10 @@ import ( "golang.org/x/net/context" + "github.com/docker/distribution/reference" "github.com/docker/docker/api/types" registrytypes "github.com/docker/docker/api/types/registry" "github.com/docker/docker/pkg/term" - "github.com/docker/docker/reference" "github.com/docker/docker/registry" ) @@ -174,7 +174,7 @@ func RetrieveAuthTokenFromImage(ctx context.Context, cli *DockerCli, image strin // resolveAuthConfigFromImage retrieves that AuthConfig using the image string func resolveAuthConfigFromImage(ctx context.Context, cli *DockerCli, image string) (types.AuthConfig, error) { - registryRef, err := reference.ParseNamed(image) + registryRef, err := reference.ParseNormalizedNamed(image) if err != nil { return types.AuthConfig{}, err } diff --git a/components/cli/command/service/trust.go b/components/cli/command/service/trust.go index 15f8a708f0..d466f3b648 100644 --- a/components/cli/command/service/trust.go +++ b/components/cli/command/service/trust.go @@ -5,11 +5,10 @@ import ( "fmt" "github.com/Sirupsen/logrus" - distreference "github.com/docker/distribution/reference" + "github.com/docker/distribution/reference" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/trust" - "github.com/docker/docker/reference" "github.com/docker/docker/registry" "github.com/docker/notary/tuf/data" "github.com/opencontainers/go-digest" @@ -24,41 +23,34 @@ func resolveServiceImageDigest(dockerCli *command.DockerCli, service *swarm.Serv return nil } - image := service.TaskTemplate.ContainerSpec.Image - - // We only attempt to resolve the digest if the reference - // could be parsed as a digest reference. Specifying an image ID - // is valid but not resolvable. There is no warning message for - // an image ID because it's valid to use one. - if _, err := digest.Parse(image); err == nil { - return nil - } - - ref, err := reference.ParseNamed(image) + ref, err := reference.ParseAnyReference(service.TaskTemplate.ContainerSpec.Image) if err != nil { - return fmt.Errorf("Could not parse image reference %s", service.TaskTemplate.ContainerSpec.Image) + return errors.Wrapf(err, "invalid reference %s", service.TaskTemplate.ContainerSpec.Image) } - if _, ok := ref.(reference.Canonical); !ok { - ref = reference.WithDefaultTag(ref) - taggedRef, ok := ref.(reference.NamedTagged) + // If reference does not have digest (is not canonical nor image id) + if _, ok := ref.(reference.Digested); !ok { + namedRef, ok := ref.(reference.Named) if !ok { - // This should never happen because a reference either - // has a digest, or WithDefaultTag would give it a tag. - return errors.New("Failed to resolve image digest using content trust: reference is missing a tag") + return errors.New("failed to resolve image digest using content trust: reference is not named") + } + taggedRef := reference.EnsureTagged(namedRef) + resolvedImage, err := trustedResolveDigest(context.Background(), dockerCli, taggedRef) if err != nil { - return fmt.Errorf("Failed to resolve image digest using content trust: %v", err) + return errors.Wrap(err, "failed to resolve image digest using content trust") } - logrus.Debugf("resolved image tag to %s using content trust", resolvedImage.String()) - service.TaskTemplate.ContainerSpec.Image = resolvedImage.String() + resolvedFamiliar := reference.FamiliarString(resolvedImage) + logrus.Debugf("resolved image tag to %s using content trust", resolvedFamiliar) + service.TaskTemplate.ContainerSpec.Image = resolvedFamiliar } + return nil } -func trustedResolveDigest(ctx context.Context, cli *command.DockerCli, ref reference.NamedTagged) (distreference.Canonical, error) { +func trustedResolveDigest(ctx context.Context, cli *command.DockerCli, ref reference.NamedTagged) (reference.Canonical, error) { repoInfo, err := registry.ParseRepositoryInfo(ref) if err != nil { return nil, err @@ -78,7 +70,7 @@ func trustedResolveDigest(ctx context.Context, cli *command.DockerCli, ref refer // Only get the tag if it's in the top level targets role or the releases delegation role // ignore it if it's in any other delegation roles if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole { - return nil, trust.NotaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", ref.String())) + return nil, trust.NotaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", reference.FamiliarString(ref))) } logrus.Debugf("retrieving target for %s role\n", t.Role) @@ -89,8 +81,6 @@ func trustedResolveDigest(ctx context.Context, cli *command.DockerCli, ref refer dgst := digest.NewDigestFromHex("sha256", hex.EncodeToString(h)) - // Using distribution reference package to make sure that adding a - // digest does not erase the tag. When the two reference packages - // are unified, this will no longer be an issue. - return distreference.WithDigest(ref, dgst) + // Allow returning canonical reference with tag + return reference.WithDigest(ref, dgst) } From 1d5659f53b4fc2e58dc087cee06803603c8ad69f Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 4 Jan 2017 10:06:59 -0500 Subject: [PATCH 540/978] Update Compose schema to match docker-compose. Signed-off-by: Daniel Nephin Upstream-commit: cd3c323c381e28fc06b0885b4b3d70237d7e7ca8 Component: cli --- components/cli/compose/schema/bindata.go | 4 ++-- .../schema/data/config_schema_v3.0.json | 23 +++++++++++-------- components/cli/compose/types/types.go | 2 ++ 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/components/cli/compose/schema/bindata.go b/components/cli/compose/schema/bindata.go index c3774130b3..c976509355 100644 --- a/components/cli/compose/schema/bindata.go +++ b/components/cli/compose/schema/bindata.go @@ -68,7 +68,7 @@ func (fi bindataFileInfo) Sys() interface{} { return nil } -var _dataConfig_schema_v30Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x5a\x4d\x8f\xdb\x36\x13\xbe\xfb\x57\x08\x4a\x6e\xf1\xee\x06\x78\x83\x17\x68\x6e\x3d\xf6\xd4\x9e\xbb\x50\x04\x5a\x1a\xdb\xcc\x52\x24\x33\xa4\x9c\x75\x02\xff\xf7\x82\xfa\xb2\x48\x93\xa2\x6c\x2b\x4d\x0e\xbd\x2c\xd6\xe2\xcc\x70\xbe\xf8\xcc\x70\xa4\xef\xab\x24\x49\xdf\xaa\x62\x0f\x15\x49\x3f\x26\xe9\x5e\x6b\xf9\xf1\xe9\xe9\xb3\x12\xfc\xa1\x7d\xfa\x28\x70\xf7\x54\x22\xd9\xea\x87\xf7\x1f\x9e\xda\x67\x6f\xd2\xb5\xe1\xa3\xa5\x61\x29\x04\xdf\xd2\x5d\xde\xae\xe4\x87\xff\x3d\xbe\x7f\x34\xec\x2d\x89\x3e\x4a\x30\x44\x62\xf3\x19\x0a\xdd\x3e\x43\xf8\x52\x53\x04\xc3\xfc\x9c\x1e\x00\x15\x15\x3c\xcd\xd6\x2b\xb3\x26\x51\x48\x40\x4d\x41\xa5\x1f\x13\xa3\x5c\x92\x0c\x24\xfd\x83\x91\x58\xa5\x91\xf2\x5d\xda\x3c\x3e\x35\x12\x92\x24\x55\x80\x07\x5a\x8c\x24\x0c\xaa\xbe\x79\x3a\xcb\x7f\x1a\xc8\xd6\xae\xd4\x91\xb2\xcd\x73\x49\xb4\x06\xe4\x7f\x5d\xea\xd6\x2c\x7f\x7a\x26\x0f\xdf\x7e\x7f\xf8\xfb\xfd\xc3\x6f\x8f\xf9\x43\xf6\xee\xad\xb5\x6c\xfc\x8b\xb0\x6d\xb7\x2f\x61\x4b\x39\xd5\x54\xf0\x61\xff\x74\xa0\x3c\x75\xff\x9d\x86\x8d\x49\x59\x36\xc4\x84\x59\x7b\x6f\x09\x53\x60\xdb\xcc\x41\x7f\x15\xf8\x12\xb3\x79\x20\xfb\x49\x36\x77\xfb\x7b\x6c\xb6\xcd\x39\x08\x56\x57\xd1\x08\xf6\x54\x3f\xc9\x98\x76\xfb\xfb\xe2\xb7\xea\x8d\x9e\xa4\x6d\x29\x46\x7b\x37\x0a\x5a\xd9\xee\x73\x95\x2f\xdb\xc2\xbe\x1a\x9c\x15\xf0\x52\x09\x92\x89\xa3\x79\x16\xf0\x47\x4b\x50\x01\xd7\xe9\xe0\x82\x24\x49\x37\x35\x65\xa5\xeb\x51\xc1\xe1\x4f\x23\xe2\x79\xf4\x30\x49\xbe\xbb\x07\x7b\x24\xa7\x59\xb7\x7e\x85\x03\x3e\xac\x07\x6c\x19\xd6\x0b\xc1\x35\xbc\xea\xc6\xa8\xe9\xad\x5b\x17\x88\xe2\x05\x70\x4b\x19\xcc\xe5\x20\xb8\x53\x13\x2e\x63\x54\xe9\x5c\x60\x5e\xd2\x42\xa7\x27\x87\xfd\x42\x5e\x3c\x9f\x06\xd6\xd1\xaf\x6c\xe5\x11\x98\x16\x44\xe6\xa4\x2c\x2d\x3b\x08\x22\x39\xa6\xeb\x24\xa5\x1a\x2a\xe5\x37\x31\x49\x6b\x4e\xbf\xd4\xf0\x47\x47\xa2\xb1\x06\x57\x6e\x89\x42\x2e\x2f\x78\x87\xa2\x96\xb9\x24\x68\x12\x6c\xda\xfd\x69\x21\xaa\x8a\xf0\xa5\xb2\xee\x1a\x3b\x66\x78\x5e\x70\x4d\x28\x07\xcc\x39\xa9\x62\x89\x64\x4e\x1d\xf0\x52\xe5\x6d\xfd\x9b\x4c\xa3\x6d\xde\xf2\x2b\x47\xc0\x50\x0c\x17\x8d\x47\xc9\xa7\x12\xbb\x15\x63\x52\xdb\xe8\x96\x3a\x8c\xb9\x02\x82\xc5\xfe\x46\x7e\x51\x11\xca\xe7\xf8\x0e\xb8\xc6\xa3\x14\xb4\xcd\x97\x5f\x2e\x11\x80\x1f\xf2\x01\x4b\xae\x76\x03\xf0\x03\x45\xc1\xab\xfe\x34\xcc\x01\x98\x01\xe4\x0d\xff\xab\x14\x0a\x5c\xc7\x38\x06\x8e\x97\x06\x53\x2d\x9f\xf4\x1c\xcf\xbd\xe1\xeb\x24\xe5\x75\xb5\x01\x34\x2d\x9d\x45\xb9\x15\x58\x11\xa3\x6c\xbf\xf7\x68\xd9\xf2\xb4\x27\xf3\xc6\x0e\x1c\xdb\x60\xca\x3a\x61\x39\xa3\xfc\x65\xf9\x14\x87\x57\x8d\x24\xdf\x0b\xa5\xe7\x63\xf8\x88\x7d\x0f\x84\xe9\x7d\xb1\x87\xe2\x65\x82\x7d\x4c\x65\x71\x0b\xa5\xe7\x24\x39\xad\xc8\x2e\x4e\x24\x8b\x18\x09\x23\x1b\x60\x37\xd9\xb9\xa8\xf3\x47\x62\xc5\x6e\x67\x48\x43\x19\x77\xd1\xb9\x74\xcb\xb1\x9a\x5f\x22\x3d\x00\xce\x2d\xe0\x42\x9e\x1b\x2e\x77\x31\xde\x80\x24\xf1\xee\xd3\x22\xfd\xf4\xd8\x36\x9f\x13\xa7\xaa\xf9\x8f\xb1\x34\x73\xdb\x85\xc4\xa9\xfb\xbe\x27\x8e\x85\xf3\x1a\x0a\x2b\x2a\x15\x29\x4c\xdf\x80\xa0\x02\x71\x3d\x93\x76\xcd\x7e\x5e\x89\x32\x94\xa0\x17\xc4\xae\x6f\x82\x48\x7d\x75\x21\x4c\x6e\xea\x1f\x67\x85\x2e\x7a\x81\x88\x58\x13\x52\x6f\xae\x9a\x67\x75\xe3\x29\xd6\xd0\x11\x46\x89\x82\xf8\x61\x0f\x3a\xd2\x92\x46\xe5\xe1\xc3\xcc\x9c\xf0\xf1\xfe\x7f\x92\x37\xc0\x1a\x94\x39\xbf\x47\x8e\x88\x3a\xab\xd2\x1c\x37\x9f\x22\x59\xe4\xb4\xfd\xe0\x16\x5e\xd2\x32\x8c\x15\x0d\x42\x8c\x0f\x98\x14\xa8\x2f\x4e\xd7\xbf\x53\xee\xdb\xad\xef\xae\xf6\x12\xe9\x81\x32\xd8\x81\x7d\x6b\xd9\x08\xc1\x80\x70\x0b\x7a\x10\x48\x99\x0b\xce\x8e\x33\x28\x95\x26\x18\xbd\x50\x28\x28\x6a\xa4\xfa\x98\x0b\xa9\x17\xef\x33\xd4\xbe\xca\x15\xfd\x06\x76\x34\xcf\x78\xdf\x09\xca\x2c\x1e\x5d\x52\x9e\x0b\x09\x3c\x6a\xa2\xd2\x42\xe6\x8a\xee\x38\x61\x51\x33\x0d\xe9\x0e\x49\x01\xb9\x04\xa4\xa2\xf4\x31\xac\xc7\xb1\x2d\x6b\x24\x26\x9f\x2d\x31\xba\x92\xdb\x1b\x6f\x07\x5a\xc7\x63\x56\x33\x5a\xd1\x70\x32\x7b\x50\x72\x06\x90\xb7\x20\xee\xc7\xee\x09\xdc\x3e\x6b\x4a\xb9\x86\x1d\xa0\x0f\xee\x26\x5a\x87\xe9\xce\x61\x46\xcb\xb0\x27\x68\x47\x69\x42\x8f\x86\x41\x89\xad\xf6\x33\xf8\x1a\x0a\xaf\x5e\xd6\x04\xb7\x91\xb7\xee\x14\xc9\xbc\xf4\x57\x61\xb2\xab\x46\x16\x84\xc5\x93\x17\x16\x6b\x15\xed\xee\xc6\xf3\xc5\x45\x4f\xb2\x69\x61\x4c\x66\x97\xd4\xaf\xc2\xca\x51\xf7\x8a\x09\xaf\x73\x9b\xe8\x05\xf8\x66\x7d\x63\x52\x77\xde\xf7\x3c\x24\x5c\x5f\x25\xce\x53\xd2\xc0\xe0\xcf\xe4\x07\x1e\x2c\xf0\xf0\xf9\x54\xd3\x0a\x44\xad\x23\x54\x08\x1a\xa9\xe3\xf9\x0e\xe9\x2c\x61\xa0\x7e\xcd\x4b\x7b\x49\x15\xd9\x38\xf3\xbf\x01\xa3\x6e\x0a\x6f\x72\x1e\xae\xf6\x97\xf9\xa9\xe0\x8e\x28\x17\x88\xed\x44\x6f\x3e\x0a\x99\x64\xb4\x20\x2a\x86\x32\x77\x5c\x21\x6b\x59\x12\x0d\x79\xfb\x2a\xe9\x2a\x5c\x9f\x00\x74\x49\x90\x30\x06\x8c\xaa\x6a\x0e\x40\xa6\x25\x30\x72\xbc\xa9\xe0\x35\xec\x5b\x42\x59\x8d\x90\x93\x42\x77\x6f\xab\x22\x99\x99\x56\x82\x53\x2d\xbc\x48\x31\x6f\xcb\x8a\xbc\xe6\xfd\xb6\x0d\x89\xf7\x58\x05\x1b\xaf\xb9\xb7\xbf\x51\x26\x28\x51\x63\x71\xe1\xec\x9b\x43\x74\x2e\xe4\x81\x8c\xe9\x77\xbc\x30\x1d\x41\x19\x50\x1a\x2e\xe7\x51\xfe\x68\xdd\xe8\x3a\xc1\x5c\x0a\x46\x8b\xe3\x52\x16\x16\x82\xb7\x4e\x9e\x93\x10\x77\x66\xa0\x49\x07\xd3\xe7\x54\x52\x47\x0f\x6b\xc3\xf0\x95\xf2\x52\x7c\xbd\x62\xc3\xe5\x52\x49\x32\x52\x80\x83\x77\xf7\x3a\x5a\x69\x24\x94\xeb\xab\xcb\xfa\xbd\x66\xdd\x51\xd5\x87\xfc\x8c\xa0\xfe\x40\x17\x7f\xd7\x19\x40\xfa\x42\xd6\xd1\x89\x4d\x05\x95\x40\x6f\x02\x2e\xf0\x6e\x3a\x66\x62\x4f\xb6\x40\x55\x9b\x35\xe2\xeb\xa8\xcc\x8d\x6e\xf1\xab\x44\x7c\x8c\x97\xc5\x01\x89\x4a\x52\x2d\x75\x3a\x66\x0f\x3d\x53\x6f\x0d\x4e\xa6\x87\x05\x49\x78\x60\x10\xd3\x3a\xae\x7b\x47\xa1\xea\x0d\x07\xff\x3d\xfd\xf2\x0a\xe1\x7b\x13\x3b\xff\x0e\x72\x0a\xdf\x38\xee\x03\xbd\xfe\x7d\x45\x20\xaa\xcf\x43\x27\xb9\x1e\x7c\x95\xcd\x0e\x71\xf0\x65\xc1\x72\xfa\x5f\xd9\xe0\xdd\x81\x19\xdd\xb7\x15\x11\xc8\xe8\xa8\xfe\x43\x8c\x5f\x26\xbf\x26\x8a\xe2\x8d\xb7\x83\x2b\x92\xc6\x19\x2b\x8d\x92\xe7\xf2\xea\x38\x15\xe7\xd9\x43\xf1\x8e\x23\xb3\xd5\x70\xc9\x3c\xdf\xad\xd9\x10\x3a\x35\x71\xe8\x49\x02\x43\x52\x67\xd3\xce\x79\xd3\x96\x2f\x98\xb6\x8f\xef\x26\x0a\xc5\xd4\xcb\xab\x1f\x84\xb0\x0b\x4c\x73\xfc\x31\x75\xba\xcb\xde\xbb\x97\x1f\x5f\x05\x90\x6a\xc4\x7f\xf1\x29\x96\xb1\x93\x1f\x2f\x46\x1b\xdf\xed\x31\x5b\xfb\x19\x55\x66\xf9\xc7\x21\x69\x5f\x05\x8f\x70\x22\x1b\x37\xdc\xa1\x30\x7a\x3f\xd0\x72\x87\x7c\xfd\x87\x52\xd9\xf4\x61\x5f\xf5\x7f\x4f\xab\xd3\xea\x9f\x00\x00\x00\xff\xff\xd1\xeb\xc9\xb9\x5c\x2a\x00\x00") +var _dataConfig_schema_v30Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x5a\x4f\x8f\xdb\xb8\x0e\xbf\xe7\x53\x18\x6e\x6f\xcd\xcc\x14\x78\xc5\x03\x5e\x6f\xef\xb8\xa7\xdd\xf3\x0e\x5c\x43\xb1\x99\x44\x1d\x59\x52\x29\x39\x9d\xb4\xc8\x77\x5f\xc8\xff\x22\x2b\x92\xe5\x24\xee\xb6\x87\x9e\x66\x62\x91\x14\xff\xe9\x47\x8a\xf6\xf7\x55\x92\xa4\x6f\x55\xb1\x87\x8a\xa4\x1f\x93\x74\xaf\xb5\xfc\xf8\xf4\xf4\x59\x09\xfe\xd0\x3e\x7d\x14\xb8\x7b\x2a\x91\x6c\xf5\xc3\xfb\x0f\x4f\xed\xb3\x37\xe9\xda\xf0\xd1\xd2\xb0\x14\x82\x6f\xe9\x2e\x6f\x57\xf2\xc3\x7f\x1e\xdf\x3f\x1a\xf6\x96\x44\x1f\x25\x18\x22\xb1\xf9\x0c\x85\x6e\x9f\x21\x7c\xa9\x29\x82\x61\x7e\x4e\x0f\x80\x8a\x0a\x9e\x66\xeb\x95\x59\x93\x28\x24\xa0\xa6\xa0\xd2\x8f\x89\x51\x2e\x49\x06\x92\xfe\x81\x25\x56\x69\xa4\x7c\x97\x36\x8f\x4f\x8d\x84\x24\x49\x15\xe0\x81\x16\x96\x84\x41\xd5\x37\x4f\x67\xf9\x4f\x03\xd9\xda\x95\x6a\x29\xdb\x3c\x97\x44\x6b\x40\xfe\xd7\xa5\x6e\xcd\xf2\xa7\x67\xf2\xf0\xed\xff\x0f\x7f\xbf\x7f\xf8\xdf\x63\xfe\x90\xbd\x7b\x3b\x5a\x36\xfe\x45\xd8\xb6\xdb\x97\xb0\xa5\x9c\x6a\x2a\xf8\xb0\x7f\x3a\x50\x9e\xba\xff\x4e\xc3\xc6\xa4\x2c\x1b\x62\xc2\x46\x7b\x6f\x09\x53\x30\xb6\x99\x83\xfe\x2a\xf0\x25\x66\xf3\x40\xf6\x93\x6c\xee\xf6\xf7\xd8\x3c\x36\xe7\x20\x58\x5d\x45\x23\xd8\x53\xfd\x24\x63\xda\xed\xef\x8b\xdf\xaa\x37\x7a\x92\xb6\xa5\xb0\xf6\x6e\x14\x1c\x65\xbb\xcf\x55\xbe\x6c\x0b\xfb\x6a\x70\x56\xc0\x4b\x25\x48\x26\x8e\xe6\x59\xc0\x1f\x2d\x41\x05\x5c\xa7\x83\x0b\x92\x24\xdd\xd4\x94\x95\xae\x47\x05\x87\x3f\x8d\x88\x67\xeb\x61\x92\x7c\x77\x0f\xb6\x25\xa7\x59\x1f\xfd\x0a\x07\x7c\x58\x0f\xd8\x32\xac\x17\x82\x6b\x78\xd5\x8d\x51\xd3\x5b\xb7\x2e\x10\xc5\x0b\xe0\x96\x32\x98\xcb\x41\x70\xa7\x26\x5c\xc6\xa8\xd2\xb9\xc0\xbc\xa4\x85\x4e\x4f\x0e\xfb\x85\xbc\x78\x3e\x0d\xac\xd6\xaf\x6c\xe5\x11\x98\x16\x44\xe6\xa4\x2c\x47\x76\x10\x44\x72\x4c\xd7\x49\x4a\x35\x54\xca\x6f\x62\x92\xd6\x9c\x7e\xa9\xe1\x8f\x8e\x44\x63\x0d\xae\xdc\x12\x85\x5c\x5e\xf0\x0e\x45\x2d\x73\x49\xd0\x24\xd8\xb4\xfb\xd3\x42\x54\x15\xe1\x4b\x65\xdd\x35\x76\xcc\xf0\xbc\xe0\x9a\x50\x0e\x98\x73\x52\xc5\x12\xc9\x9c\x3a\xe0\xa5\xca\xdb\xfa\x37\x99\x46\xdb\xbc\xe5\x57\x8e\x80\xa1\x18\x2e\x1a\x8f\x92\x4f\x25\x76\x2b\xc6\xa4\xb6\xd1\x2d\x75\x18\x73\x05\x04\x8b\xfd\x8d\xfc\xa2\x22\x94\xcf\xf1\x1d\x70\x8d\x47\x29\x68\x9b\x2f\xbf\x5c\x22\x00\x3f\xe4\x03\x96\x5c\xed\x06\xe0\x07\x8a\x82\x57\xfd\x69\x98\x03\x30\x03\xc8\x1b\xfe\x57\x29\x14\xb8\x8e\x71\x0c\xb4\x97\x06\x53\x47\x3e\xe9\x39\x9e\x7b\xc3\xd7\x49\xca\xeb\x6a\x03\x68\x5a\xba\x11\xe5\x56\x60\x45\x8c\xb2\xfd\xde\xd6\xf2\xc8\xd3\x9e\xcc\xb3\x1d\x68\xdb\x60\xca\x3a\x61\x39\xa3\xfc\x65\xf9\x14\x87\x57\x8d\x24\xdf\x0b\xa5\xe7\x63\xb8\xc5\xbe\x07\xc2\xf4\xbe\xd8\x43\xf1\x32\xc1\x6e\x53\x8d\xb8\x85\xd2\x73\x92\x9c\x56\x64\x17\x27\x92\x45\x8c\x84\x91\x0d\xb0\x9b\xec\x5c\xd4\xf9\x96\x58\xb1\xdb\x19\xd2\x50\xc6\x5d\x74\x2e\xdd\x72\xac\xe6\x97\x48\x0f\x80\x73\x0b\xb8\x90\xe7\x86\xcb\x5d\x8c\x37\x20\x49\xbc\xfb\x1c\x91\x7e\x7a\x6c\x9b\xcf\x89\x53\xd5\xfc\xc7\x58\x9a\xb9\xed\x42\xe2\xd4\x7d\xdf\x13\xc7\xc2\x79\x0d\xc5\x28\x2a\x15\x29\x4c\xdf\x80\xa0\x02\x71\x3d\x93\x76\xcd\x7e\x5e\x89\x32\x94\xa0\x17\xc4\xae\x6f\x82\x48\x7d\x75\x21\x4c\x6e\xea\x1f\x67\x85\x2e\x7a\x81\x88\x58\x13\x52\x6f\xae\x9a\x67\x75\xe3\x29\xd6\xd0\x11\x46\x89\x82\xf8\x61\x0f\x3a\x72\x24\x8d\xca\xc3\x87\x99\x39\xe1\xe3\xfd\xef\x24\x6f\x80\x35\x28\x73\x7e\x8f\x1c\x11\x75\x56\xa5\x39\x6e\x3e\x45\xb2\xc8\x69\xfb\xc1\x2d\xbc\xa4\x65\x18\x2b\x1a\x84\xb0\x0f\x98\x14\xa8\x2f\x4e\xd7\xbf\x53\xee\xdb\xad\xef\xae\xf6\x12\xe9\x81\x32\xd8\xc1\xf8\xd6\xb2\x11\x82\x01\xe1\x23\xe8\x41\x20\x65\x2e\x38\x3b\xce\xa0\x54\x9a\x60\xf4\x42\xa1\xa0\xa8\x91\xea\x63\x2e\xa4\x5e\xbc\xcf\x50\xfb\x2a\x57\xf4\x1b\x8c\xa3\x79\xc6\xfb\x4e\x50\x36\xe2\x39\xaa\x42\xdf\x56\xaf\x95\x2e\x29\xcf\x85\x04\x1e\xf5\x8e\xd2\x42\xe6\x3b\x24\x05\xe4\x12\x90\x8a\xd2\x67\xe0\xda\x8e\x75\x59\x23\x31\xfb\x5f\x8a\x51\x74\xc7\x09\x8b\x39\x5a\x57\x72\x7b\xe3\xc5\x42\xeb\x78\xb8\x6b\x46\x2b\x1a\x3e\x07\x1e\x80\x9d\x51\x03\x5a\xfc\xf7\xc3\xfe\x04\xe4\x9f\x35\xa5\x5c\xc3\x0e\xd0\x87\x94\x13\x5d\xc7\x74\xd3\x31\xa3\xdb\xd8\x13\x1c\x07\x74\x42\x8f\x86\x41\x89\xad\xf6\x33\xf8\x7a\x11\xaf\x5e\xa3\xe1\x6f\x23\x6f\xdd\x29\x92\x79\xe9\xaf\x82\x73\x57\x8d\x2c\x88\xa8\x27\x2f\xa2\xd6\x2a\xda\x18\x36\x34\x5c\x4d\x35\x35\x03\xa9\x35\xc5\x5c\x14\x2f\x4c\xa3\x64\x0e\x41\x49\xfd\xda\xae\x1c\xcb\xae\x98\x23\x3b\x77\x96\x5e\x80\x6f\xa2\x68\x93\x46\x27\xb0\xd3\xd3\xcd\x8e\x28\x38\x79\xa4\x8a\x6c\x9c\x99\x9b\xef\x70\x9b\x6c\xc4\x43\x1c\x63\x10\x34\x52\x27\x2e\x1d\xda\x8e\xf0\x04\xd4\xaf\x39\x38\xd0\xb4\x02\x51\xfb\x6b\xd6\xca\xce\xef\x8e\x29\xb5\x26\xb3\x91\xa0\x5a\x94\x6e\x4c\x9f\x87\xa0\xf6\xfd\x45\x34\x70\x73\x0e\x09\x82\x64\xb4\x20\x2a\x06\x44\x77\x5c\x50\x6b\x59\x12\x0d\x79\xfb\xa2\xea\x2a\xe8\x9f\xc0\x7c\x49\x90\x30\x06\x8c\xaa\x6a\x0e\x86\xa6\x25\x30\x72\xbc\xa9\x7c\x36\xec\x5b\x42\x59\x8d\x90\x93\x42\x77\xef\xc2\x22\x39\x97\x56\x82\x53\x2d\xbc\x08\x31\x6f\xcb\x8a\xbc\xe6\xfd\xb6\x0d\x89\xf7\xc0\x04\xdb\xba\xb9\x77\x4b\x2b\x13\x94\xa8\xb1\xb8\x70\xf6\xcd\x21\x3a\xd7\xfa\x40\xc6\xf4\x3b\x5e\x98\x8e\xa0\x0c\x92\x0c\x57\xff\x28\x7f\xb4\xb4\x74\x7d\x66\x2e\x05\xa3\xc5\x71\x29\x0b\x0b\xc1\x5b\x27\xcf\x49\x88\x3b\x33\xd0\xa4\x83\x69\x85\x2a\xa9\xa3\x87\xb5\x61\xf8\x4a\x79\x29\xbe\x5e\xb1\xe1\x72\xa9\x24\x19\x29\xc0\xc1\xbb\x7b\x1d\xad\x34\x12\xca\xf5\xd5\xe5\xfc\x5e\xb3\xee\xa8\xe6\x43\x7e\x46\x50\x7f\xa0\x8b\xbf\x49\x0d\x20\x7d\x21\xeb\xe8\x3c\xa8\x82\x4a\xa0\x37\x01\x17\x78\xf3\x1d\x33\xb1\x27\x5b\xa0\xaa\xcd\x1a\x20\x76\x54\xe6\xbe\xb8\xf8\x6d\x23\x3e\x24\xcc\xe2\x80\x44\x25\xa9\x96\x3a\x1d\xb3\x47\xaa\xa9\xb7\x06\x27\xd3\xa3\x88\x24\x3c\x8e\x88\x69\x1d\xd7\xbd\xa3\x50\xf5\x86\xc3\x64\x47\x65\xf9\xd3\xf7\x9e\x77\xfe\x35\xe5\x14\xbe\x94\xdc\x07\x7a\xfd\xdb\x90\x40\x54\x9f\x87\x9e\x79\x3d\xf8\x2a\x9b\x1d\xe2\xe0\xab\x88\xe5\xf4\xbf\xb2\xc1\xbb\x03\x33\xba\x2f\x37\x22\x90\xd1\x51\xfd\x46\x8c\xdf\xf9\x75\x65\x7e\x39\x43\x2a\x2b\xcf\x2e\xef\x8f\x53\x29\x31\x7b\x3a\xdf\x71\x64\x63\x35\x5c\x32\xcf\x07\x74\x63\xb4\x9d\x1a\x4a\xf4\x24\x81\x69\xad\xb3\x69\xe7\xc4\x69\xcb\x17\xcc\xf0\xc7\x77\x13\x35\x65\xea\x2d\xda\x0f\x02\xe3\x05\x06\x3e\xfe\x98\x3a\x8d\x68\xef\xdd\xcb\xaf\xc0\x02\xa0\x66\xf1\x5f\x7c\x13\x66\xec\xe4\xc7\x8b\xf9\xc6\xf7\xf1\xd0\xae\xfd\x9e\x2b\x1b\xf9\xc7\x21\x69\xdf\x49\x5b\x90\x92\xd9\xbd\x79\x28\x8c\xde\x2f\xc5\xdc\x91\x61\xff\xc5\x56\xe6\x87\xab\x95\xfd\xb7\xf9\xba\x6e\x75\x5a\xfd\x13\x00\x00\xff\xff\x46\xf7\x7b\x23\xe5\x2a\x00\x00") func dataConfig_schema_v30JsonBytes() ([]byte, error) { return bindataRead( @@ -182,7 +182,6 @@ type bintree struct { Func func() (*asset, error) Children map[string]*bintree } - var _bintree = &bintree{nil, map[string]*bintree{ "data": &bintree{nil, map[string]*bintree{ "config_schema_v3.0.json": &bintree{dataConfig_schema_v30Json, map[string]*bintree{}}, @@ -235,3 +234,4 @@ func _filePath(dir, name string) string { cannonicalName := strings.Replace(name, "\\", "/", -1) return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) } + diff --git a/components/cli/compose/schema/data/config_schema_v3.0.json b/components/cli/compose/schema/data/config_schema_v3.0.json index 520e57d5e2..584b6ef5d8 100644 --- a/components/cli/compose/schema/data/config_schema_v3.0.json +++ b/components/cli/compose/schema/data/config_schema_v3.0.json @@ -167,9 +167,10 @@ "restart": {"type": "string"}, "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, "shm_size": {"type": ["number", "string"]}, + "sysctls": {"$ref": "#/definitions/list_or_dict"}, "stdin_open": {"type": "boolean"}, - "stop_signal": {"type": "string"}, "stop_grace_period": {"type": "string", "format": "duration"}, + "stop_signal": {"type": "string"}, "tmpfs": {"$ref": "#/definitions/string_or_list"}, "tty": {"type": "boolean"}, "ulimits": { @@ -192,6 +193,7 @@ } }, "user": {"type": "string"}, + "userns_mode": {"type": "string"}, "volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, "working_dir": {"type": "string"} }, @@ -200,10 +202,11 @@ "healthcheck": { "id": "#/definitions/healthcheck", - "type": ["object", "null"], + "type": "object", + "additionalProperties": false, "properties": { - "interval": {"type":"string"}, - "timeout": {"type":"string"}, + "disable": {"type": "boolean"}, + "interval": {"type": "string"}, "retries": {"type": "number"}, "test": { "oneOf": [ @@ -211,9 +214,8 @@ {"type": "array", "items": {"type": "string"}} ] }, - "disable": {"type": "boolean"} - }, - "additionalProperties": false + "timeout": {"type": "string"} + } }, "deployment": { "id": "#/definitions/deployment", @@ -326,10 +328,11 @@ "type": ["boolean", "object"], "properties": { "name": {"type": "string"} - } - } + }, + "additionalProperties": false + }, + "labels": {"$ref": "#/definitions/list_or_dict"} }, - "labels": {"$ref": "#/definitions/list_or_dict"}, "additionalProperties": false }, diff --git a/components/cli/compose/types/types.go b/components/cli/compose/types/types.go index 5244bd1163..393bee2f8a 100644 --- a/components/cli/compose/types/types.go +++ b/components/cli/compose/types/types.go @@ -25,7 +25,9 @@ var UnsupportedProperties = []string{ "security_opt", "shm_size", "stop_signal", + "sysctls", "tmpfs", + "userns_mode", } // DeprecatedProperties that were removed from the v3 format, but their From d47c7b7dfbc12705932efda3de911faf2c91b9f9 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 20 Jan 2017 12:53:19 -0500 Subject: [PATCH 541/978] Add missing network.internal. Signed-off-by: Daniel Nephin Upstream-commit: 3dd116fede13f299aa824222f54d4bee07248c01 Component: cli --- components/cli/compose/convert/compose.go | 7 ++++--- components/cli/compose/schema/data/config_schema_v3.0.json | 1 + components/cli/compose/types/types.go | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/components/cli/compose/convert/compose.go b/components/cli/compose/convert/compose.go index 70c1762a48..532f4c4b29 100644 --- a/components/cli/compose/convert/compose.go +++ b/components/cli/compose/convert/compose.go @@ -58,9 +58,10 @@ func Networks(namespace Namespace, networks networkMap, servicesNetworks map[str } createOpts := types.NetworkCreate{ - Labels: AddStackLabel(namespace, network.Labels), - Driver: network.Driver, - Options: network.DriverOpts, + Labels: AddStackLabel(namespace, network.Labels), + Driver: network.Driver, + Options: network.DriverOpts, + Internal: network.Internal, } if network.Ipam.Driver != "" || len(network.Ipam.Config) > 0 { diff --git a/components/cli/compose/schema/data/config_schema_v3.0.json b/components/cli/compose/schema/data/config_schema_v3.0.json index 584b6ef5d8..fbcd8bb859 100644 --- a/components/cli/compose/schema/data/config_schema_v3.0.json +++ b/components/cli/compose/schema/data/config_schema_v3.0.json @@ -308,6 +308,7 @@ }, "additionalProperties": false }, + "internal": {"type": "boolean"}, "labels": {"$ref": "#/definitions/list_or_dict"} }, "additionalProperties": false diff --git a/components/cli/compose/types/types.go b/components/cli/compose/types/types.go index 393bee2f8a..3f2f038834 100644 --- a/components/cli/compose/types/types.go +++ b/components/cli/compose/types/types.go @@ -204,6 +204,7 @@ type NetworkConfig struct { DriverOpts map[string]string `mapstructure:"driver_opts"` Ipam IPAMConfig External External + Internal bool Labels map[string]string `compose:"list_or_dict_equals"` } From 23c2c8d9bb1a550bbed62f24bd0ba6d5da9222f4 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Tue, 22 Nov 2016 16:23:21 -0800 Subject: [PATCH 542/978] Add `--format` flag for `docker plugin ls` This fix tries to address the enhancement discussed in 28735 to add `--format` for the output of `docker plugin ls`. This fix 1. Add `--format` and `--quiet` flags to `docker plugin ls` 2. Convert the current implementation to use `formatter`, consistent with other docker list commands. 3. Add `pluginsFormat` for config.json. Related docs has been updated. Several unit tests have been added to cover the changes. This fix is related to 28708 and 28735. Signed-off-by: Yong Tang Upstream-commit: c799b20f5b5cc209e1220c674ed005bba42743ef Component: cli --- components/cli/command/formatter/plugin.go | 87 ++++++++ .../cli/command/formatter/plugin_test.go | 188 ++++++++++++++++++ components/cli/command/plugin/list.go | 39 ++-- components/cli/config/configfile/file.go | 1 + 4 files changed, 294 insertions(+), 21 deletions(-) create mode 100644 components/cli/command/formatter/plugin.go create mode 100644 components/cli/command/formatter/plugin_test.go diff --git a/components/cli/command/formatter/plugin.go b/components/cli/command/formatter/plugin.go new file mode 100644 index 0000000000..5f94714a6b --- /dev/null +++ b/components/cli/command/formatter/plugin.go @@ -0,0 +1,87 @@ +package formatter + +import ( + "strings" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/stringutils" +) + +const ( + defaultPluginTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Description}}\t{{.Enabled}}" + + pluginIDHeader = "ID" + descriptionHeader = "DESCRIPTION" + enabledHeader = "ENABLED" +) + +// NewPluginFormat returns a Format for rendering using a plugin Context +func NewPluginFormat(source string, quiet bool) Format { + switch source { + case TableFormatKey: + if quiet { + return defaultQuietFormat + } + return defaultPluginTableFormat + case RawFormatKey: + if quiet { + return `plugin_id: {{.ID}}` + } + return `plugin_id: {{.ID}}\nname: {{.Name}}\ndescription: {{.Description}}\nenabled: {{.Enabled}}\n` + } + return Format(source) +} + +// PluginWrite writes the context +func PluginWrite(ctx Context, plugins []*types.Plugin) error { + render := func(format func(subContext subContext) error) error { + for _, plugin := range plugins { + pluginCtx := &pluginContext{trunc: ctx.Trunc, p: *plugin} + if err := format(pluginCtx); err != nil { + return err + } + } + return nil + } + return ctx.Write(&pluginContext{}, render) +} + +type pluginContext struct { + HeaderContext + trunc bool + p types.Plugin +} + +func (c *pluginContext) MarshalJSON() ([]byte, error) { + return marshalJSON(c) +} + +func (c *pluginContext) ID() string { + c.AddHeader(pluginIDHeader) + if c.trunc { + return stringid.TruncateID(c.p.ID) + } + return c.p.ID +} + +func (c *pluginContext) Name() string { + c.AddHeader(nameHeader) + return c.p.Name +} + +func (c *pluginContext) Description() string { + c.AddHeader(descriptionHeader) + desc := strings.Replace(c.p.Config.Description, "\n", "", -1) + desc = strings.Replace(desc, "\r", "", -1) + if c.trunc { + desc = stringutils.Ellipsis(desc, 45) + } + + return desc +} + +func (c *pluginContext) Enabled() bool { + c.AddHeader(enabledHeader) + return c.p.Enabled +} diff --git a/components/cli/command/formatter/plugin_test.go b/components/cli/command/formatter/plugin_test.go new file mode 100644 index 0000000000..9ddbe11dff --- /dev/null +++ b/components/cli/command/formatter/plugin_test.go @@ -0,0 +1,188 @@ +package formatter + +import ( + "bytes" + "encoding/json" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestPluginContext(t *testing.T) { + pluginID := stringid.GenerateRandomID() + + var ctx pluginContext + cases := []struct { + pluginCtx pluginContext + expValue string + expHeader string + call func() string + }{ + {pluginContext{ + p: types.Plugin{ID: pluginID}, + trunc: false, + }, pluginID, pluginIDHeader, ctx.ID}, + {pluginContext{ + p: types.Plugin{ID: pluginID}, + trunc: true, + }, stringid.TruncateID(pluginID), pluginIDHeader, ctx.ID}, + {pluginContext{ + p: types.Plugin{Name: "plugin_name"}, + }, "plugin_name", nameHeader, ctx.Name}, + {pluginContext{ + p: types.Plugin{Config: types.PluginConfig{Description: "plugin_description"}}, + }, "plugin_description", descriptionHeader, ctx.Description}, + } + + for _, c := range cases { + ctx = c.pluginCtx + v := c.call() + if strings.Contains(v, ",") { + compareMultipleValues(t, v, c.expValue) + } else if v != c.expValue { + t.Fatalf("Expected %s, was %s\n", c.expValue, v) + } + + h := ctx.FullHeader() + if h != c.expHeader { + t.Fatalf("Expected %s, was %s\n", c.expHeader, h) + } + } +} + +func TestPluginContextWrite(t *testing.T) { + cases := []struct { + context Context + expected string + }{ + + // Errors + { + Context{Format: "{{InvalidFunction}}"}, + `Template parsing error: template: :1: function "InvalidFunction" not defined +`, + }, + { + Context{Format: "{{nil}}"}, + `Template parsing error: template: :1:2: executing "" at : nil is not a command +`, + }, + // Table format + { + Context{Format: NewPluginFormat("table", false)}, + `ID NAME DESCRIPTION ENABLED +pluginID1 foobar_baz description 1 true +pluginID2 foobar_bar description 2 false +`, + }, + { + Context{Format: NewPluginFormat("table", true)}, + `pluginID1 +pluginID2 +`, + }, + { + Context{Format: NewPluginFormat("table {{.Name}}", false)}, + `NAME +foobar_baz +foobar_bar +`, + }, + { + Context{Format: NewPluginFormat("table {{.Name}}", true)}, + `NAME +foobar_baz +foobar_bar +`, + }, + // Raw Format + { + Context{Format: NewPluginFormat("raw", false)}, + `plugin_id: pluginID1 +name: foobar_baz +description: description 1 +enabled: true + +plugin_id: pluginID2 +name: foobar_bar +description: description 2 +enabled: false + +`, + }, + { + Context{Format: NewPluginFormat("raw", true)}, + `plugin_id: pluginID1 +plugin_id: pluginID2 +`, + }, + // Custom Format + { + Context{Format: NewPluginFormat("{{.Name}}", false)}, + `foobar_baz +foobar_bar +`, + }, + } + + for _, testcase := range cases { + plugins := []*types.Plugin{ + {ID: "pluginID1", Name: "foobar_baz", Config: types.PluginConfig{Description: "description 1"}, Enabled: true}, + {ID: "pluginID2", Name: "foobar_bar", Config: types.PluginConfig{Description: "description 2"}, Enabled: false}, + } + out := bytes.NewBufferString("") + testcase.context.Output = out + err := PluginWrite(testcase.context, plugins) + if err != nil { + assert.Error(t, err, testcase.expected) + } else { + assert.Equal(t, out.String(), testcase.expected) + } + } +} + +func TestPluginContextWriteJSON(t *testing.T) { + plugins := []*types.Plugin{ + {ID: "pluginID1", Name: "foobar_baz"}, + {ID: "pluginID2", Name: "foobar_bar"}, + } + expectedJSONs := []map[string]interface{}{ + {"Description": "", "Enabled": false, "ID": "pluginID1", "Name": "foobar_baz"}, + {"Description": "", "Enabled": false, "ID": "pluginID2", "Name": "foobar_bar"}, + } + + out := bytes.NewBufferString("") + err := PluginWrite(Context{Format: "{{json .}}", Output: out}, plugins) + if err != nil { + t.Fatal(err) + } + for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { + var m map[string]interface{} + if err := json.Unmarshal([]byte(line), &m); err != nil { + t.Fatal(err) + } + assert.DeepEqual(t, m, expectedJSONs[i]) + } +} + +func TestPluginContextWriteJSONField(t *testing.T) { + plugins := []*types.Plugin{ + {ID: "pluginID1", Name: "foobar_baz"}, + {ID: "pluginID2", Name: "foobar_bar"}, + } + out := bytes.NewBufferString("") + err := PluginWrite(Context{Format: "{{json .ID}}", Output: out}, plugins) + if err != nil { + t.Fatal(err) + } + for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { + var s string + if err := json.Unmarshal([]byte(line), &s); err != nil { + t.Fatal(err) + } + assert.Equal(t, s, plugins[i].ID) + } +} diff --git a/components/cli/command/plugin/list.go b/components/cli/command/plugin/list.go index 8fd16dae3f..51590224b0 100644 --- a/components/cli/command/plugin/list.go +++ b/components/cli/command/plugin/list.go @@ -1,20 +1,17 @@ package plugin import ( - "fmt" - "strings" - "text/tabwriter" - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" - "github.com/docker/docker/pkg/stringid" - "github.com/docker/docker/pkg/stringutils" + "github.com/docker/docker/cli/command/formatter" "github.com/spf13/cobra" "golang.org/x/net/context" ) type listOptions struct { + quiet bool noTrunc bool + format string } func newListCommand(dockerCli *command.DockerCli) *cobra.Command { @@ -32,7 +29,9 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() + flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display plugin IDs") flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output") + flags.StringVar(&opts.format, "format", "", "Pretty-print plugins using a Go template") return cmd } @@ -43,21 +42,19 @@ func runList(dockerCli *command.DockerCli, opts listOptions) error { return err } - w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0) - fmt.Fprintf(w, "ID \tNAME \tDESCRIPTION\tENABLED") - fmt.Fprintf(w, "\n") - - for _, p := range plugins { - id := p.ID - desc := strings.Replace(p.Config.Description, "\n", " ", -1) - desc = strings.Replace(desc, "\r", " ", -1) - if !opts.noTrunc { - id = stringid.TruncateID(p.ID) - desc = stringutils.Ellipsis(desc, 45) + format := opts.format + if len(format) == 0 { + if len(dockerCli.ConfigFile().PluginsFormat) > 0 && !opts.quiet { + format = dockerCli.ConfigFile().PluginsFormat + } else { + format = formatter.TableFormatKey } - - fmt.Fprintf(w, "%s\t%s\t%s\t%v\n", id, p.Name, desc, p.Enabled) } - w.Flush() - return nil + + pluginsCtx := formatter.Context{ + Output: dockerCli.Out(), + Format: formatter.NewPluginFormat(format, opts.quiet), + Trunc: !opts.noTrunc, + } + return formatter.PluginWrite(pluginsCtx, plugins) } diff --git a/components/cli/config/configfile/file.go b/components/cli/config/configfile/file.go index 39097133a4..e8fe96e847 100644 --- a/components/cli/config/configfile/file.go +++ b/components/cli/config/configfile/file.go @@ -27,6 +27,7 @@ type ConfigFile struct { PsFormat string `json:"psFormat,omitempty"` ImagesFormat string `json:"imagesFormat,omitempty"` NetworksFormat string `json:"networksFormat,omitempty"` + PluginsFormat string `json:"pluginsFormat,omitempty"` VolumesFormat string `json:"volumesFormat,omitempty"` StatsFormat string `json:"statsFormat,omitempty"` DetachKeys string `json:"detachKeys,omitempty"` From abe2284d2824e8394ced8a1f9eabfc21d054a867 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 17 Jan 2017 15:46:07 +0100 Subject: [PATCH 543/978] fix flag descriptions for content-trust Commit ed13c3abfb242905ec012e8255dc6f26dcf122f6 added flags for Docker Content Trust. Depending on the `verify` boolean, the message is "Skip image verification", or "Skip image signing". "Signing" is intended for `docker push` / `docker plugin push`. During the migration to Cobra, this boolean got flipped for `docker push` (9640e3a4514f96a890310757a09fd77a3c70e931), causing `docker push` to show the incorrect flag description. This patch changes the flags to use the correct description for `docker push`, and `docker plugin push`. To prevent this confusion in future, the boolean argument is removed, and a `AddTrustSigningFlags()` function is added. Signed-off-by: Sebastiaan van Stijn Upstream-commit: 62ff1a0ea7738218bf3b2ca1a99e00d8f2af4285 Component: cli --- components/cli/command/container/create.go | 2 +- components/cli/command/container/run.go | 2 +- components/cli/command/image/build.go | 2 +- components/cli/command/image/pull.go | 2 +- components/cli/command/image/push.go | 2 +- components/cli/command/plugin/install.go | 2 +- components/cli/command/plugin/push.go | 2 +- components/cli/command/trust.go | 26 +++++++++++++--------- 8 files changed, 22 insertions(+), 18 deletions(-) diff --git a/components/cli/command/container/create.go b/components/cli/command/container/create.go index 13890d9ef5..787d09b3f6 100644 --- a/components/cli/command/container/create.go +++ b/components/cli/command/container/create.go @@ -52,7 +52,7 @@ func NewCreateCommand(dockerCli *command.DockerCli) *cobra.Command { // with hostname flags.Bool("help", false, "Print usage") - command.AddTrustedFlags(flags, true) + command.AddTrustVerificationFlags(flags) copts = addFlags(flags) return cmd } diff --git a/components/cli/command/container/run.go b/components/cli/command/container/run.go index cbe64548ea..e805ca1a57 100644 --- a/components/cli/command/container/run.go +++ b/components/cli/command/container/run.go @@ -61,7 +61,7 @@ func NewRunCommand(dockerCli *command.DockerCli) *cobra.Command { // with hostname flags.Bool("help", false, "Print usage") - command.AddTrustedFlags(flags, true) + command.AddTrustVerificationFlags(flags) copts = addFlags(flags) return cmd } diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go index 5d6e611406..3c92ba20b9 100644 --- a/components/cli/command/image/build.go +++ b/components/cli/command/image/build.go @@ -108,7 +108,7 @@ func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command { flags.StringSliceVar(&options.securityOpt, "security-opt", []string{}, "Security options") flags.StringVar(&options.networkMode, "network", "default", "Set the networking mode for the RUN instructions during build") - command.AddTrustedFlags(flags, true) + command.AddTrustVerificationFlags(flags) flags.BoolVar(&options.squash, "squash", false, "Squash newly built layers into a single new layer") flags.SetAnnotation("squash", "experimental", nil) diff --git a/components/cli/command/image/pull.go b/components/cli/command/image/pull.go index 24933fe846..e840671c62 100644 --- a/components/cli/command/image/pull.go +++ b/components/cli/command/image/pull.go @@ -36,7 +36,7 @@ func NewPullCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() flags.BoolVarP(&opts.all, "all-tags", "a", false, "Download all tagged images in the repository") - command.AddTrustedFlags(flags, true) + command.AddTrustVerificationFlags(flags) return cmd } diff --git a/components/cli/command/image/push.go b/components/cli/command/image/push.go index a8ce4945ec..a5ba7d794e 100644 --- a/components/cli/command/image/push.go +++ b/components/cli/command/image/push.go @@ -24,7 +24,7 @@ func NewPushCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() - command.AddTrustedFlags(flags, true) + command.AddTrustSigningFlags(flags) return cmd } diff --git a/components/cli/command/plugin/install.go b/components/cli/command/plugin/install.go index a64dc2525a..fd30600370 100644 --- a/components/cli/command/plugin/install.go +++ b/components/cli/command/plugin/install.go @@ -47,7 +47,7 @@ func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command { flags.BoolVar(&options.disable, "disable", false, "Do not enable the plugin on install") flags.StringVar(&options.alias, "alias", "", "Local name for plugin") - command.AddTrustedFlags(flags, true) + command.AddTrustVerificationFlags(flags) return cmd } diff --git a/components/cli/command/plugin/push.go b/components/cli/command/plugin/push.go index b0766307f3..1a9c592a93 100644 --- a/components/cli/command/plugin/push.go +++ b/components/cli/command/plugin/push.go @@ -26,7 +26,7 @@ func newPushCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() - command.AddTrustedFlags(flags, true) + command.AddTrustSigningFlags(flags) return cmd } diff --git a/components/cli/command/trust.go b/components/cli/command/trust.go index b4c8a84ee5..c0742bc5b2 100644 --- a/components/cli/command/trust.go +++ b/components/cli/command/trust.go @@ -12,13 +12,20 @@ var ( untrusted bool ) -// AddTrustedFlags adds content trust flags to the current command flagset -func AddTrustedFlags(fs *pflag.FlagSet, verify bool) { - trusted, message := setupTrustedFlag(verify) - fs.BoolVar(&untrusted, "disable-content-trust", !trusted, message) +// AddTrustVerificationFlags adds content trust flags to the provided flagset +func AddTrustVerificationFlags(fs *pflag.FlagSet) { + trusted := getDefaultTrustState() + fs.BoolVar(&untrusted, "disable-content-trust", !trusted, "Skip image verification") } -func setupTrustedFlag(verify bool) (bool, string) { +// AddTrustSigningFlags adds "signing" flags to the provided flagset +func AddTrustSigningFlags(fs *pflag.FlagSet) { + trusted := getDefaultTrustState() + fs.BoolVar(&untrusted, "disable-content-trust", !trusted, "Skip image signing") +} + +// getDefaultTrustState returns true if content trust is enabled through the $DOCKER_CONTENT_TRUST environment variable. +func getDefaultTrustState() bool { var trusted bool if e := os.Getenv("DOCKER_CONTENT_TRUST"); e != "" { if t, err := strconv.ParseBool(e); t || err != nil { @@ -26,14 +33,11 @@ func setupTrustedFlag(verify bool) (bool, string) { trusted = true } } - message := "Skip image signing" - if verify { - message = "Skip image verification" - } - return trusted, message + return trusted } -// IsTrusted returns true if content trust is enabled +// IsTrusted returns true if content trust is enabled, either through the $DOCKER_CONTENT_TRUST environment variable, +// or through `--disabled-content-trust=false` on a command. func IsTrusted() bool { return !untrusted } From 4c7cf08fd0fcd587ae6c33a21a7c156349c862b3 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Thu, 19 Jan 2017 10:17:56 -0800 Subject: [PATCH 544/978] Fix failure in `docker ps --format` when `.Label` has args This fix tries to fix the issue in 30279 where `docker ps --format` fails if `.Label` has args. For example: ``` docker ps --format '{{.ID}}\t{{.Names}}\t{{.Label "some.label"}}' ``` The reason for the failure is that during the preprocessing phase to detect the existance of `.Size`, the `listOptionsProcessor` does not has a method of `Label(name string) string`. This results in the failure of ``` template: :1:24: executing "" at <.Label>: Label is not a method but has arguments ``` This fix fixes the issue by adding needed method of `Label(name string) string`. This fix fixes 30279. Signed-off-by: Yong Tang Upstream-commit: a2afbcbb57a25009081c9cd867416a98215baecd Component: cli --- components/cli/command/container/list.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/components/cli/command/container/list.go b/components/cli/command/container/list.go index 451c531a8b..e0f4fdf21f 100644 --- a/components/cli/command/container/list.go +++ b/components/cli/command/container/list.go @@ -73,6 +73,12 @@ func (o listOptionsProcessor) Size() bool { return true } +// Label is needed here as it allows the correct pre-processing +// because Label() is a method with arguments +func (o listOptionsProcessor) Label(name string) string { + return "" +} + func buildContainerListOptions(opts *psOptions) (*types.ContainerListOptions, error) { options := &types.ContainerListOptions{ All: opts.all, From a84143df5aac0e129fbccaa58b61d8b77510ebcc Mon Sep 17 00:00:00 2001 From: John Howard Date: Wed, 11 Jan 2017 15:55:43 -0800 Subject: [PATCH 545/978] Windows: Use sequential file access Signed-off-by: John Howard Upstream-commit: 0964347819b79d0e8bd5d21a1b1715cec2cf856c Component: cli --- components/cli/command/utils.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/components/cli/command/utils.go b/components/cli/command/utils.go index 1837ca41f0..f9255cf87f 100644 --- a/components/cli/command/utils.go +++ b/components/cli/command/utils.go @@ -3,16 +3,19 @@ package command import ( "fmt" "io" - "io/ioutil" "os" "path/filepath" "runtime" "strings" + + "github.com/docker/docker/pkg/system" ) // CopyToFile writes the content of the reader to the specified file func CopyToFile(outfile string, r io.Reader) error { - tmpFile, err := ioutil.TempFile(filepath.Dir(outfile), ".docker_temp_") + // We use sequential file access here to avoid depleting the standby list + // on Windows. On Linux, this is a call directly to ioutil.TempFile + tmpFile, err := system.TempFileSequential(filepath.Dir(outfile), ".docker_temp_") if err != nil { return err } From c55820cad879616e5d219937f9f1caf5dac3ff99 Mon Sep 17 00:00:00 2001 From: Kenfe-Mickael Laventure Date: Mon, 23 Jan 2017 13:52:33 -0800 Subject: [PATCH 546/978] Ensure proper value is used when computing reclaimable space When Size was reverted to be equal to VirtualSize, the df command formatter was not correctly updated to account for the change. Signed-off-by: Kenfe-Mickael Laventure Upstream-commit: 3494b518a5cb080da11096aac513b37cb545cc44 Component: cli --- components/cli/command/formatter/disk_usage.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/components/cli/command/formatter/disk_usage.go b/components/cli/command/formatter/disk_usage.go index ff1ab768c0..dc5eec41d7 100644 --- a/components/cli/command/formatter/disk_usage.go +++ b/components/cli/command/formatter/disk_usage.go @@ -196,7 +196,10 @@ func (c *diskUsageImagesContext) Reclaimable() string { c.AddHeader(reclaimableHeader) for _, i := range c.images { if i.Containers != 0 { - used += i.Size + if i.VirtualSize == -1 || i.SharedSize == -1 { + continue + } + used += i.VirtualSize - i.SharedSize } } From a578545068823a8e936598b3a9610ff37870b8d6 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 24 Jan 2017 14:19:31 +0100 Subject: [PATCH 547/978] do not ignore local build-contexts starting with "github.com" Docker special-cases build-contexts starting with `github.com`, and treats them as remote URLs. Because of this special treatment, local build contexts in a directory named "github.com" are ignored by `docker build`. This patch changes the way the build-context is detected and first checks if a local path with the given name exists before considering it to be a remote URL. Before this change; $ mkdir -p github.com/foo/bar && echo -e "FROM scratch\nLABEL iam=local" > github.com/foo/bar/Dockerfile $ docker build -t dont-ignore-me github.com/foo/bar Username for 'https://github.com': After this change; $ mkdir -p github.com/foo/bar && echo -e "FROM scratch\nLABEL iam=local" > github.com/foo/bar/Dockerfile $ docker build -t dont-ignore-me github.com/foo/bar Sending build context to Docker daemon 2.048 kB Step 1/2 : FROM scratch ---> Step 2/2 : LABEL iam local ---> Using cache ---> ae2c603fe970 Successfully built ae2c603fe970 Signed-off-by: Sebastiaan van Stijn Upstream-commit: bfd9613e5f97f8ecc9bc3b0033fc0b6ce271fba4 Component: cli --- components/cli/command/image/build.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go index 3c92ba20b9..fe903c74ef 100644 --- a/components/cli/command/image/build.go +++ b/components/cli/command/image/build.go @@ -156,12 +156,14 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { switch { case specifiedContext == "-": buildCtx, relDockerfile, err = build.GetContextFromReader(dockerCli.In(), options.dockerfileName) + case isLocalDir(specifiedContext): + contextDir, relDockerfile, err = build.GetContextFromLocalDir(specifiedContext, options.dockerfileName) case urlutil.IsGitURL(specifiedContext): tempDir, relDockerfile, err = build.GetContextFromGitURL(specifiedContext, options.dockerfileName) case urlutil.IsURL(specifiedContext): buildCtx, relDockerfile, err = build.GetContextFromURL(progBuff, specifiedContext, options.dockerfileName) default: - contextDir, relDockerfile, err = build.GetContextFromLocalDir(specifiedContext, options.dockerfileName) + return fmt.Errorf("unable to prepare context: path %q not found", specifiedContext) } if err != nil { @@ -356,6 +358,11 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { return nil } +func isLocalDir(c string) bool { + _, err := os.Stat(c) + return err == nil +} + type translatorFunc func(context.Context, reference.NamedTagged) (reference.Canonical, error) // validateTag checks if the given image name can be resolved. From e80af1f403deb129173be5c276920bafa666a032 Mon Sep 17 00:00:00 2001 From: Kenfe-Mickael Laventure Date: Fri, 6 May 2016 15:09:46 -0700 Subject: [PATCH 548/978] Allow adding rules to cgroup devices.allow on container create/run This introduce a new `--device-cgroup-rule` flag that allow a user to add one or more entry to the container cgroup device `devices.allow` Signed-off-by: Kenfe-Mickael Laventure Upstream-commit: f33bf818a2af8acc3095a17a70ab8ac3a36e2578 Component: cli --- components/cli/command/container/opts.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/components/cli/command/container/opts.go b/components/cli/command/container/opts.go index 55cc3c3b29..eabf9faca9 100644 --- a/components/cli/command/container/opts.go +++ b/components/cli/command/container/opts.go @@ -6,6 +6,7 @@ import ( "fmt" "io/ioutil" "path" + "regexp" "strconv" "strings" "time" @@ -21,6 +22,10 @@ import ( "github.com/spf13/pflag" ) +var ( + deviceCgroupRuleRegexp = regexp.MustCompile("^[acb] ([0-9]+|\\*):([0-9]+|\\*) [rwm]{1,3}$") +) + // containerOptions is a data object with all the options for creating a container type containerOptions struct { attach opts.ListOpts @@ -36,6 +41,7 @@ type containerOptions struct { deviceWriteIOps opts.ThrottledeviceOpt env opts.ListOpts labels opts.ListOpts + deviceCgroupRules opts.ListOpts devices opts.ListOpts ulimits *opts.UlimitOpt sysctls *opts.MapOpts @@ -127,6 +133,7 @@ func addFlags(flags *pflag.FlagSet) *containerOptions { dns: opts.NewListOpts(opts.ValidateIPAddress), dnsOptions: opts.NewListOpts(nil), dnsSearch: opts.NewListOpts(opts.ValidateDNSSearch), + deviceCgroupRules: opts.NewListOpts(validateDeviceCgroupRule), deviceReadBps: opts.NewThrottledeviceOpt(opts.ValidateThrottleBpsDevice), deviceReadIOps: opts.NewThrottledeviceOpt(opts.ValidateThrottleIOpsDevice), deviceWriteBps: opts.NewThrottledeviceOpt(opts.ValidateThrottleBpsDevice), @@ -154,6 +161,7 @@ func addFlags(flags *pflag.FlagSet) *containerOptions { // General purpose flags flags.VarP(&copts.attach, "attach", "a", "Attach to STDIN, STDOUT or STDERR") + flags.Var(&copts.deviceCgroupRules, "device-cgroup-rule", "Add a rule to the cgroup allowed devices list") flags.Var(&copts.devices, "device", "Add a host device to the container") flags.VarP(&copts.env, "env", "e", "Set environment variables") flags.Var(&copts.envFile, "env-file", "Read in a file of environment variables") @@ -548,6 +556,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*container.Config, *c IOMaximumIOps: copts.ioMaxIOps, IOMaximumBandwidth: uint64(maxIOBandwidth), Ulimits: copts.ulimits.GetList(), + DeviceCgroupRules: copts.deviceCgroupRules.GetAll(), Devices: deviceMappings, } @@ -762,6 +771,17 @@ func parseDevice(device string) (container.DeviceMapping, error) { return deviceMapping, nil } +// validateDeviceCgroupRule validates a device cgroup rule string format +// It will make sure 'val' is in the form: +// 'type major:minor mode' +func validateDeviceCgroupRule(val string) (string, error) { + if deviceCgroupRuleRegexp.MatchString(val) { + return val, nil + } + + return val, fmt.Errorf("invalid device cgroup format '%s'", val) +} + // validDeviceMode checks if the mode for device is valid or not. // Valid mode is a composition of r (read), w (write), and m (mknod). func validDeviceMode(mode string) bool { From fb3da331a1c843e2a305f5788f655f9cfcdcf02b Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 10 Jan 2017 17:10:53 -0500 Subject: [PATCH 549/978] Add v3.1 schema and support validating multiple version. Signed-off-by: Daniel Nephin Upstream-commit: 7215ebffa872ed6b34aa14e63bd4fd0fc46c0717 Component: cli --- components/cli/compose/loader/loader.go | 7 +- components/cli/compose/schema/bindata.go | 27 +- .../schema/data/config_schema_v3.1.json | 426 ++++++++++++++++++ components/cli/compose/schema/schema.go | 30 +- components/cli/compose/schema/schema_test.go | 47 +- 5 files changed, 511 insertions(+), 26 deletions(-) create mode 100644 components/cli/compose/schema/data/config_schema_v3.1.json diff --git a/components/cli/compose/loader/loader.go b/components/cli/compose/loader/loader.go index 9e46b97594..c9554a4b44 100644 --- a/components/cli/compose/loader/loader.go +++ b/components/cli/compose/loader/loader.go @@ -62,16 +62,11 @@ func Load(configDetails types.ConfigDetails) (*types.Config, error) { } } - if err := schema.Validate(configDict); err != nil { + if err := schema.Validate(configDict, schema.Version(configDict)); err != nil { return nil, err } cfg := types.Config{} - version := configDict["version"].(string) - if version != "3" && version != "3.0" { - return nil, fmt.Errorf(`Unsupported Compose file version: %#v. The only version supported is "3" (or "3.0")`, version) - } - if services, ok := configDict["services"]; ok { servicesConfig, err := interpolation.Interpolate(services.(types.Dict), "service", os.LookupEnv) if err != nil { diff --git a/components/cli/compose/schema/bindata.go b/components/cli/compose/schema/bindata.go index c976509355..6d900e0a9a 100644 --- a/components/cli/compose/schema/bindata.go +++ b/components/cli/compose/schema/bindata.go @@ -1,6 +1,7 @@ // Code generated by go-bindata. // sources: // data/config_schema_v3.0.json +// data/config_schema_v3.1.json // DO NOT EDIT! package schema @@ -88,6 +89,26 @@ func dataConfig_schema_v30Json() (*asset, error) { return a, nil } +var _dataConfig_schema_v31Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x1a\x4d\x93\xdb\xa8\xf2\xee\x5f\xa1\x52\x72\x8b\x67\x26\xaf\x5e\xea\x55\xbd\xdc\xde\xf1\x9d\x76\xcf\x3b\xe5\xa8\xb0\xd4\x96\xc9\x20\x20\x80\x9c\x71\x52\xfe\xef\x5b\xe8\xcb\x80\x41\x60\x5b\xd9\xa4\x6a\xf7\x34\x63\xd1\xdd\xf4\x77\x37\x0d\xdf\x57\x59\x96\xbf\x95\xe5\x1e\x1a\x94\x7f\xcc\xf2\xbd\x52\xfc\xe3\xd3\xd3\x67\xc9\xe8\x43\xff\xf5\x91\x89\xfa\xa9\x12\x68\xa7\x1e\xde\x7f\x78\xea\xbf\xbd\xc9\xd7\x1a\x0f\x57\x1a\xa5\x64\x74\x87\xeb\xa2\x5f\x29\x0e\xff\x7e\xfc\xd7\xa3\x46\xef\x41\xd4\x91\x83\x06\x62\xdb\xcf\x50\xaa\xfe\x9b\x80\x2f\x2d\x16\xa0\x91\x9f\xf3\x03\x08\x89\x19\xcd\x37\xeb\x95\x5e\xe3\x82\x71\x10\x0a\x83\xcc\x3f\x66\x9a\xb9\x2c\x9b\x40\xc6\x0f\x06\x59\xa9\x04\xa6\x75\xde\x7d\x3e\x75\x14\xb2\x2c\x97\x20\x0e\xb8\x34\x28\x4c\xac\xbe\x79\x3a\xd3\x7f\x9a\xc0\xd6\x2e\x55\x83\xd9\xee\x3b\x47\x4a\x81\xa0\xbf\x5f\xf2\xd6\x2d\x7f\x7a\x46\x0f\xdf\xfe\xf7\xf0\xc7\xfb\x87\xff\x3e\x16\x0f\x9b\x77\x6f\xad\x65\xad\x5f\x01\xbb\x7e\xfb\x0a\x76\x98\x62\x85\x19\x9d\xf6\xcf\x27\xc8\xd3\xf0\xdf\x69\xda\x18\x55\x55\x07\x8c\x88\xb5\xf7\x0e\x11\x09\xb6\xcc\x14\xd4\x57\x26\x5e\x62\x32\x4f\x60\x3f\x49\xe6\x61\x7f\x8f\xcc\xb6\x38\x07\x46\xda\x26\x6a\xc1\x11\xea\x27\x09\xd3\x6f\xbf\x8c\xfd\x24\x94\x02\x54\xdc\x65\x7b\xa8\x9f\xe6\xb1\x7a\xfb\xfb\x04\x5e\x8d\x42\xcf\xc2\xf6\x10\xc6\xde\x1d\x83\x56\x78\xfb\x54\xe5\x0b\xaf\xb0\xae\x26\x65\x05\xb4\x54\x01\x27\xec\xa8\xbf\x05\xf4\xd1\x03\x34\x40\x55\x3e\xa9\x20\xcb\xf2\x6d\x8b\x49\xe5\x6a\x94\x51\xf8\x4d\x93\x78\x36\x3e\x66\xd9\x77\x37\x93\x19\x74\xba\x75\xeb\x57\xd8\xe0\xd3\x7a\x40\x96\x69\xbd\x64\x54\xc1\xab\xea\x84\x9a\xdf\xba\x57\x01\x2b\x5f\x40\xec\x30\x81\x54\x0c\x24\x6a\x39\xa3\x32\x82\xa5\x2a\x98\x28\x2a\x5c\xaa\xfc\xe4\xa0\x5f\xd0\x8b\xfb\xd3\x84\x6a\xfc\xda\xac\x3c\x04\xf3\x12\xf1\x02\x55\x95\x25\x07\x12\x02\x1d\xf3\x75\x96\x63\x05\x8d\xf4\x8b\x98\xe5\x2d\xc5\x5f\x5a\xf8\xff\x00\xa2\x44\x0b\x2e\xdd\x4a\x30\xbe\x3c\xe1\x5a\xb0\x96\x17\x1c\x09\xed\x60\xf3\xea\xcf\x4b\xd6\x34\x88\x2e\xe5\x75\xd7\xc8\x91\xa0\x79\x46\x15\xc2\x14\x44\x41\x51\x13\x73\x24\x1d\x75\x40\x2b\x59\xf4\x05\x7f\xd6\x8d\x76\x45\x8f\x2f\x1d\x02\x53\xf5\x5f\xd4\x1e\x15\x9d\x73\xec\x9e\x8c\x76\x6d\xcd\x5b\xee\x20\x16\x12\x90\x28\xf7\x37\xe2\xb3\x06\x61\x9a\xa2\x3b\xa0\x4a\x1c\x39\xc3\xbd\xbf\xfc\x72\x8e\x00\xf4\x50\x4c\xb9\xe4\x6a\x35\x00\x3d\x60\xc1\x68\x33\x46\x43\x4a\x82\x99\x92\xbc\xc6\x7f\xe5\x4c\x82\xab\x18\x47\x40\x73\x69\x12\xd5\xd2\xc9\x88\xf1\x3c\x0a\xbe\xce\x72\xda\x36\x5b\x10\xba\x87\xb5\x20\x77\x4c\x34\x48\x33\x3b\xee\x6d\x2c\x5b\x9a\xf6\x78\x9e\xa9\x40\x53\x06\x5d\xd6\x11\x29\x08\xa6\x2f\xcb\xbb\x38\xbc\x2a\x81\x8a\x3d\x93\x2a\x3d\x87\x1b\xe8\x7b\x40\x44\xed\xcb\x3d\x94\x2f\x33\xe8\x26\x94\x85\xcd\xa4\x4a\x71\x72\xdc\xa0\x3a\x0e\xc4\xcb\x18\x08\x41\x5b\x20\x37\xc9\xb9\xa8\xf2\x0d\xb2\xac\xae\x35\x68\xc8\xe3\x2e\x3a\x97\x61\x39\x56\xf3\x2b\x81\x0f\x20\x52\x0b\x38\xe3\xe7\x86\xcb\x5d\x8c\x37\x20\x59\xbc\xfb\xb4\x40\x3f\x3d\xf6\xcd\xe7\x4c\x54\x75\xff\x11\x92\x6f\xdc\x76\x21\x73\xea\xbe\xef\x8b\x23\x61\x5a\x43\x61\x59\xa5\x41\xa5\xee\x1b\x04\xc8\x80\x5d\xcf\xa0\xc3\xe9\xa6\x68\x58\x15\x72\xd0\x0b\x60\x57\x37\xc1\x4c\x7d\x75\x21\xcc\x6e\xea\x1f\x93\x4c\x17\x3d\x40\x44\xa4\x09\xb1\x97\xca\xe6\x99\xdd\xb8\x8b\x75\x70\x88\x60\x24\x21\x1e\xec\x41\x45\x5a\xd4\x30\x3f\x7c\x48\xf4\x09\x1f\xee\x7f\x66\x71\x03\xa8\x41\x9a\xe9\x3d\x72\x84\xd4\x99\x95\x2e\xdc\x7c\x8c\x6c\x22\xd1\xf6\x83\x5b\x78\x8e\xab\x70\xae\xe8\x32\x84\x19\x60\x9c\x09\x75\x11\x5d\x7f\x4d\xb9\xef\xb7\xbe\xbb\xda\x73\x81\x0f\x98\x40\x0d\xf6\xa9\x65\xcb\x18\x01\x44\xad\xd4\x23\x00\x55\x05\xa3\xe4\x98\x00\x29\x15\x12\xd1\x03\x85\x84\xb2\x15\x58\x1d\x0b\xc6\xd5\xe2\x7d\x86\xdc\x37\x85\xc4\xdf\xc0\xb6\xe6\x39\xdf\x0f\x84\x36\x0e\x43\xce\x84\xe4\x46\x83\x86\x52\x52\x3c\x8c\x3d\x89\x30\x9a\xa8\xe2\x29\x2a\x97\xac\x15\x65\xea\x01\x5b\xef\x89\x44\x0d\xa9\x47\x78\xed\x6e\x76\xd8\xcc\x03\xd7\xd7\x00\x5f\x14\xba\xc1\x84\xb1\xaa\xec\xfe\x36\xf3\xca\xc9\x1b\xfa\xf2\x28\x4b\x75\x5b\xb7\x26\x55\x85\x69\xc1\x38\xd0\x68\x6c\x48\xc5\x78\x21\x71\x4d\x11\x89\xc6\x87\x06\xad\x05\x2a\xa1\xe0\x20\x30\xf3\x6a\x6d\x6d\x26\x85\xaa\x15\x48\xb3\x6a\x91\x51\x0d\xdf\xdd\x78\xac\x54\x2a\x1e\xec\x2d\xc1\x0d\x0e\x07\x8d\xc7\x6b\x13\x3a\x80\xbe\xfa\xfb\x8b\xfe\x4c\xc1\x3f\x73\x8a\xa9\x82\x5a\xbb\xc9\xa5\x53\xcd\xf4\x9c\xf3\x2d\x67\x42\xaf\xb9\x47\xc2\xb6\xd2\x0c\x1f\x59\x1f\x98\x3b\xe5\x47\xf0\x75\xa2\x5e\xbe\xac\xbb\x8e\x8e\xde\x7a\x60\x64\xe3\x85\xbf\xaa\x98\xbb\x6c\x6c\x82\xf5\xd4\x1f\x54\xad\x8c\x1e\x0b\x3a\x18\x2a\xe7\x5a\xda\x09\xd4\x18\xda\x2f\x5a\x2d\x74\x9b\xac\x83\xa0\xc2\x7e\x6e\x57\x8e\x64\x57\x8c\xdd\x9d\x13\xeb\x48\xc0\x37\x4f\x36\x41\xdd\x99\xf2\xf3\xe4\x9b\x63\x27\x72\x9e\xc4\x07\x86\xcb\xda\x95\xc4\xc1\xca\x33\x3e\x9d\x2a\xdc\x00\x6b\x55\x04\x4a\x80\x12\xd8\xd1\xfc\x98\x8a\x4d\x62\x20\x7f\xcd\xc1\x50\x85\x25\xda\x3a\x33\xe6\x29\x9d\xdd\x64\xde\xec\x3c\xc0\x1f\x07\x46\x73\xc6\x35\x20\x17\xb0\x6d\x4a\xb0\x08\xe0\x04\x97\x48\xc6\x12\xd2\x1d\x63\x8a\x96\x57\x48\x41\xd1\xdf\xcf\x5e\x55\x02\x66\x72\x3f\x47\x02\x11\x02\x04\xcb\x26\x25\x97\xe6\x15\x10\x74\xbc\xa9\x36\x76\xe8\x3b\x84\x49\x2b\xa0\x40\xa5\x1a\xae\x80\x23\x9e\x99\x37\x8c\x62\xc5\xbc\x99\x22\x6d\xcb\x06\xbd\x16\xe3\xb6\x1d\x48\xac\xc3\xb1\x9b\xfb\xd4\x09\x83\xe1\x09\x7d\x03\x78\x5d\x95\x9e\x31\xd1\xb9\xe6\x07\x3c\x66\xdc\xf1\x42\x74\x01\x52\x27\xa5\x69\x00\x14\xc5\x8f\x96\x98\xe1\xb4\x51\x70\x46\x70\x79\x5c\x4a\xc2\x92\xd1\x5e\xc9\x29\x0e\x71\xa7\x07\x6a\x77\xd0\x2d\x51\xc3\x55\x34\x58\x3b\x84\xaf\x98\x56\xec\xeb\x15\x1b\x2e\xe7\x4a\x9c\xa0\x12\x9c\x7c\x77\xaf\xa2\xa5\x12\x08\x53\x75\x75\x59\xbf\x57\xac\x3b\xaa\xfa\xe4\x9f\x91\xac\x3f\xc1\xc5\xef\xd3\x03\x99\xbe\xe4\x6d\x74\x2a\xd8\x40\xc3\x84\xd7\x01\x17\x78\xf0\x11\x13\x71\x04\x5b\xa0\xaa\x25\x8d\x91\x07\xa8\x82\xf1\xe5\x4f\x1d\xf1\x51\xf1\x26\x9e\x90\x30\x47\xcd\x52\xd1\x91\x3c\x58\xcf\xbd\x35\x38\x9b\x9f\x5f\x64\xe1\x19\x46\x8c\xeb\x38\xef\x03\x84\x6c\xb7\x34\x30\x4a\xb8\x3c\x6d\xf8\x6e\xfb\xd3\x8f\x2b\xa7\xf0\xe1\xe4\xbe\xa4\x37\xde\x89\x05\xac\xfa\x3c\x75\x92\xeb\x49\x57\x9b\x64\x13\x07\x2f\xa4\x96\xe3\xff\xca\x06\xef\x8e\x9c\x31\x3c\x58\x8a\xa4\x8c\x01\xea\x9f\x8c\xf1\xcb\xf8\xd7\x4c\x51\xbc\xf1\x74\x70\xf5\xcb\xb4\x98\xd3\x0c\x50\x37\x17\xd2\x84\x27\x46\x7f\x7b\x43\xd8\xa3\x40\xc3\x20\x97\x67\xf8\x39\x3d\x26\xdf\x80\x0d\x18\x1b\x9b\x0d\x17\xcc\xf3\x2a\xd7\xae\x65\x73\xa3\x9f\x11\x24\x70\x23\xe2\x6c\x3a\x28\x6f\x5e\xf2\x05\xf3\xc7\xe3\xbb\x99\x8a\x3d\x77\x53\xfd\x83\x4a\xdd\x02\x63\x35\xbf\x4d\x9d\x36\x7f\xd4\xee\xe5\x4b\xcb\x40\xf4\x1b\xf8\x17\xef\x2e\xb5\x9c\xf4\x78\x31\x63\xfa\x6e\x8f\x46\xfb\x37\x93\x1b\x4b\x3f\x0e\x48\xff\xee\xc3\x48\xd8\x1b\xf3\xe4\x13\x32\xa3\xf7\x35\xa6\x3b\x98\x1d\x5f\x45\x06\xee\x21\x56\xe6\xdf\xee\x05\xeb\xea\xb4\xfa\x33\x00\x00\xff\xff\xb7\x14\xdd\xc9\x3a\x2f\x00\x00") + +func dataConfig_schema_v31JsonBytes() ([]byte, error) { + return bindataRead( + _dataConfig_schema_v31Json, + "data/config_schema_v3.1.json", + ) +} + +func dataConfig_schema_v31Json() (*asset, error) { + bytes, err := dataConfig_schema_v31JsonBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "data/config_schema_v3.1.json", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + // Asset loads and returns the asset for the given name. // It returns an error if the asset could not be found or // could not be loaded. @@ -141,6 +162,7 @@ func AssetNames() []string { // _bindata is a table, holding each asset generator, mapped to its name. var _bindata = map[string]func() (*asset, error){ "data/config_schema_v3.0.json": dataConfig_schema_v30Json, + "data/config_schema_v3.1.json": dataConfig_schema_v31Json, } // AssetDir returns the file names below a certain @@ -183,8 +205,9 @@ type bintree struct { Children map[string]*bintree } var _bintree = &bintree{nil, map[string]*bintree{ - "data": &bintree{nil, map[string]*bintree{ - "config_schema_v3.0.json": &bintree{dataConfig_schema_v30Json, map[string]*bintree{}}, + "data": {nil, map[string]*bintree{ + "config_schema_v3.0.json": {dataConfig_schema_v30Json, map[string]*bintree{}}, + "config_schema_v3.1.json": {dataConfig_schema_v31Json, map[string]*bintree{}}, }}, }} diff --git a/components/cli/compose/schema/data/config_schema_v3.1.json b/components/cli/compose/schema/data/config_schema_v3.1.json new file mode 100644 index 0000000000..c43f296b55 --- /dev/null +++ b/components/cli/compose/schema/data/config_schema_v3.1.json @@ -0,0 +1,426 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "config_schema_v3.1.json", + "type": "object", + "required": ["version"], + + "properties": { + "version": { + "type": "string" + }, + + "services": { + "id": "#/properties/services", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "$ref": "#/definitions/service" + } + }, + "additionalProperties": false + }, + + "networks": { + "id": "#/properties/networks", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "$ref": "#/definitions/network" + } + } + }, + + "volumes": { + "id": "#/properties/volumes", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "$ref": "#/definitions/volume" + } + }, + "additionalProperties": false + }, + + "secrets": { + "id": "#/properties/secrets", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "$ref": "#/definitions/secret" + } + }, + "additionalProperties": false + } + }, + + "additionalProperties": false, + + "definitions": { + + "service": { + "id": "#/definitions/service", + "type": "object", + + "properties": { + "deploy": {"$ref": "#/definitions/deployment"}, + "build": { + "oneOf": [ + {"type": "string"}, + { + "type": "object", + "properties": { + "context": {"type": "string"}, + "dockerfile": {"type": "string"}, + "args": {"$ref": "#/definitions/list_or_dict"} + }, + "additionalProperties": false + } + ] + }, + "cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "cgroup_parent": {"type": "string"}, + "command": { + "oneOf": [ + {"type": "string"}, + {"type": "array", "items": {"type": "string"}} + ] + }, + "container_name": {"type": "string"}, + "depends_on": {"$ref": "#/definitions/list_of_strings"}, + "devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "dns": {"$ref": "#/definitions/string_or_list"}, + "dns_search": {"$ref": "#/definitions/string_or_list"}, + "domainname": {"type": "string"}, + "entrypoint": { + "oneOf": [ + {"type": "string"}, + {"type": "array", "items": {"type": "string"}} + ] + }, + "env_file": {"$ref": "#/definitions/string_or_list"}, + "environment": {"$ref": "#/definitions/list_or_dict"}, + + "expose": { + "type": "array", + "items": { + "type": ["string", "number"], + "format": "expose" + }, + "uniqueItems": true + }, + + "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "extra_hosts": {"$ref": "#/definitions/list_or_dict"}, + "healthcheck": {"$ref": "#/definitions/healthcheck"}, + "hostname": {"type": "string"}, + "image": {"type": "string"}, + "ipc": {"type": "string"}, + "labels": {"$ref": "#/definitions/list_or_dict"}, + "links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + + "logging": { + "type": "object", + + "properties": { + "driver": {"type": "string"}, + "options": { + "type": "object", + "patternProperties": { + "^.+$": {"type": ["string", "number", "null"]} + } + } + }, + "additionalProperties": false + }, + + "mac_address": {"type": "string"}, + "network_mode": {"type": "string"}, + + "networks": { + "oneOf": [ + {"$ref": "#/definitions/list_of_strings"}, + { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "oneOf": [ + { + "type": "object", + "properties": { + "aliases": {"$ref": "#/definitions/list_of_strings"}, + "ipv4_address": {"type": "string"}, + "ipv6_address": {"type": "string"} + }, + "additionalProperties": false + }, + {"type": "null"} + ] + } + }, + "additionalProperties": false + } + ] + }, + "pid": {"type": ["string", "null"]}, + + "ports": { + "type": "array", + "items": { + "type": ["string", "number"], + "format": "ports" + }, + "uniqueItems": true + }, + + "privileged": {"type": "boolean"}, + "read_only": {"type": "boolean"}, + "restart": {"type": "string"}, + "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "shm_size": {"type": ["number", "string"]}, + "secrets": { + "type": "array", + "items": { + "oneOf": [ + {"type": "string"}, + { + "type": "object", + "properties": { + "source": {"type": "string"}, + "target": {"type": "string"}, + "uid": {"type": "string"}, + "gid": {"type": "string"}, + "mode": {"type": "number"} + } + } + ] + } + }, + "sysctls": {"$ref": "#/definitions/list_or_dict"}, + "stdin_open": {"type": "boolean"}, + "stop_signal": {"type": "string"}, + "stop_grace_period": {"type": "string", "format": "duration"}, + "tmpfs": {"$ref": "#/definitions/string_or_list"}, + "tty": {"type": "boolean"}, + "ulimits": { + "type": "object", + "patternProperties": { + "^[a-z]+$": { + "oneOf": [ + {"type": "integer"}, + { + "type":"object", + "properties": { + "hard": {"type": "integer"}, + "soft": {"type": "integer"} + }, + "required": ["soft", "hard"], + "additionalProperties": false + } + ] + } + } + }, + "user": {"type": "string"}, + "userns_mode": {"type": "string"}, + "volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "working_dir": {"type": "string"} + }, + "additionalProperties": false + }, + + "healthcheck": { + "id": "#/definitions/healthcheck", + "type": ["object", "null"], + "properties": { + "interval": {"type":"string"}, + "timeout": {"type":"string"}, + "retries": {"type": "number"}, + "test": { + "oneOf": [ + {"type": "string"}, + {"type": "array", "items": {"type": "string"}} + ] + }, + "disable": {"type": "boolean"} + }, + "additionalProperties": false + }, + "deployment": { + "id": "#/definitions/deployment", + "type": ["object", "null"], + "properties": { + "mode": {"type": "string"}, + "replicas": {"type": "integer"}, + "labels": {"$ref": "#/definitions/list_or_dict"}, + "update_config": { + "type": "object", + "properties": { + "parallelism": {"type": "integer"}, + "delay": {"type": "string", "format": "duration"}, + "failure_action": {"type": "string"}, + "monitor": {"type": "string", "format": "duration"}, + "max_failure_ratio": {"type": "number"} + }, + "additionalProperties": false + }, + "resources": { + "type": "object", + "properties": { + "limits": {"$ref": "#/definitions/resource"}, + "reservations": {"$ref": "#/definitions/resource"} + } + }, + "restart_policy": { + "type": "object", + "properties": { + "condition": {"type": "string"}, + "delay": {"type": "string", "format": "duration"}, + "max_attempts": {"type": "integer"}, + "window": {"type": "string", "format": "duration"} + }, + "additionalProperties": false + }, + "placement": { + "type": "object", + "properties": { + "constraints": {"type": "array", "items": {"type": "string"}} + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + + "resource": { + "id": "#/definitions/resource", + "type": "object", + "properties": { + "cpus": {"type": "string"}, + "memory": {"type": "string"} + }, + "additionalProperties": false + }, + + "network": { + "id": "#/definitions/network", + "type": ["object", "null"], + "properties": { + "driver": {"type": "string"}, + "driver_opts": { + "type": "object", + "patternProperties": { + "^.+$": {"type": ["string", "number"]} + } + }, + "ipam": { + "type": "object", + "properties": { + "driver": {"type": "string"}, + "config": { + "type": "array", + "items": { + "type": "object", + "properties": { + "subnet": {"type": "string"} + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "external": { + "type": ["boolean", "object"], + "properties": { + "name": {"type": "string"} + }, + "additionalProperties": false + }, + "labels": {"$ref": "#/definitions/list_or_dict"} + }, + "additionalProperties": false + }, + + "volume": { + "id": "#/definitions/volume", + "type": ["object", "null"], + "properties": { + "driver": {"type": "string"}, + "driver_opts": { + "type": "object", + "patternProperties": { + "^.+$": {"type": ["string", "number"]} + } + }, + "external": { + "type": ["boolean", "object"], + "properties": { + "name": {"type": "string"} + } + } + }, + "labels": {"$ref": "#/definitions/list_or_dict"}, + "additionalProperties": false + }, + + "secret": { + "id": "#/definitions/secret", + "type": "object", + "properties": { + "file": {"type": "string"}, + "external": { + "type": ["boolean", "object"], + "properties": { + "name": {"type": "string"} + } + } + }, + "labels": {"$ref": "#/definitions/list_or_dict"}, + "additionalProperties": false + }, + + "string_or_list": { + "oneOf": [ + {"type": "string"}, + {"$ref": "#/definitions/list_of_strings"} + ] + }, + + "list_of_strings": { + "type": "array", + "items": {"type": "string"}, + "uniqueItems": true + }, + + "list_or_dict": { + "oneOf": [ + { + "type": "object", + "patternProperties": { + ".+": { + "type": ["string", "number", "null"] + } + }, + "additionalProperties": false + }, + {"type": "array", "items": {"type": "string"}, "uniqueItems": true} + ] + }, + + "constraints": { + "service": { + "id": "#/definitions/constraints/service", + "anyOf": [ + {"required": ["build"]}, + {"required": ["image"]} + ], + "properties": { + "build": { + "required": ["context"] + } + } + } + } + } +} diff --git a/components/cli/compose/schema/schema.go b/components/cli/compose/schema/schema.go index 6366cab48e..ae33c77fbe 100644 --- a/components/cli/compose/schema/schema.go +++ b/components/cli/compose/schema/schema.go @@ -7,9 +7,15 @@ import ( "strings" "time" + "github.com/pkg/errors" "github.com/xeipuuv/gojsonschema" ) +const ( + defaultVersion = "1.0" + versionField = "version" +) + type portsFormatChecker struct{} func (checker portsFormatChecker) IsFormat(input string) bool { @@ -30,11 +36,29 @@ func init() { gojsonschema.FormatCheckers.Add("duration", durationFormatChecker{}) } +// Version returns the version of the config, defaulting to version 1.0 +func Version(config map[string]interface{}) string { + version, ok := config[versionField] + if !ok { + return defaultVersion + } + return normalizeVersion(fmt.Sprintf("%v", version)) +} + +func normalizeVersion(version string) string { + switch version { + case "3": + return "3.0" + default: + return version + } +} + // Validate uses the jsonschema to validate the configuration -func Validate(config map[string]interface{}) error { - schemaData, err := Asset("data/config_schema_v3.0.json") +func Validate(config map[string]interface{}, version string) error { + schemaData, err := Asset(fmt.Sprintf("data/config_schema_v%s.json", version)) if err != nil { - return err + return errors.Errorf("unsupported Compose file version: %s", version) } schemaLoader := gojsonschema.NewStringLoader(string(schemaData)) diff --git a/components/cli/compose/schema/schema_test.go b/components/cli/compose/schema/schema_test.go index be98f807de..0935d4022e 100644 --- a/components/cli/compose/schema/schema_test.go +++ b/components/cli/compose/schema/schema_test.go @@ -8,7 +8,35 @@ import ( type dict map[string]interface{} -func TestValid(t *testing.T) { +func TestValidate(t *testing.T) { + config := dict{ + "version": "3.0", + "services": dict{ + "foo": dict{ + "image": "busybox", + }, + }, + } + + assert.NoError(t, Validate(config, "3.0")) +} + +func TestValidateUndefinedTopLevelOption(t *testing.T) { + config := dict{ + "version": "3.0", + "helicopters": dict{ + "foo": dict{ + "image": "busybox", + }, + }, + } + + err := Validate(config, "3.0") + assert.Error(t, err) + assert.Contains(t, err.Error(), "Additional property helicopters is not allowed") +} + +func TestValidateInvalidVersion(t *testing.T) { config := dict{ "version": "2.1", "services": dict{ @@ -18,18 +46,7 @@ func TestValid(t *testing.T) { }, } - assert.NoError(t, Validate(config)) -} - -func TestUndefinedTopLevelOption(t *testing.T) { - config := dict{ - "version": "2.1", - "helicopters": dict{ - "foo": dict{ - "image": "busybox", - }, - }, - } - - assert.Error(t, Validate(config)) + err := Validate(config, "2.1") + assert.Error(t, err) + assert.Contains(t, err.Error(), "unsupported Compose file version: 2.1") } From 5805451da400e3952572dcb2f89e6beaaab07620 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 10 Jan 2017 17:40:53 -0500 Subject: [PATCH 550/978] Implement secret types for compose file. Signed-off-by: Daniel Nephin Upstream-commit: 0382f4f3657ffbcd30a2a3b36f61f390b9e2f2fb Component: cli --- components/cli/command/service/create.go | 2 +- components/cli/command/service/parse.go | 4 +- components/cli/command/service/update.go | 2 +- components/cli/command/stack/deploy.go | 29 ++++++- components/cli/compose/convert/compose.go | 27 +++++++ components/cli/compose/convert/service.go | 39 +++++++++- components/cli/compose/loader/loader.go | 76 +++++++++++++++---- components/cli/compose/loader/loader_test.go | 18 +++++ components/cli/compose/schema/bindata.go | 8 +- .../schema/data/config_schema_v3.1.json | 4 +- components/cli/compose/types/types.go | 18 +++++ 11 files changed, 201 insertions(+), 26 deletions(-) diff --git a/components/cli/command/service/create.go b/components/cli/command/service/create.go index ca2bb089fd..1355c19c65 100644 --- a/components/cli/command/service/create.go +++ b/components/cli/command/service/create.go @@ -62,7 +62,7 @@ func runCreate(dockerCli *command.DockerCli, opts *serviceOptions) error { specifiedSecrets := opts.secrets.Value() if len(specifiedSecrets) > 0 { // parse and validate secrets - secrets, err := parseSecrets(apiClient, specifiedSecrets) + secrets, err := ParseSecrets(apiClient, specifiedSecrets) if err != nil { return err } diff --git a/components/cli/command/service/parse.go b/components/cli/command/service/parse.go index 6af7e3bb8e..ce9b454edd 100644 --- a/components/cli/command/service/parse.go +++ b/components/cli/command/service/parse.go @@ -10,9 +10,9 @@ import ( "golang.org/x/net/context" ) -// parseSecrets retrieves the secrets from the requested names and converts +// ParseSecrets retrieves the secrets from the requested names and converts // them to secret references to use with the spec -func parseSecrets(client client.SecretAPIClient, requestedSecrets []*types.SecretRequestOption) ([]*swarmtypes.SecretReference, error) { +func ParseSecrets(client client.SecretAPIClient, requestedSecrets []*types.SecretRequestOption) ([]*swarmtypes.SecretReference, error) { secretRefs := make(map[string]*swarmtypes.SecretReference) ctx := context.Background() diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index df0977d86d..3feef4823a 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -443,7 +443,7 @@ func getUpdatedSecrets(apiClient client.SecretAPIClient, flags *pflag.FlagSet, s if flags.Changed(flagSecretAdd) { values := flags.Lookup(flagSecretAdd).Value.(*opts.SecretOpt).Value() - addSecrets, err := parseSecrets(apiClient, values) + addSecrets, err := ParseSecrets(apiClient, values) if err != nil { return nil, err } diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index 306a583e1e..6856624128 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -126,7 +126,16 @@ func deployCompose(ctx context.Context, dockerCli *command.DockerCli, opts deplo if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil { return err } - services, err := convert.Services(namespace, config) + + secrets, err := convert.Secrets(namespace, config.Secrets) + if err != nil { + return err + } + if err := createSecrets(ctx, dockerCli, namespace, secrets); err != nil { + return err + } + + services, err := convert.Services(namespace, config, dockerCli.Client()) if err != nil { return err } @@ -211,6 +220,24 @@ func validateExternalNetworks( return nil } +func createSecrets( + ctx context.Context, + dockerCli *command.DockerCli, + namespace convert.Namespace, + secrets []swarm.SecretSpec, +) error { + client := dockerCli.Client() + + for _, secret := range secrets { + fmt.Fprintf(dockerCli.Out(), "Creating secret %s\n", secret.Name) + _, err := client.SecretCreate(ctx, secret) + if err != nil { + return err + } + } + return nil +} + func createNetworks( ctx context.Context, dockerCli *command.DockerCli, diff --git a/components/cli/compose/convert/compose.go b/components/cli/compose/convert/compose.go index 532f4c4b29..efcf8a6979 100644 --- a/components/cli/compose/convert/compose.go +++ b/components/cli/compose/convert/compose.go @@ -1,8 +1,11 @@ package convert import ( + "io/ioutil" + "github.com/docker/docker/api/types" networktypes "github.com/docker/docker/api/types/network" + "github.com/docker/docker/api/types/swarm" composetypes "github.com/docker/docker/cli/compose/types" ) @@ -82,3 +85,27 @@ func Networks(namespace Namespace, networks networkMap, servicesNetworks map[str return result, externalNetworks } + +// Secrets converts secrets from the Compose type to the engine API type +func Secrets(namespace Namespace, secrets map[string]composetypes.SecretConfig) ([]swarm.SecretSpec, error) { + result := []swarm.SecretSpec{} + for name, secret := range secrets { + if secret.External.External { + continue + } + + data, err := ioutil.ReadFile(secret.File) + if err != nil { + return nil, err + } + + result = append(result, swarm.SecretSpec{ + Annotations: swarm.Annotations{ + Name: namespace.Scope(name), + Labels: AddStackLabel(namespace, secret.Labels), + }, + Data: data, + }) + } + return result, nil +} diff --git a/components/cli/compose/convert/service.go b/components/cli/compose/convert/service.go index a245987c8f..78ad308d38 100644 --- a/components/cli/compose/convert/service.go +++ b/components/cli/compose/convert/service.go @@ -2,20 +2,26 @@ package convert import ( "fmt" + "os" "time" + "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/swarm" + servicecli "github.com/docker/docker/cli/command/service" composetypes "github.com/docker/docker/cli/compose/types" + "github.com/docker/docker/client" "github.com/docker/docker/opts" runconfigopts "github.com/docker/docker/runconfig/opts" "github.com/docker/go-connections/nat" ) // Services from compose-file types to engine API types +// TODO: fix secrets API so that SecretAPIClient is not required here func Services( namespace Namespace, config *composetypes.Config, + client client.SecretAPIClient, ) (map[string]swarm.ServiceSpec, error) { result := make(map[string]swarm.ServiceSpec) @@ -24,7 +30,12 @@ func Services( networks := config.Networks for _, service := range services { - serviceSpec, err := convertService(namespace, service, networks, volumes) + + secrets, err := convertServiceSecrets(client, namespace, service.Secrets) + if err != nil { + return nil, err + } + serviceSpec, err := convertService(namespace, service, networks, volumes, secrets) if err != nil { return nil, err } @@ -39,6 +50,7 @@ func convertService( service composetypes.ServiceConfig, networkConfigs map[string]composetypes.NetworkConfig, volumes map[string]composetypes.VolumeConfig, + secrets []*swarm.SecretReference, ) (swarm.ServiceSpec, error) { name := namespace.Scope(service.Name) @@ -108,6 +120,7 @@ func convertService( StopGracePeriod: service.StopGracePeriod, TTY: service.Tty, OpenStdin: service.StdinOpen, + Secrets: secrets, }, LogDriver: logDriver, Resources: resources, @@ -163,6 +176,30 @@ func convertServiceNetworks( return nets, nil } +// TODO: fix secrets API so that SecretAPIClient is not required here +func convertServiceSecrets( + client client.SecretAPIClient, + namespace Namespace, + secrets []composetypes.ServiceSecretConfig, +) ([]*swarm.SecretReference, error) { + opts := []*types.SecretRequestOption{} + for _, secret := range secrets { + target := secret.Target + if target == "" { + target = secret.Source + } + opts = append(opts, &types.SecretRequestOption{ + Source: namespace.Scope(secret.Source), + Target: target, + UID: secret.UID, + GID: secret.GID, + Mode: os.FileMode(secret.Mode), + }) + } + + return servicecli.ParseSecrets(client, opts) +} + func convertExtraHosts(extraHosts map[string]string) []string { hosts := []string{} for host, ip := range extraHosts { diff --git a/components/cli/compose/loader/loader.go b/components/cli/compose/loader/loader.go index c9554a4b44..a43347f475 100644 --- a/components/cli/compose/loader/loader.go +++ b/components/cli/compose/loader/loader.go @@ -109,6 +109,20 @@ func Load(configDetails types.ConfigDetails) (*types.Config, error) { cfg.Volumes = volumesMapping } + if secrets, ok := configDict["secrets"]; ok { + secretsConfig, err := interpolation.Interpolate(secrets.(types.Dict), "secret", os.LookupEnv) + if err != nil { + return nil, err + } + + secretsMapping, err := loadSecrets(secretsConfig, configDetails.WorkingDir) + if err != nil { + return nil, err + } + + cfg.Secrets = secretsMapping + } + return &cfg, nil } @@ -210,13 +224,15 @@ func transformHook( ) (interface{}, error) { switch target { case reflect.TypeOf(types.External{}): - return transformExternal(source, target, data) + return transformExternal(data) case reflect.TypeOf(make(map[string]string, 0)): return transformMapStringString(source, target, data) case reflect.TypeOf(types.UlimitsConfig{}): - return transformUlimits(source, target, data) + return transformUlimits(data) case reflect.TypeOf(types.UnitBytes(0)): return loadSize(data) + case reflect.TypeOf(types.ServiceSecretConfig{}): + return transformServiceSecret(data) } switch target.Kind() { case reflect.Struct: @@ -311,7 +327,7 @@ func resolveEnvironment(serviceConfig *types.ServiceConfig, serviceDict types.Di var envVars []string for _, file := range envFiles { - filePath := path.Join(workingDir, file) + filePath := absPath(workingDir, file) fileVars, err := opts.ParseEnvFile(filePath) if err != nil { return err @@ -341,7 +357,7 @@ func resolveVolumePaths(volumes []string, workingDir string) error { } if strings.HasPrefix(parts[0], ".") { - parts[0] = path.Join(workingDir, parts[0]) + parts[0] = absPath(workingDir, parts[0]) } parts[0] = expandUser(parts[0]) @@ -359,11 +375,7 @@ func expandUser(path string) string { return path } -func transformUlimits( - source reflect.Type, - target reflect.Type, - data interface{}, -) (interface{}, error) { +func transformUlimits(data interface{}) (interface{}, error) { switch value := data.(type) { case int: return types.UlimitsConfig{Single: value}, nil @@ -407,6 +419,32 @@ func loadVolumes(source types.Dict) (map[string]types.VolumeConfig, error) { return volumes, nil } +// TODO: remove duplicate with networks/volumes +func loadSecrets(source types.Dict, workingDir string) (map[string]types.SecretConfig, error) { + secrets := make(map[string]types.SecretConfig) + err := transform(source, &secrets) + if err != nil { + return secrets, err + } + for name, secret := range secrets { + if secret.External.External && secret.External.Name == "" { + secret.External.Name = name + secrets[name] = secret + } + if secret.File != "" { + secret.File = absPath(workingDir, secret.File) + } + } + return secrets, nil +} + +func absPath(workingDir string, filepath string) string { + if path.IsAbs(filepath) { + return filepath + } + return path.Join(workingDir, filepath) +} + func transformStruct( source reflect.Type, target reflect.Type, @@ -490,11 +528,7 @@ func convertField( return data, nil } -func transformExternal( - source reflect.Type, - target reflect.Type, - data interface{}, -) (interface{}, error) { +func transformExternal(data interface{}) (interface{}, error) { switch value := data.(type) { case bool: return map[string]interface{}{"external": value}, nil @@ -507,6 +541,20 @@ func transformExternal( } } +func transformServiceSecret(data interface{}) (interface{}, error) { + switch value := data.(type) { + case string: + return map[string]interface{}{"source": value}, nil + case types.Dict: + return data, nil + case map[string]interface{}: + return data, nil + default: + return data, fmt.Errorf("invalid type %T for external", value) + } + +} + func toYAMLName(name string) string { nameParts := fieldNameRegexp.FindAllString(name, -1) for i, p := range nameParts { diff --git a/components/cli/compose/loader/loader_test.go b/components/cli/compose/loader/loader_test.go index e15be7c549..f7fee89ede 100644 --- a/components/cli/compose/loader/loader_test.go +++ b/components/cli/compose/loader/loader_test.go @@ -163,6 +163,24 @@ func TestLoad(t *testing.T) { assert.Equal(t, sampleConfig.Volumes, actual.Volumes) } +func TestLoadV31(t *testing.T) { + actual, err := loadYAML(` +version: "3.1" +services: + foo: + image: busybox + secrets: [super] +secrets: + super: + external: true +`) + if !assert.NoError(t, err) { + return + } + assert.Equal(t, len(actual.Services), 1) + assert.Equal(t, len(actual.Secrets), 1) +} + func TestParseAndLoad(t *testing.T) { actual, err := loadYAML(sampleYAML) if !assert.NoError(t, err) { diff --git a/components/cli/compose/schema/bindata.go b/components/cli/compose/schema/bindata.go index 6d900e0a9a..3713315b2a 100644 --- a/components/cli/compose/schema/bindata.go +++ b/components/cli/compose/schema/bindata.go @@ -89,7 +89,7 @@ func dataConfig_schema_v30Json() (*asset, error) { return a, nil } -var _dataConfig_schema_v31Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x1a\x4d\x93\xdb\xa8\xf2\xee\x5f\xa1\x52\x72\x8b\x67\x26\xaf\x5e\xea\x55\xbd\xdc\xde\xf1\x9d\x76\xcf\x3b\xe5\xa8\xb0\xd4\x96\xc9\x20\x20\x80\x9c\x71\x52\xfe\xef\x5b\xe8\xcb\x80\x41\x60\x5b\xd9\xa4\x6a\xf7\x34\x63\xd1\xdd\xf4\x77\x37\x0d\xdf\x57\x59\x96\xbf\x95\xe5\x1e\x1a\x94\x7f\xcc\xf2\xbd\x52\xfc\xe3\xd3\xd3\x67\xc9\xe8\x43\xff\xf5\x91\x89\xfa\xa9\x12\x68\xa7\x1e\xde\x7f\x78\xea\xbf\xbd\xc9\xd7\x1a\x0f\x57\x1a\xa5\x64\x74\x87\xeb\xa2\x5f\x29\x0e\xff\x7e\xfc\xd7\xa3\x46\xef\x41\xd4\x91\x83\x06\x62\xdb\xcf\x50\xaa\xfe\x9b\x80\x2f\x2d\x16\xa0\x91\x9f\xf3\x03\x08\x89\x19\xcd\x37\xeb\x95\x5e\xe3\x82\x71\x10\x0a\x83\xcc\x3f\x66\x9a\xb9\x2c\x9b\x40\xc6\x0f\x06\x59\xa9\x04\xa6\x75\xde\x7d\x3e\x75\x14\xb2\x2c\x97\x20\x0e\xb8\x34\x28\x4c\xac\xbe\x79\x3a\xd3\x7f\x9a\xc0\xd6\x2e\x55\x83\xd9\xee\x3b\x47\x4a\x81\xa0\xbf\x5f\xf2\xd6\x2d\x7f\x7a\x46\x0f\xdf\xfe\xf7\xf0\xc7\xfb\x87\xff\x3e\x16\x0f\x9b\x77\x6f\xad\x65\xad\x5f\x01\xbb\x7e\xfb\x0a\x76\x98\x62\x85\x19\x9d\xf6\xcf\x27\xc8\xd3\xf0\xdf\x69\xda\x18\x55\x55\x07\x8c\x88\xb5\xf7\x0e\x11\x09\xb6\xcc\x14\xd4\x57\x26\x5e\x62\x32\x4f\x60\x3f\x49\xe6\x61\x7f\x8f\xcc\xb6\x38\x07\x46\xda\x26\x6a\xc1\x11\xea\x27\x09\xd3\x6f\xbf\x8c\xfd\x24\x94\x02\x54\xdc\x65\x7b\xa8\x9f\xe6\xb1\x7a\xfb\xfb\x04\x5e\x8d\x42\xcf\xc2\xf6\x10\xc6\xde\x1d\x83\x56\x78\xfb\x54\xe5\x0b\xaf\xb0\xae\x26\x65\x05\xb4\x54\x01\x27\xec\xa8\xbf\x05\xf4\xd1\x03\x34\x40\x55\x3e\xa9\x20\xcb\xf2\x6d\x8b\x49\xe5\x6a\x94\x51\xf8\x4d\x93\x78\x36\x3e\x66\xd9\x77\x37\x93\x19\x74\xba\x75\xeb\x57\xd8\xe0\xd3\x7a\x40\x96\x69\xbd\x64\x54\xc1\xab\xea\x84\x9a\xdf\xba\x57\x01\x2b\x5f\x40\xec\x30\x81\x54\x0c\x24\x6a\x39\xa3\x32\x82\xa5\x2a\x98\x28\x2a\x5c\xaa\xfc\xe4\xa0\x5f\xd0\x8b\xfb\xd3\x84\x6a\xfc\xda\xac\x3c\x04\xf3\x12\xf1\x02\x55\x95\x25\x07\x12\x02\x1d\xf3\x75\x96\x63\x05\x8d\xf4\x8b\x98\xe5\x2d\xc5\x5f\x5a\xf8\xff\x00\xa2\x44\x0b\x2e\xdd\x4a\x30\xbe\x3c\xe1\x5a\xb0\x96\x17\x1c\x09\xed\x60\xf3\xea\xcf\x4b\xd6\x34\x88\x2e\xe5\x75\xd7\xc8\x91\xa0\x79\x46\x15\xc2\x14\x44\x41\x51\x13\x73\x24\x1d\x75\x40\x2b\x59\xf4\x05\x7f\xd6\x8d\x76\x45\x8f\x2f\x1d\x02\x53\xf5\x5f\xd4\x1e\x15\x9d\x73\xec\x9e\x8c\x76\x6d\xcd\x5b\xee\x20\x16\x12\x90\x28\xf7\x37\xe2\xb3\x06\x61\x9a\xa2\x3b\xa0\x4a\x1c\x39\xc3\xbd\xbf\xfc\x72\x8e\x00\xf4\x50\x4c\xb9\xe4\x6a\x35\x00\x3d\x60\xc1\x68\x33\x46\x43\x4a\x82\x99\x92\xbc\xc6\x7f\xe5\x4c\x82\xab\x18\x47\x40\x73\x69\x12\xd5\xd2\xc9\x88\xf1\x3c\x0a\xbe\xce\x72\xda\x36\x5b\x10\xba\x87\xb5\x20\x77\x4c\x34\x48\x33\x3b\xee\x6d\x2c\x5b\x9a\xf6\x78\x9e\xa9\x40\x53\x06\x5d\xd6\x11\x29\x08\xa6\x2f\xcb\xbb\x38\xbc\x2a\x81\x8a\x3d\x93\x2a\x3d\x87\x1b\xe8\x7b\x40\x44\xed\xcb\x3d\x94\x2f\x33\xe8\x26\x94\x85\xcd\xa4\x4a\x71\x72\xdc\xa0\x3a\x0e\xc4\xcb\x18\x08\x41\x5b\x20\x37\xc9\xb9\xa8\xf2\x0d\xb2\xac\xae\x35\x68\xc8\xe3\x2e\x3a\x97\x61\x39\x56\xf3\x2b\x81\x0f\x20\x52\x0b\x38\xe3\xe7\x86\xcb\x5d\x8c\x37\x20\x59\xbc\xfb\xb4\x40\x3f\x3d\xf6\xcd\xe7\x4c\x54\x75\xff\x11\x92\x6f\xdc\x76\x21\x73\xea\xbe\xef\x8b\x23\x61\x5a\x43\x61\x59\xa5\x41\xa5\xee\x1b\x04\xc8\x80\x5d\xcf\xa0\xc3\xe9\xa6\x68\x58\x15\x72\xd0\x0b\x60\x57\x37\xc1\x4c\x7d\x75\x21\xcc\x6e\xea\x1f\x93\x4c\x17\x3d\x40\x44\xa4\x09\xb1\x97\xca\xe6\x99\xdd\xb8\x8b\x75\x70\x88\x60\x24\x21\x1e\xec\x41\x45\x5a\xd4\x30\x3f\x7c\x48\xf4\x09\x1f\xee\x7f\x66\x71\x03\xa8\x41\x9a\xe9\x3d\x72\x84\xd4\x99\x95\x2e\xdc\x7c\x8c\x6c\x22\xd1\xf6\x83\x5b\x78\x8e\xab\x70\xae\xe8\x32\x84\x19\x60\x9c\x09\x75\x11\x5d\x7f\x4d\xb9\xef\xb7\xbe\xbb\xda\x73\x81\x0f\x98\x40\x0d\xf6\xa9\x65\xcb\x18\x01\x44\xad\xd4\x23\x00\x55\x05\xa3\xe4\x98\x00\x29\x15\x12\xd1\x03\x85\x84\xb2\x15\x58\x1d\x0b\xc6\xd5\xe2\x7d\x86\xdc\x37\x85\xc4\xdf\xc0\xb6\xe6\x39\xdf\x0f\x84\x36\x0e\x43\xce\x84\xe4\x46\x83\x86\x52\x52\x3c\x8c\x3d\x89\x30\x9a\xa8\xe2\x29\x2a\x97\xac\x15\x65\xea\x01\x5b\xef\x89\x44\x0d\xa9\x47\x78\xed\x6e\x76\xd8\xcc\x03\xd7\xd7\x00\x5f\x14\xba\xc1\x84\xb1\xaa\xec\xfe\x36\xf3\xca\xc9\x1b\xfa\xf2\x28\x4b\x75\x5b\xb7\x26\x55\x85\x69\xc1\x38\xd0\x68\x6c\x48\xc5\x78\x21\x71\x4d\x11\x89\xc6\x87\x06\xad\x05\x2a\xa1\xe0\x20\x30\xf3\x6a\x6d\x6d\x26\x85\xaa\x15\x48\xb3\x6a\x91\x51\x0d\xdf\xdd\x78\xac\x54\x2a\x1e\xec\x2d\xc1\x0d\x0e\x07\x8d\xc7\x6b\x13\x3a\x80\xbe\xfa\xfb\x8b\xfe\x4c\xc1\x3f\x73\x8a\xa9\x82\x5a\xbb\xc9\xa5\x53\xcd\xf4\x9c\xf3\x2d\x67\x42\xaf\xb9\x47\xc2\xb6\xd2\x0c\x1f\x59\x1f\x98\x3b\xe5\x47\xf0\x75\xa2\x5e\xbe\xac\xbb\x8e\x8e\xde\x7a\x60\x64\xe3\x85\xbf\xaa\x98\xbb\x6c\x6c\x82\xf5\xd4\x1f\x54\xad\x8c\x1e\x0b\x3a\x18\x2a\xe7\x5a\xda\x09\xd4\x18\xda\x2f\x5a\x2d\x74\x9b\xac\x83\xa0\xc2\x7e\x6e\x57\x8e\x64\x57\x8c\xdd\x9d\x13\xeb\x48\xc0\x37\x4f\x36\x41\xdd\x99\xf2\xf3\xe4\x9b\x63\x27\x72\x9e\xc4\x07\x86\xcb\xda\x95\xc4\xc1\xca\x33\x3e\x9d\x2a\xdc\x00\x6b\x55\x04\x4a\x80\x12\xd8\xd1\xfc\x98\x8a\x4d\x62\x20\x7f\xcd\xc1\x50\x85\x25\xda\x3a\x33\xe6\x29\x9d\xdd\x64\xde\xec\x3c\xc0\x1f\x07\x46\x73\xc6\x35\x20\x17\xb0\x6d\x4a\xb0\x08\xe0\x04\x97\x48\xc6\x12\xd2\x1d\x63\x8a\x96\x57\x48\x41\xd1\xdf\xcf\x5e\x55\x02\x66\x72\x3f\x47\x02\x11\x02\x04\xcb\x26\x25\x97\xe6\x15\x10\x74\xbc\xa9\x36\x76\xe8\x3b\x84\x49\x2b\xa0\x40\xa5\x1a\xae\x80\x23\x9e\x99\x37\x8c\x62\xc5\xbc\x99\x22\x6d\xcb\x06\xbd\x16\xe3\xb6\x1d\x48\xac\xc3\xb1\x9b\xfb\xd4\x09\x83\xe1\x09\x7d\x03\x78\x5d\x95\x9e\x31\xd1\xb9\xe6\x07\x3c\x66\xdc\xf1\x42\x74\x01\x52\x27\xa5\x69\x00\x14\xc5\x8f\x96\x98\xe1\xb4\x51\x70\x46\x70\x79\x5c\x4a\xc2\x92\xd1\x5e\xc9\x29\x0e\x71\xa7\x07\x6a\x77\xd0\x2d\x51\xc3\x55\x34\x58\x3b\x84\xaf\x98\x56\xec\xeb\x15\x1b\x2e\xe7\x4a\x9c\xa0\x12\x9c\x7c\x77\xaf\xa2\xa5\x12\x08\x53\x75\x75\x59\xbf\x57\xac\x3b\xaa\xfa\xe4\x9f\x91\xac\x3f\xc1\xc5\xef\xd3\x03\x99\xbe\xe4\x6d\x74\x2a\xd8\x40\xc3\x84\xd7\x01\x17\x78\xf0\x11\x13\x71\x04\x5b\xa0\xaa\x25\x8d\x91\x07\xa8\x82\xf1\xe5\x4f\x1d\xf1\x51\xf1\x26\x9e\x90\x30\x47\xcd\x52\xd1\x91\x3c\x58\xcf\xbd\x35\x38\x9b\x9f\x5f\x64\xe1\x19\x46\x8c\xeb\x38\xef\x03\x84\x6c\xb7\x34\x30\x4a\xb8\x3c\x6d\xf8\x6e\xfb\xd3\x8f\x2b\xa7\xf0\xe1\xe4\xbe\xa4\x37\xde\x89\x05\xac\xfa\x3c\x75\x92\xeb\x49\x57\x9b\x64\x13\x07\x2f\xa4\x96\xe3\xff\xca\x06\xef\x8e\x9c\x31\x3c\x58\x8a\xa4\x8c\x01\xea\x9f\x8c\xf1\xcb\xf8\xd7\x4c\x51\xbc\xf1\x74\x70\xf5\xcb\xb4\x98\xd3\x0c\x50\x37\x17\xd2\x84\x27\x46\x7f\x7b\x43\xd8\xa3\x40\xc3\x20\x97\x67\xf8\x39\x3d\x26\xdf\x80\x0d\x18\x1b\x9b\x0d\x17\xcc\xf3\x2a\xd7\xae\x65\x73\xa3\x9f\x11\x24\x70\x23\xe2\x6c\x3a\x28\x6f\x5e\xf2\x05\xf3\xc7\xe3\xbb\x99\x8a\x3d\x77\x53\xfd\x83\x4a\xdd\x02\x63\x35\xbf\x4d\x9d\x36\x7f\xd4\xee\xe5\x4b\xcb\x40\xf4\x1b\xf8\x17\xef\x2e\xb5\x9c\xf4\x78\x31\x63\xfa\x6e\x8f\x46\xfb\x37\x93\x1b\x4b\x3f\x0e\x48\xff\xee\xc3\x48\xd8\x1b\xf3\xe4\x13\x32\xa3\xf7\x35\xa6\x3b\x98\x1d\x5f\x45\x06\xee\x21\x56\xe6\xdf\xee\x05\xeb\xea\xb4\xfa\x33\x00\x00\xff\xff\xb7\x14\xdd\xc9\x3a\x2f\x00\x00") +var _dataConfig_schema_v31Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x1a\x4d\x93\xdb\xa8\xf2\xee\x5f\xa1\x52\x72\x8b\x67\x26\xaf\x5e\xea\x55\xbd\xdc\xf6\xb8\xa7\xdd\xf3\x4e\x39\x2a\x46\x6a\xcb\x64\x10\x10\x40\xce\x38\x29\xff\xf7\x2d\xf4\x65\xc0\x20\xb0\xad\xec\xcc\x61\x4f\x33\x16\xdd\x4d\x7f\x77\xd3\xf0\x73\x95\x65\xf9\x7b\x59\xee\xa0\x41\xf9\xe7\x2c\xdf\x29\xc5\x3f\x3f\x3c\x7c\x95\x8c\xde\xf5\x5f\xef\x99\xa8\x1f\x2a\x81\xb6\xea\xee\xe3\xa7\x87\xfe\xdb\xbb\x7c\xad\xf1\x70\xa5\x51\x4a\x46\xb7\xb8\x2e\xfa\x95\x62\xff\xdf\xfb\xff\xdc\x6b\xf4\x1e\x44\x1d\x38\x68\x20\xf6\xf4\x15\x4a\xd5\x7f\x13\xf0\xad\xc5\x02\x34\xf2\x63\xbe\x07\x21\x31\xa3\xf9\x66\xbd\xd2\x6b\x5c\x30\x0e\x42\x61\x90\xf9\xe7\x4c\x33\x97\x65\x13\xc8\xf8\xc1\x20\x2b\x95\xc0\xb4\xce\xbb\xcf\xc7\x8e\x42\x96\xe5\x12\xc4\x1e\x97\x06\x85\x89\xd5\x77\x0f\x27\xfa\x0f\x13\xd8\xda\xa5\x6a\x30\xdb\x7d\xe7\x48\x29\x10\xf4\xcf\x73\xde\xba\xe5\x2f\x8f\xe8\xee\xc7\x6f\x77\x7f\x7d\xbc\xfb\xff\x7d\x71\xb7\xf9\xf0\xde\x5a\xd6\xfa\x15\xb0\xed\xb7\xaf\x60\x8b\x29\x56\x98\xd1\x69\xff\x7c\x82\x3c\x0e\xff\x1d\xa7\x8d\x51\x55\x75\xc0\x88\x58\x7b\x6f\x11\x91\x60\xcb\x4c\x41\x7d\x67\xe2\x39\x26\xf3\x04\xf6\x4a\x32\x0f\xfb\x7b\x64\xb6\xc5\xd9\x33\xd2\x36\x51\x0b\x8e\x50\xaf\x24\x4c\xbf\xfd\x32\xf6\x93\x50\x0a\x50\x71\x97\xed\xa1\x5e\xcd\x63\xf5\xf6\xb7\x09\xbc\x1a\x85\x9e\x85\xed\x21\x8c\xbd\x3b\x06\xad\xf0\xf6\xa9\xca\x17\x5e\x61\x5d\x4d\xca\x0a\x68\xa9\x02\x4e\xd8\x41\x7f\x0b\xe8\xa3\x07\x68\x80\xaa\x7c\x52\x41\x96\xe5\x4f\x2d\x26\x95\xab\x51\x46\xe1\x0f\x4d\xe2\xd1\xf8\x98\x65\x3f\xdd\x4c\x66\xd0\xe9\xd6\xad\x5f\x61\x83\x4f\xeb\x01\x59\xa6\xf5\x92\x51\x05\x2f\xaa\x13\x6a\x7e\xeb\x5e\x05\xac\x7c\x06\xb1\xc5\x04\x52\x31\x90\xa8\xe5\x8c\xca\x08\x96\xaa\x60\xa2\xa8\x70\xa9\xf2\xa3\x83\x7e\x46\x2f\xee\x4f\x13\xaa\xf1\x6b\xb3\xf2\x10\xcc\x4b\xc4\x0b\x54\x55\x96\x1c\x48\x08\x74\xc8\xd7\x59\x8e\x15\x34\xd2\x2f\x62\x96\xb7\x14\x7f\x6b\xe1\xf7\x01\x44\x89\x16\x5c\xba\x95\x60\x7c\x79\xc2\xb5\x60\x2d\x2f\x38\x12\xda\xc1\xe6\xd5\x9f\x97\xac\x69\x10\x5d\xca\xeb\x2e\x91\x23\x41\xf3\x8c\x2a\x84\x29\x88\x82\xa2\x26\xe6\x48\x3a\xea\x80\x56\xb2\xe8\x0b\xfe\xac\x1b\x6d\x8b\x1e\x5f\x3a\x04\xa6\xea\xbf\xa8\x3d\x2a\x3a\xe7\xd8\x3d\x19\xed\xda\x9a\xb7\xdc\x41\x2c\x24\x20\x51\xee\xae\xc4\x67\x0d\xc2\x34\x45\x77\x40\x95\x38\x70\x86\x7b\x7f\x79\x73\x8e\x00\x74\x5f\x4c\xb9\xe4\x62\x35\x00\xdd\x63\xc1\x68\x33\x46\x43\x4a\x82\x99\x92\xbc\xc6\x7f\xe1\x4c\x82\xab\x18\x47\x40\x73\x69\x12\xd5\xd2\xc9\x88\xf1\x38\x0a\xbe\xce\x72\xda\x36\x4f\x20\x74\x0f\x6b\x41\x6e\x99\x68\x90\x66\x76\xdc\xdb\x58\xb6\x34\xed\xf1\x3c\x53\x81\xa6\x0c\xba\xac\x23\x52\x10\x4c\x9f\x97\x77\x71\x78\x51\x02\x15\x3b\x26\x55\x7a\x0e\x37\xd0\x77\x80\x88\xda\x95\x3b\x28\x9f\x67\xd0\x4d\x28\x0b\x9b\x49\x95\xe2\xe4\xb8\x41\x75\x1c\x88\x97\x31\x10\x82\x9e\x80\x5c\x25\xe7\xa2\xca\x37\xc8\xb2\xba\xd6\xa0\x21\x8f\x3b\xeb\x5c\x86\xe5\x58\xcd\xaf\x04\xde\x83\x48\x2d\xe0\x8c\x9f\x1a\x2e\x77\x31\xde\x80\x64\xf1\xee\xd3\x02\xfd\x72\xdf\x37\x9f\x33\x51\xd5\xfd\x47\x48\xbe\x71\xdb\x85\xcc\xa9\xfb\xbe\x2f\x8e\x84\x69\x0d\x85\x65\x95\x06\x95\xba\x6f\x10\x20\x03\x76\x3d\x81\x0e\xa7\x9b\xa2\x61\x55\xc8\x41\xcf\x80\x5d\xdd\x04\x33\xf5\xc5\x85\x30\xbb\xaa\x7f\x4c\x32\x5d\xf4\x00\x11\x91\x26\xc4\x5e\x2a\x9b\x27\x76\xe3\x2e\xd6\xc1\x21\x82\x91\x84\x78\xb0\x07\x15\x69\x51\xc3\x7c\xff\x29\xd1\x27\x7c\xb8\xff\x9b\xc5\x0d\xa0\x06\x69\xa6\xf7\xc8\x11\x52\x27\x56\xba\x70\xf3\x31\xb2\x89\x44\xdb\x2f\x6e\xe1\x39\xae\xc2\xb9\xa2\xcb\x10\x66\x80\x71\x26\xd4\x59\x74\xfd\x33\xe5\xbe\xdf\xfa\xe6\x6a\xcf\x05\xde\x63\x02\x35\xd8\xa7\x96\x27\xc6\x08\x20\x6a\xa5\x1e\x01\xa8\x2a\x18\x25\x87\x04\x48\xa9\x90\x88\x1e\x28\x24\x94\xad\xc0\xea\x50\x30\xae\x16\xef\x33\xe4\xae\x29\x24\xfe\x01\xb6\x35\x4f\xf9\x7e\x20\xb4\x71\x18\x72\x26\x24\x57\x1a\x34\x94\x92\xe2\x61\xec\x49\x84\xd1\x44\x15\x4f\x51\xb9\x64\xad\x28\x53\x0f\xd8\x7a\x4f\x24\x6a\x48\x3d\xc2\x6b\x77\xb3\xc3\x66\x1e\xb8\xbe\x04\xf8\xac\xd0\x0d\x26\x8c\x55\x65\xf7\xb7\x99\x57\x8e\xde\xd0\x97\x07\x59\xaa\xeb\xba\x35\xa9\x2a\x4c\x0b\xc6\x81\x46\x63\x43\x2a\xc6\x0b\x89\x6b\x8a\x48\x34\x3e\x34\x68\x2d\x50\x09\x05\x07\x81\x99\x57\x6b\x6b\x33\x29\x54\xad\x40\x9a\x55\x8b\x8c\x6a\xf8\xf6\xca\x63\xa5\x52\xf1\x60\x6f\x09\x6e\x70\x38\x68\x3c\x5e\x9b\xd0\x01\xf4\xd5\xdf\x5f\xf4\x67\x0a\xfe\x89\x53\x4c\x15\xd4\xda\x4d\xce\x9d\x6a\xa6\xe7\x9c\x6f\x39\x13\x7a\xcd\x1d\x12\xb6\x95\x66\xf8\xc8\xfa\xc0\xdc\x2a\x3f\x82\xaf\x13\xf5\xf2\x65\xdd\x75\x74\xf4\xd6\x03\x23\x1b\x2f\xfc\x45\xc5\xdc\x65\x63\x13\xac\xa7\xfe\xa0\x6a\x65\xf4\x58\xd0\xc1\x50\x39\xd7\xd2\x4e\xa0\xc6\xd0\x7e\xd1\x6a\xa1\xdb\x64\x1d\x04\x15\xf6\x73\xbb\x72\x24\xbb\x60\xec\xee\x9c\x58\x47\x02\xbe\x79\xb2\x09\xea\xce\x94\x1f\x27\xdf\x1c\x3b\x91\xd3\x24\x3e\x30\x5c\xd6\xae\x24\xf6\x56\x9e\xf1\xe9\x54\xe1\x06\x58\xab\x22\x50\x02\x94\xc0\x8e\xe6\xc7\x54\x6c\x12\x03\xf9\x36\x07\x43\x15\x96\xe8\xc9\x99\x31\x4f\xe9\xec\x2a\xf3\x66\xa7\x01\xfe\x38\x30\x9a\x33\xae\x01\xb9\x80\x6d\x53\x82\x45\x00\x27\xb8\x44\x32\x96\x90\x6e\x18\x53\xb4\xbc\x42\x0a\x8a\xfe\x7e\xf6\xa2\x12\x30\x93\xfb\x39\x12\x88\x10\x20\x58\x36\x29\xb9\x34\xaf\x80\xa0\xc3\x55\xb5\xb1\x43\xdf\x22\x4c\x5a\x01\x05\x2a\xd5\x70\x05\x1c\xf1\xcc\xbc\x61\x14\x2b\xe6\xcd\x14\x69\x5b\x36\xe8\xa5\x18\xb7\xed\x40\x62\x1d\x8e\xdd\xdc\xa7\x4e\x18\x0c\x4f\xe8\x1b\xc0\xcb\xaa\xf4\x8c\x89\x4e\x35\x3f\xe0\x31\xe3\x8e\x67\xa2\x0b\x90\x3a\x29\x4d\x03\xa0\x28\x7e\xb4\xc4\x0c\xa7\x8d\x82\x33\x82\xcb\xc3\x52\x12\x96\x8c\xf6\x4a\x4e\x71\x88\x1b\x3d\x50\xbb\x83\x6e\x89\x1a\xae\xa2\xc1\xda\x21\x7c\xc7\xb4\x62\xdf\x2f\xd8\x70\x39\x57\xe2\x04\x95\xe0\xe4\xbb\x5b\x15\x2d\x95\x40\x98\xaa\x8b\xcb\xfa\xad\x62\xdd\x50\xd5\x27\xff\x8c\x64\xfd\x09\x2e\x7e\x9f\x1e\xc8\xf4\x25\x6f\xa3\x53\xc1\x06\x1a\x26\xbc\x0e\xb8\xc0\x83\x8f\x98\x88\x23\xd8\x02\x55\x2d\x69\x8c\x3c\x40\x15\x8c\x2f\x7f\xea\x88\x8f\x8a\x37\xf1\x84\x84\x39\x6a\x96\x8a\x8e\xe4\xc1\x7a\xee\xad\xc1\xd9\xfc\xfc\x22\x0b\xcf\x30\x62\x5c\xc7\x79\x1f\x20\x64\xfb\x44\x03\xa3\x84\xf3\xd3\x86\xef\xb6\x3f\xfd\xb8\x72\x0c\x1f\x4e\x6e\x4b\x7a\xe3\x9d\x58\xc0\xaa\x8f\x53\x27\xb9\x9e\x74\xb5\x49\x36\x71\xf0\x42\x6a\x39\xfe\x2f\x6c\xf0\x6e\xc8\x19\xc3\x83\xa5\x48\xca\x18\xa0\xfe\xcd\x18\x6f\xc6\xbf\x66\x8a\xe2\x95\xa7\x83\x8b\x5f\xa6\xc5\x9c\x66\x80\xba\xba\x90\x26\x3c\x31\x7a\x53\x86\x78\x95\xf8\x75\x86\x81\x86\x49\xce\x4f\xf1\x73\x9a\x4c\xbe\x03\x1b\x30\x36\x36\x1b\x2e\x98\xe7\x5d\xae\x5d\xcd\xe6\x86\x3f\x23\x48\xe0\x4e\xc4\xd9\x74\x50\xe2\xbc\xe4\x0b\x66\x90\xfb\x0f\x33\x35\x7b\xee\xae\xfa\x17\x15\xbb\x05\x06\x6b\x7e\x9b\x3a\x8d\xfe\xa8\xdd\xf3\xb7\x96\x81\xf8\x37\xf0\xcf\x5e\x5e\x6a\x39\xe9\xe1\x6c\xca\xf4\xd3\x1e\x8e\xf6\xaf\x26\x37\x96\x7e\x1c\x90\xfe\xe5\x87\x91\xb2\x37\xe6\xd9\x27\x64\x46\xef\x7b\x4c\x77\x34\x3b\xbe\x8b\x0c\xdc\x44\xac\xcc\xbf\xdd\x1b\xd6\xd5\x71\xf5\x77\x00\x00\x00\xff\xff\xc8\x0f\x22\x69\x3c\x2f\x00\x00") func dataConfig_schema_v31JsonBytes() ([]byte, error) { return bindataRead( @@ -205,9 +205,9 @@ type bintree struct { Children map[string]*bintree } var _bintree = &bintree{nil, map[string]*bintree{ - "data": {nil, map[string]*bintree{ - "config_schema_v3.0.json": {dataConfig_schema_v30Json, map[string]*bintree{}}, - "config_schema_v3.1.json": {dataConfig_schema_v31Json, map[string]*bintree{}}, + "data": &bintree{nil, map[string]*bintree{ + "config_schema_v3.0.json": &bintree{dataConfig_schema_v30Json, map[string]*bintree{}}, + "config_schema_v3.1.json": &bintree{dataConfig_schema_v31Json, map[string]*bintree{}}, }}, }} diff --git a/components/cli/compose/schema/data/config_schema_v3.1.json b/components/cli/compose/schema/data/config_schema_v3.1.json index c43f296b55..b67203218a 100644 --- a/components/cli/compose/schema/data/config_schema_v3.1.json +++ b/components/cli/compose/schema/data/config_schema_v3.1.json @@ -374,9 +374,9 @@ "properties": { "name": {"type": "string"} } - } + }, + "labels": {"$ref": "#/definitions/list_or_dict"} }, - "labels": {"$ref": "#/definitions/list_or_dict"}, "additionalProperties": false }, diff --git a/components/cli/compose/types/types.go b/components/cli/compose/types/types.go index 3f2f038834..d70d01ed29 100644 --- a/components/cli/compose/types/types.go +++ b/components/cli/compose/types/types.go @@ -71,6 +71,7 @@ type Config struct { Services []ServiceConfig Networks map[string]NetworkConfig Volumes map[string]VolumeConfig + Secrets map[string]SecretConfig } // ServiceConfig is the configuration of one service @@ -108,6 +109,7 @@ type ServiceConfig struct { Privileged bool ReadOnly bool `mapstructure:"read_only"` Restart string + Secrets []ServiceSecretConfig SecurityOpt []string `mapstructure:"security_opt"` StdinOpen bool `mapstructure:"stdin_open"` StopGracePeriod *time.Duration `mapstructure:"stop_grace_period"` @@ -191,6 +193,15 @@ type ServiceNetworkConfig struct { Ipv6Address string `mapstructure:"ipv6_address"` } +// ServiceSecretConfig is the secret configuration for a service +type ServiceSecretConfig struct { + Source string + Target string + UID string + GID string + Mode uint32 +} + // UlimitsConfig the ulimit configuration type UlimitsConfig struct { Single int @@ -233,3 +244,10 @@ type External struct { Name string External bool } + +// SecretConfig for a secret +type SecretConfig struct { + File string + External External + Labels map[string]string `compose:"list_or_dict_equals"` +} From a568e7cd60e9eee6bf01c92b4cdf211b20ebefa8 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 13 Jan 2017 11:26:29 -0500 Subject: [PATCH 551/978] Add integration test for stack deploy with secrets. Signed-off-by: Daniel Nephin Upstream-commit: 4a1c23bc262b6933e941117eba78149171d715d9 Component: cli --- components/cli/command/secret/utils.go | 5 +-- components/cli/command/stack/deploy.go | 27 ++++++++++++---- .../cli/compose/convert/compose_test.go | 32 +++++++++++++++++++ 3 files changed, 55 insertions(+), 9 deletions(-) diff --git a/components/cli/command/secret/utils.go b/components/cli/command/secret/utils.go index 42493896ca..11d31ffd16 100644 --- a/components/cli/command/secret/utils.go +++ b/components/cli/command/secret/utils.go @@ -11,7 +11,8 @@ import ( "golang.org/x/net/context" ) -func getSecretsByNameOrIDPrefixes(ctx context.Context, client client.APIClient, terms []string) ([]swarm.Secret, error) { +// GetSecretsByNameOrIDPrefixes returns secrets given a list of ids or names +func GetSecretsByNameOrIDPrefixes(ctx context.Context, client client.APIClient, terms []string) ([]swarm.Secret, error) { args := filters.NewArgs() for _, n := range terms { args.Add("names", n) @@ -24,7 +25,7 @@ func getSecretsByNameOrIDPrefixes(ctx context.Context, client client.APIClient, } func getCliRequestedSecretIDs(ctx context.Context, client client.APIClient, terms []string) ([]string, error) { - secrets, err := getSecretsByNameOrIDPrefixes(ctx, client, terms) + secrets, err := GetSecretsByNameOrIDPrefixes(ctx, client, terms) if err != nil { return nil, err } diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index 6856624128..203ae6d39c 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -1,24 +1,24 @@ package stack import ( - "errors" "fmt" "io/ioutil" "os" "sort" "strings" - "github.com/spf13/cobra" - "golang.org/x/net/context" - "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + secretcli "github.com/docker/docker/cli/command/secret" "github.com/docker/docker/cli/compose/convert" "github.com/docker/docker/cli/compose/loader" composetypes "github.com/docker/docker/cli/compose/types" dockerclient "github.com/docker/docker/client" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "golang.org/x/net/context" ) const ( @@ -228,9 +228,22 @@ func createSecrets( ) error { client := dockerCli.Client() - for _, secret := range secrets { - fmt.Fprintf(dockerCli.Out(), "Creating secret %s\n", secret.Name) - _, err := client.SecretCreate(ctx, secret) + for _, secretSpec := range secrets { + // TODO: fix this after https://github.com/docker/docker/pull/29218 + secrets, err := secretcli.GetSecretsByNameOrIDPrefixes(ctx, client, []string{secretSpec.Name}) + switch { + case err != nil: + return err + case len(secrets) > 1: + return errors.Errorf("ambiguous secret name: %s", secretSpec.Name) + case len(secrets) == 0: + fmt.Fprintf(dockerCli.Out(), "Creating secret %s\n", secretSpec.Name) + _, err = client.SecretCreate(ctx, secretSpec) + default: + secret := secrets[0] + // Update secret to ensure that the local data hasn't changed + err = client.SecretUpdate(ctx, secret.ID, secret.Meta.Version, secretSpec) + } if err != nil { return err } diff --git a/components/cli/compose/convert/compose_test.go b/components/cli/compose/convert/compose_test.go index d88ac7f7c4..18c7aac938 100644 --- a/components/cli/compose/convert/compose_test.go +++ b/components/cli/compose/convert/compose_test.go @@ -7,6 +7,7 @@ import ( "github.com/docker/docker/api/types/network" composetypes "github.com/docker/docker/cli/compose/types" "github.com/docker/docker/pkg/testutil/assert" + "github.com/docker/docker/pkg/testutil/tempfile" ) func TestNamespaceScope(t *testing.T) { @@ -88,3 +89,34 @@ func TestNetworks(t *testing.T) { assert.DeepEqual(t, networks, expected) assert.DeepEqual(t, externals, []string{"special"}) } + +func TestSecrets(t *testing.T) { + namespace := Namespace{name: "foo"} + + secretText := "this is the first secret" + secretFile := tempfile.NewTempFile(t, "convert-secrets", secretText) + defer secretFile.Remove() + + source := map[string]composetypes.SecretConfig{ + "one": { + File: secretFile.Name(), + Labels: map[string]string{"monster": "mash"}, + }, + "ext": { + External: composetypes.External{ + External: true, + }, + }, + } + + specs, err := Secrets(namespace, source) + assert.NilError(t, err) + assert.Equal(t, len(specs), 1) + secret := specs[0] + assert.Equal(t, secret.Name, "foo_one") + assert.DeepEqual(t, secret.Labels, map[string]string{ + "monster": "mash", + LabelNamespace: "foo", + }) + assert.DeepEqual(t, secret.Data, []byte(secretText)) +} From c63bb4b9c18cea1596f18f42230b49cca6669e4d Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 18 Jan 2017 13:06:36 -0500 Subject: [PATCH 552/978] Test and fix external secrets in stack deploy. Signed-off-by: Daniel Nephin Upstream-commit: 682d75fa3fb73b536aa3e98c51130c82771b173a Component: cli --- components/cli/compose/convert/service.go | 12 ++++++++++-- components/cli/compose/loader/loader.go | 3 +-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/components/cli/compose/convert/service.go b/components/cli/compose/convert/service.go index 78ad308d38..573f7723fd 100644 --- a/components/cli/compose/convert/service.go +++ b/components/cli/compose/convert/service.go @@ -31,7 +31,7 @@ func Services( for _, service := range services { - secrets, err := convertServiceSecrets(client, namespace, service.Secrets) + secrets, err := convertServiceSecrets(client, namespace, service.Secrets, config.Secrets) if err != nil { return nil, err } @@ -181,6 +181,7 @@ func convertServiceSecrets( client client.SecretAPIClient, namespace Namespace, secrets []composetypes.ServiceSecretConfig, + secretSpecs map[string]composetypes.SecretConfig, ) ([]*swarm.SecretReference, error) { opts := []*types.SecretRequestOption{} for _, secret := range secrets { @@ -188,8 +189,15 @@ func convertServiceSecrets( if target == "" { target = secret.Source } + + source := namespace.Scope(secret.Source) + secretSpec := secretSpecs[secret.Source] + if secretSpec.External.External { + source = secretSpec.External.Name + } + opts = append(opts, &types.SecretRequestOption{ - Source: namespace.Scope(secret.Source), + Source: source, Target: target, UID: secret.UID, GID: secret.GID, diff --git a/components/cli/compose/loader/loader.go b/components/cli/compose/loader/loader.go index a43347f475..39f69a03ff 100644 --- a/components/cli/compose/loader/loader.go +++ b/components/cli/compose/loader/loader.go @@ -422,8 +422,7 @@ func loadVolumes(source types.Dict) (map[string]types.VolumeConfig, error) { // TODO: remove duplicate with networks/volumes func loadSecrets(source types.Dict, workingDir string) (map[string]types.SecretConfig, error) { secrets := make(map[string]types.SecretConfig) - err := transform(source, &secrets) - if err != nil { + if err := transform(source, &secrets); err != nil { return secrets, err } for name, secret := range secrets { From 65bef339c158331aea4342bdee4b14fb7a71b9c9 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 18 Jan 2017 14:40:29 -0500 Subject: [PATCH 553/978] Remove secrets as part of stack remove. Signed-off-by: Daniel Nephin Upstream-commit: 40bde3ee0092005d3e09a4b3150e8d888bc620c2 Component: cli --- components/cli/command/stack/common.go | 10 ++++ components/cli/command/stack/remove.go | 75 +++++++++++++++++++------- 2 files changed, 67 insertions(+), 18 deletions(-) diff --git a/components/cli/command/stack/common.go b/components/cli/command/stack/common.go index 5c4996d666..72719f94fc 100644 --- a/components/cli/command/stack/common.go +++ b/components/cli/command/stack/common.go @@ -48,3 +48,13 @@ func getStackNetworks( ctx, types.NetworkListOptions{Filters: getStackFilter(namespace)}) } + +func getStackSecrets( + ctx context.Context, + apiclient client.APIClient, + namespace string, +) ([]swarm.Secret, error) { + return apiclient.SecretList( + ctx, + types.SecretListOptions{Filters: getStackFilter(namespace)}) +} diff --git a/components/cli/command/stack/remove.go b/components/cli/command/stack/remove.go index 734ff92a53..966c1aa6bf 100644 --- a/components/cli/command/stack/remove.go +++ b/components/cli/command/stack/remove.go @@ -3,11 +3,12 @@ package stack import ( "fmt" - "golang.org/x/net/context" - + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type removeOptions struct { @@ -33,41 +34,79 @@ func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { func runRemove(dockerCli *command.DockerCli, opts removeOptions) error { namespace := opts.namespace client := dockerCli.Client() - stderr := dockerCli.Err() ctx := context.Background() - hasError := false services, err := getServices(ctx, client, namespace) if err != nil { return err } - for _, service := range services { - fmt.Fprintf(stderr, "Removing service %s\n", service.Spec.Name) - if err := client.ServiceRemove(ctx, service.ID); err != nil { - hasError = true - fmt.Fprintf(stderr, "Failed to remove service %s: %s", service.ID, err) - } - } networks, err := getStackNetworks(ctx, client, namespace) if err != nil { return err } - for _, network := range networks { - fmt.Fprintf(stderr, "Removing network %s\n", network.Name) - if err := client.NetworkRemove(ctx, network.ID); err != nil { - hasError = true - fmt.Fprintf(stderr, "Failed to remove network %s: %s", network.ID, err) - } + + secrets, err := getStackSecrets(ctx, client, namespace) + if err != nil { + return err } - if len(services) == 0 && len(networks) == 0 { + if len(services)+len(networks)+len(secrets) == 0 { fmt.Fprintf(dockerCli.Out(), "Nothing found in stack: %s\n", namespace) return nil } + hasError := removeServices(ctx, dockerCli, services) + hasError = removeSecrets(ctx, dockerCli, secrets) || hasError + hasError = removeNetworks(ctx, dockerCli, networks) || hasError + if hasError { return fmt.Errorf("Failed to remove some resources") } return nil } + +func removeServices( + ctx context.Context, + dockerCli *command.DockerCli, + services []swarm.Service, +) bool { + var err error + for _, service := range services { + fmt.Fprintf(dockerCli.Err(), "Removing service %s\n", service.Spec.Name) + if err = dockerCli.Client().ServiceRemove(ctx, service.ID); err != nil { + fmt.Fprintf(dockerCli.Err(), "Failed to remove service %s: %s", service.ID, err) + } + } + return err != nil +} + +func removeNetworks( + ctx context.Context, + dockerCli *command.DockerCli, + networks []types.NetworkResource, +) bool { + var err error + for _, network := range networks { + fmt.Fprintf(dockerCli.Err(), "Removing network %s\n", network.Name) + if err = dockerCli.Client().NetworkRemove(ctx, network.ID); err != nil { + fmt.Fprintf(dockerCli.Err(), "Failed to remove network %s: %s", network.ID, err) + } + } + return err != nil +} + +func removeSecrets( + ctx context.Context, + dockerCli *command.DockerCli, + secrets []swarm.Secret, +) bool { + var err error + for _, secret := range secrets { + fmt.Fprintf(dockerCli.Err(), "Removing secret %s\n", secret.Spec.Name) + if err = dockerCli.Client().SecretRemove(ctx, secret.ID); err != nil { + fmt.Fprintf(dockerCli.Err(), "Failed to remove secret %s: %s", secret.ID, err) + } + } + return err != nil +} From ef211db9b69c8339b2895f9943063b5e96fa3aa3 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 20 Jan 2017 17:06:35 -0500 Subject: [PATCH 554/978] Rebase Compose v3.1 schema on the latest v3 schema. Signed-off-by: Daniel Nephin Upstream-commit: b0eabe77183d513661fcc76b49c87b6accc4e33a Component: cli --- components/cli/compose/schema/bindata.go | 2 +- .../schema/data/config_schema_v3.1.json | 22 ++++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/components/cli/compose/schema/bindata.go b/components/cli/compose/schema/bindata.go index 3713315b2a..9486e91ae0 100644 --- a/components/cli/compose/schema/bindata.go +++ b/components/cli/compose/schema/bindata.go @@ -89,7 +89,7 @@ func dataConfig_schema_v30Json() (*asset, error) { return a, nil } -var _dataConfig_schema_v31Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x1a\x4d\x93\xdb\xa8\xf2\xee\x5f\xa1\x52\x72\x8b\x67\x26\xaf\x5e\xea\x55\xbd\xdc\xf6\xb8\xa7\xdd\xf3\x4e\x39\x2a\x46\x6a\xcb\x64\x10\x10\x40\xce\x38\x29\xff\xf7\x2d\xf4\x65\xc0\x20\xb0\xad\xec\xcc\x61\x4f\x33\x16\xdd\x4d\x7f\x77\xd3\xf0\x73\x95\x65\xf9\x7b\x59\xee\xa0\x41\xf9\xe7\x2c\xdf\x29\xc5\x3f\x3f\x3c\x7c\x95\x8c\xde\xf5\x5f\xef\x99\xa8\x1f\x2a\x81\xb6\xea\xee\xe3\xa7\x87\xfe\xdb\xbb\x7c\xad\xf1\x70\xa5\x51\x4a\x46\xb7\xb8\x2e\xfa\x95\x62\xff\xdf\xfb\xff\xdc\x6b\xf4\x1e\x44\x1d\x38\x68\x20\xf6\xf4\x15\x4a\xd5\x7f\x13\xf0\xad\xc5\x02\x34\xf2\x63\xbe\x07\x21\x31\xa3\xf9\x66\xbd\xd2\x6b\x5c\x30\x0e\x42\x61\x90\xf9\xe7\x4c\x33\x97\x65\x13\xc8\xf8\xc1\x20\x2b\x95\xc0\xb4\xce\xbb\xcf\xc7\x8e\x42\x96\xe5\x12\xc4\x1e\x97\x06\x85\x89\xd5\x77\x0f\x27\xfa\x0f\x13\xd8\xda\xa5\x6a\x30\xdb\x7d\xe7\x48\x29\x10\xf4\xcf\x73\xde\xba\xe5\x2f\x8f\xe8\xee\xc7\x6f\x77\x7f\x7d\xbc\xfb\xff\x7d\x71\xb7\xf9\xf0\xde\x5a\xd6\xfa\x15\xb0\xed\xb7\xaf\x60\x8b\x29\x56\x98\xd1\x69\xff\x7c\x82\x3c\x0e\xff\x1d\xa7\x8d\x51\x55\x75\xc0\x88\x58\x7b\x6f\x11\x91\x60\xcb\x4c\x41\x7d\x67\xe2\x39\x26\xf3\x04\xf6\x4a\x32\x0f\xfb\x7b\x64\xb6\xc5\xd9\x33\xd2\x36\x51\x0b\x8e\x50\xaf\x24\x4c\xbf\xfd\x32\xf6\x93\x50\x0a\x50\x71\x97\xed\xa1\x5e\xcd\x63\xf5\xf6\xb7\x09\xbc\x1a\x85\x9e\x85\xed\x21\x8c\xbd\x3b\x06\xad\xf0\xf6\xa9\xca\x17\x5e\x61\x5d\x4d\xca\x0a\x68\xa9\x02\x4e\xd8\x41\x7f\x0b\xe8\xa3\x07\x68\x80\xaa\x7c\x52\x41\x96\xe5\x4f\x2d\x26\x95\xab\x51\x46\xe1\x0f\x4d\xe2\xd1\xf8\x98\x65\x3f\xdd\x4c\x66\xd0\xe9\xd6\xad\x5f\x61\x83\x4f\xeb\x01\x59\xa6\xf5\x92\x51\x05\x2f\xaa\x13\x6a\x7e\xeb\x5e\x05\xac\x7c\x06\xb1\xc5\x04\x52\x31\x90\xa8\xe5\x8c\xca\x08\x96\xaa\x60\xa2\xa8\x70\xa9\xf2\xa3\x83\x7e\x46\x2f\xee\x4f\x13\xaa\xf1\x6b\xb3\xf2\x10\xcc\x4b\xc4\x0b\x54\x55\x96\x1c\x48\x08\x74\xc8\xd7\x59\x8e\x15\x34\xd2\x2f\x62\x96\xb7\x14\x7f\x6b\xe1\xf7\x01\x44\x89\x16\x5c\xba\x95\x60\x7c\x79\xc2\xb5\x60\x2d\x2f\x38\x12\xda\xc1\xe6\xd5\x9f\x97\xac\x69\x10\x5d\xca\xeb\x2e\x91\x23\x41\xf3\x8c\x2a\x84\x29\x88\x82\xa2\x26\xe6\x48\x3a\xea\x80\x56\xb2\xe8\x0b\xfe\xac\x1b\x6d\x8b\x1e\x5f\x3a\x04\xa6\xea\xbf\xa8\x3d\x2a\x3a\xe7\xd8\x3d\x19\xed\xda\x9a\xb7\xdc\x41\x2c\x24\x20\x51\xee\xae\xc4\x67\x0d\xc2\x34\x45\x77\x40\x95\x38\x70\x86\x7b\x7f\x79\x73\x8e\x00\x74\x5f\x4c\xb9\xe4\x62\x35\x00\xdd\x63\xc1\x68\x33\x46\x43\x4a\x82\x99\x92\xbc\xc6\x7f\xe1\x4c\x82\xab\x18\x47\x40\x73\x69\x12\xd5\xd2\xc9\x88\xf1\x38\x0a\xbe\xce\x72\xda\x36\x4f\x20\x74\x0f\x6b\x41\x6e\x99\x68\x90\x66\x76\xdc\xdb\x58\xb6\x34\xed\xf1\x3c\x53\x81\xa6\x0c\xba\xac\x23\x52\x10\x4c\x9f\x97\x77\x71\x78\x51\x02\x15\x3b\x26\x55\x7a\x0e\x37\xd0\x77\x80\x88\xda\x95\x3b\x28\x9f\x67\xd0\x4d\x28\x0b\x9b\x49\x95\xe2\xe4\xb8\x41\x75\x1c\x88\x97\x31\x10\x82\x9e\x80\x5c\x25\xe7\xa2\xca\x37\xc8\xb2\xba\xd6\xa0\x21\x8f\x3b\xeb\x5c\x86\xe5\x58\xcd\xaf\x04\xde\x83\x48\x2d\xe0\x8c\x9f\x1a\x2e\x77\x31\xde\x80\x64\xf1\xee\xd3\x02\xfd\x72\xdf\x37\x9f\x33\x51\xd5\xfd\x47\x48\xbe\x71\xdb\x85\xcc\xa9\xfb\xbe\x2f\x8e\x84\x69\x0d\x85\x65\x95\x06\x95\xba\x6f\x10\x20\x03\x76\x3d\x81\x0e\xa7\x9b\xa2\x61\x55\xc8\x41\xcf\x80\x5d\xdd\x04\x33\xf5\xc5\x85\x30\xbb\xaa\x7f\x4c\x32\x5d\xf4\x00\x11\x91\x26\xc4\x5e\x2a\x9b\x27\x76\xe3\x2e\xd6\xc1\x21\x82\x91\x84\x78\xb0\x07\x15\x69\x51\xc3\x7c\xff\x29\xd1\x27\x7c\xb8\xff\x9b\xc5\x0d\xa0\x06\x69\xa6\xf7\xc8\x11\x52\x27\x56\xba\x70\xf3\x31\xb2\x89\x44\xdb\x2f\x6e\xe1\x39\xae\xc2\xb9\xa2\xcb\x10\x66\x80\x71\x26\xd4\x59\x74\xfd\x33\xe5\xbe\xdf\xfa\xe6\x6a\xcf\x05\xde\x63\x02\x35\xd8\xa7\x96\x27\xc6\x08\x20\x6a\xa5\x1e\x01\xa8\x2a\x18\x25\x87\x04\x48\xa9\x90\x88\x1e\x28\x24\x94\xad\xc0\xea\x50\x30\xae\x16\xef\x33\xe4\xae\x29\x24\xfe\x01\xb6\x35\x4f\xf9\x7e\x20\xb4\x71\x18\x72\x26\x24\x57\x1a\x34\x94\x92\xe2\x61\xec\x49\x84\xd1\x44\x15\x4f\x51\xb9\x64\xad\x28\x53\x0f\xd8\x7a\x4f\x24\x6a\x48\x3d\xc2\x6b\x77\xb3\xc3\x66\x1e\xb8\xbe\x04\xf8\xac\xd0\x0d\x26\x8c\x55\x65\xf7\xb7\x99\x57\x8e\xde\xd0\x97\x07\x59\xaa\xeb\xba\x35\xa9\x2a\x4c\x0b\xc6\x81\x46\x63\x43\x2a\xc6\x0b\x89\x6b\x8a\x48\x34\x3e\x34\x68\x2d\x50\x09\x05\x07\x81\x99\x57\x6b\x6b\x33\x29\x54\xad\x40\x9a\x55\x8b\x8c\x6a\xf8\xf6\xca\x63\xa5\x52\xf1\x60\x6f\x09\x6e\x70\x38\x68\x3c\x5e\x9b\xd0\x01\xf4\xd5\xdf\x5f\xf4\x67\x0a\xfe\x89\x53\x4c\x15\xd4\xda\x4d\xce\x9d\x6a\xa6\xe7\x9c\x6f\x39\x13\x7a\xcd\x1d\x12\xb6\x95\x66\xf8\xc8\xfa\xc0\xdc\x2a\x3f\x82\xaf\x13\xf5\xf2\x65\xdd\x75\x74\xf4\xd6\x03\x23\x1b\x2f\xfc\x45\xc5\xdc\x65\x63\x13\xac\xa7\xfe\xa0\x6a\x65\xf4\x58\xd0\xc1\x50\x39\xd7\xd2\x4e\xa0\xc6\xd0\x7e\xd1\x6a\xa1\xdb\x64\x1d\x04\x15\xf6\x73\xbb\x72\x24\xbb\x60\xec\xee\x9c\x58\x47\x02\xbe\x79\xb2\x09\xea\xce\x94\x1f\x27\xdf\x1c\x3b\x91\xd3\x24\x3e\x30\x5c\xd6\xae\x24\xf6\x56\x9e\xf1\xe9\x54\xe1\x06\x58\xab\x22\x50\x02\x94\xc0\x8e\xe6\xc7\x54\x6c\x12\x03\xf9\x36\x07\x43\x15\x96\xe8\xc9\x99\x31\x4f\xe9\xec\x2a\xf3\x66\xa7\x01\xfe\x38\x30\x9a\x33\xae\x01\xb9\x80\x6d\x53\x82\x45\x00\x27\xb8\x44\x32\x96\x90\x6e\x18\x53\xb4\xbc\x42\x0a\x8a\xfe\x7e\xf6\xa2\x12\x30\x93\xfb\x39\x12\x88\x10\x20\x58\x36\x29\xb9\x34\xaf\x80\xa0\xc3\x55\xb5\xb1\x43\xdf\x22\x4c\x5a\x01\x05\x2a\xd5\x70\x05\x1c\xf1\xcc\xbc\x61\x14\x2b\xe6\xcd\x14\x69\x5b\x36\xe8\xa5\x18\xb7\xed\x40\x62\x1d\x8e\xdd\xdc\xa7\x4e\x18\x0c\x4f\xe8\x1b\xc0\xcb\xaa\xf4\x8c\x89\x4e\x35\x3f\xe0\x31\xe3\x8e\x67\xa2\x0b\x90\x3a\x29\x4d\x03\xa0\x28\x7e\xb4\xc4\x0c\xa7\x8d\x82\x33\x82\xcb\xc3\x52\x12\x96\x8c\xf6\x4a\x4e\x71\x88\x1b\x3d\x50\xbb\x83\x6e\x89\x1a\xae\xa2\xc1\xda\x21\x7c\xc7\xb4\x62\xdf\x2f\xd8\x70\x39\x57\xe2\x04\x95\xe0\xe4\xbb\x5b\x15\x2d\x95\x40\x98\xaa\x8b\xcb\xfa\xad\x62\xdd\x50\xd5\x27\xff\x8c\x64\xfd\x09\x2e\x7e\x9f\x1e\xc8\xf4\x25\x6f\xa3\x53\xc1\x06\x1a\x26\xbc\x0e\xb8\xc0\x83\x8f\x98\x88\x23\xd8\x02\x55\x2d\x69\x8c\x3c\x40\x15\x8c\x2f\x7f\xea\x88\x8f\x8a\x37\xf1\x84\x84\x39\x6a\x96\x8a\x8e\xe4\xc1\x7a\xee\xad\xc1\xd9\xfc\xfc\x22\x0b\xcf\x30\x62\x5c\xc7\x79\x1f\x20\x64\xfb\x44\x03\xa3\x84\xf3\xd3\x86\xef\xb6\x3f\xfd\xb8\x72\x0c\x1f\x4e\x6e\x4b\x7a\xe3\x9d\x58\xc0\xaa\x8f\x53\x27\xb9\x9e\x74\xb5\x49\x36\x71\xf0\x42\x6a\x39\xfe\x2f\x6c\xf0\x6e\xc8\x19\xc3\x83\xa5\x48\xca\x18\xa0\xfe\xcd\x18\x6f\xc6\xbf\x66\x8a\xe2\x95\xa7\x83\x8b\x5f\xa6\xc5\x9c\x66\x80\xba\xba\x90\x26\x3c\x31\x7a\x53\x86\x78\x95\xf8\x75\x86\x81\x86\x49\xce\x4f\xf1\x73\x9a\x4c\xbe\x03\x1b\x30\x36\x36\x1b\x2e\x98\xe7\x5d\xae\x5d\xcd\xe6\x86\x3f\x23\x48\xe0\x4e\xc4\xd9\x74\x50\xe2\xbc\xe4\x0b\x66\x90\xfb\x0f\x33\x35\x7b\xee\xae\xfa\x17\x15\xbb\x05\x06\x6b\x7e\x9b\x3a\x8d\xfe\xa8\xdd\xf3\xb7\x96\x81\xf8\x37\xf0\xcf\x5e\x5e\x6a\x39\xe9\xe1\x6c\xca\xf4\xd3\x1e\x8e\xf6\xaf\x26\x37\x96\x7e\x1c\x90\xfe\xe5\x87\x91\xb2\x37\xe6\xd9\x27\x64\x46\xef\x7b\x4c\x77\x34\x3b\xbe\x8b\x0c\xdc\x44\xac\xcc\xbf\xdd\x1b\xd6\xd5\x71\xf5\x77\x00\x00\x00\xff\xff\xc8\x0f\x22\x69\x3c\x2f\x00\x00") +var _dataConfig_schema_v31Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x1a\xcb\x8e\xdb\x36\xf0\xee\xaf\x10\x94\xdc\xe2\xdd\x4d\xd1\xa0\x40\x73\xeb\xb1\xa7\xf6\xdc\x85\x23\xd0\xd2\x58\x66\x96\x22\x19\x92\x72\xd6\x09\xfc\xef\x05\xf5\x32\x45\x91\x22\x6d\x2b\xd9\x45\xd1\xd3\xae\xc5\x99\xe1\xbc\x67\x38\xe4\xf7\x55\x92\xa4\x6f\x65\xbe\x87\x0a\xa5\x1f\x93\x74\xaf\x14\xff\xf8\xf0\xf0\x59\x32\x7a\xd7\x7e\xbd\x67\xa2\x7c\x28\x04\xda\xa9\xbb\xf7\x1f\x1e\xda\x6f\x6f\xd2\xb5\xc6\xc3\x85\x46\xc9\x19\xdd\xe1\x32\x6b\x57\xb2\xc3\xaf\xf7\xbf\xdc\x6b\xf4\x16\x44\x1d\x39\x68\x20\xb6\xfd\x0c\xb9\x6a\xbf\x09\xf8\x52\x63\x01\x1a\xf9\x31\x3d\x80\x90\x98\xd1\x74\xb3\x5e\xe9\x35\x2e\x18\x07\xa1\x30\xc8\xf4\x63\xa2\x99\x4b\x92\x01\xa4\xff\x60\x90\x95\x4a\x60\x5a\xa6\xcd\xe7\x53\x43\x21\x49\x52\x09\xe2\x80\x73\x83\xc2\xc0\xea\x9b\x87\x33\xfd\x87\x01\x6c\x6d\x53\x35\x98\x6d\xbe\x73\xa4\x14\x08\xfa\xf7\x94\xb7\x66\xf9\xd3\x23\xba\xfb\xf6\xc7\xdd\x3f\xef\xef\x7e\xbf\xcf\xee\x36\xef\xde\x8e\x96\xb5\x7e\x05\xec\xda\xed\x0b\xd8\x61\x8a\x15\x66\x74\xd8\x3f\x1d\x20\x4f\xdd\x7f\xa7\x61\x63\x54\x14\x0d\x30\x22\xa3\xbd\x77\x88\x48\x18\xcb\x4c\x41\x7d\x65\xe2\x29\x24\xf3\x00\xf6\x42\x32\x77\xfb\x3b\x64\x1e\x8b\x73\x60\xa4\xae\x82\x16\xec\xa1\x5e\x48\x98\x76\xfb\x65\xec\x27\x21\x17\xa0\xc2\x2e\xdb\x42\xbd\x98\xc7\xea\xed\x6f\x13\x78\xd5\x0b\x3d\x0b\xdb\x42\x18\x7b\x37\x0c\x8e\xc2\xdb\xa5\x2a\x57\x78\xf9\x75\x35\x28\xcb\xa3\xa5\x02\x38\x61\x47\xfd\xcd\xa3\x8f\x16\xa0\x02\xaa\xd2\x41\x05\x49\x92\x6e\x6b\x4c\x0a\x5b\xa3\x8c\xc2\x5f\x9a\xc4\xa3\xf1\x31\x49\xbe\xdb\x99\xcc\xa0\xd3\xac\x8f\x7e\xf9\x0d\x3e\xac\x7b\x64\x19\xd6\x73\x46\x15\x3c\xab\x46\xa8\xf9\xad\x5b\x15\xb0\xfc\x09\xc4\x0e\x13\x88\xc5\x40\xa2\x94\x33\x2a\x23\x58\xaa\x8c\x89\xac\xc0\xb9\x4a\x4f\x16\xfa\x84\x5e\xd8\x9f\x06\x54\xe3\xd7\x66\xe5\x20\x98\xe6\x88\x67\xa8\x28\x46\x72\x20\x21\xd0\x31\x5d\x27\x29\x56\x50\x49\xb7\x88\x49\x5a\x53\xfc\xa5\x86\x3f\x3b\x10\x25\x6a\xb0\xe9\x16\x82\xf1\xe5\x09\x97\x82\xd5\x3c\xe3\x48\x68\x07\x9b\x57\x7f\x9a\xb3\xaa\x42\x74\x29\xaf\xbb\x44\x8e\x08\xcd\x33\xaa\x10\xa6\x20\x32\x8a\xaa\x90\x23\xe9\xa8\x03\x5a\xc8\xac\x2d\xf8\xb3\x6e\xb4\xcb\x5a\x7c\x69\x11\x18\xaa\xff\xa2\xf6\x28\xe8\x9c\x63\xb7\x64\xb4\x6b\x6b\xde\x52\x0b\x31\x93\x80\x44\xbe\xbf\x12\x9f\x55\x08\xd3\x18\xdd\x01\x55\xe2\xc8\x19\x6e\xfd\xe5\xd5\x39\x02\xd0\x43\x36\xe4\x92\x8b\xd5\x00\xf4\x80\x05\xa3\x55\x1f\x0d\x31\x09\x66\x48\xf2\x1a\xff\x99\x33\x09\xb6\x62\x2c\x01\xcd\xa5\x41\xd4\x91\x4e\x7a\x8c\xc7\x5e\xf0\x75\x92\xd2\xba\xda\x82\xd0\x3d\xec\x08\x72\xc7\x44\x85\x34\xb3\xfd\xde\xc6\xf2\x48\xd3\x0e\xcf\x33\x15\x68\xca\xa0\xcb\x3a\x22\x19\xc1\xf4\x69\x79\x17\x87\x67\x25\x50\xb6\x67\x52\xc5\xe7\x70\x03\x7d\x0f\x88\xa8\x7d\xbe\x87\xfc\x69\x06\xdd\x84\x1a\x61\x33\xa9\x62\x9c\x1c\x57\xa8\x0c\x03\xf1\x3c\x04\x42\xd0\x16\xc8\x55\x72\x2e\xaa\x7c\x83\x2c\x2b\x4b\x0d\xea\xf3\xb8\x49\xe7\xd2\x2d\x87\x6a\x7e\x21\xf0\x01\x44\x6c\x01\x67\xfc\xdc\x70\xd9\x8b\xe1\x06\x24\x09\x77\x9f\x23\xd0\x4f\xf7\x6d\xf3\x39\x13\x55\xcd\x7f\x84\xa4\x1b\xbb\x5d\x48\xac\xba\xef\xfa\x62\x49\x18\xd7\x50\x8c\xac\x52\xa1\x5c\xf7\x0d\x02\xa4\xc7\xae\x67\xd0\xee\x74\x93\x55\xac\xf0\x39\xe8\x04\xd8\xd6\x8d\x37\x53\x5f\x5c\x08\x93\xab\xfa\xc7\x28\xd3\x05\x0f\x10\x01\x69\x7c\xec\xc5\xb2\x79\x66\x37\xec\x62\x0d\x1c\x22\x18\x49\x08\x07\xbb\x57\x91\x23\x6a\x98\x1f\x3e\x44\xfa\x84\x0b\xf7\xb7\x59\x5c\x0f\xaa\x97\x66\x7c\x8f\x1c\x20\x75\x66\xa5\x09\x37\x17\x23\x9b\x40\xb4\xfd\xe0\x16\x9e\xe3\xc2\x9f\x2b\x9a\x0c\x61\x06\x18\x67\x42\x4d\xa2\xeb\xe7\x94\xfb\x76\xeb\x9b\xab\x3d\x17\xf8\x80\x09\x94\x30\x3e\xb5\x6c\x19\x23\x80\xe8\x28\xf5\x08\x40\x45\xc6\x28\x39\x46\x40\x4a\x85\x44\xf0\x40\x21\x21\xaf\x05\x56\xc7\x8c\x71\xb5\x78\x9f\x21\xf7\x55\x26\xf1\x37\x18\x5b\xf3\x9c\xef\x3b\x42\x1b\x8b\x21\x6b\x42\x72\xa5\x41\x7d\x29\x29\x1c\xc6\x8e\x44\x18\x4c\x54\xe1\x14\x95\x4a\x56\x8b\x3c\xf6\x80\xad\xf7\x44\xa2\x84\xd8\x23\xbc\x76\xb7\x71\xd8\xcc\x03\x97\x97\x00\x4f\x0a\x5d\x67\xc2\x50\x55\xb6\x7f\x9b\x79\xe5\xe4\x0c\x7d\x79\x94\xb9\xba\xae\x5b\x93\xaa\xc0\x34\x63\x1c\x68\x30\x36\xa4\x62\x3c\x2b\x05\xca\x21\xe3\x20\x30\x73\xaa\x62\x6d\x46\x7a\x51\x0b\xa4\xf7\x9f\x92\x91\xb8\xa4\x88\x84\xc2\x4c\x55\x7c\x77\xe5\xb1\x52\xa9\x70\xb0\xd7\x04\x57\xd8\x1f\x34\x0e\xaf\x8d\xe8\x00\xda\xea\xef\x2e\xfa\x33\x05\xff\xcc\x29\xa6\x0a\x4a\xed\x26\x53\xa7\x9a\xe9\x39\xe7\x5b\xce\x88\x5e\x73\x8f\xc4\xd8\xa0\x33\x7c\x24\x6d\x60\xee\x94\x1b\xc1\xd5\x89\x3a\xf9\x1a\xdd\x75\x34\xf4\xd6\x1d\x23\x1b\x27\xfc\x45\xc5\xdc\x66\x63\xe3\xad\xa7\xee\xa0\xaa\x65\xf0\x58\xd0\xc0\x50\x39\xd7\xd2\x0e\xa0\xc6\xd0\x7e\xd1\x6a\xa1\xdb\x64\x1d\x04\x05\x76\x73\xbb\xb2\x24\xbb\x60\xec\x6e\x9d\x58\x7b\x02\xae\x79\xb2\x09\x1a\x9c\xbf\xcf\xcf\xb6\x3b\x20\xef\xdc\x19\x4b\xb4\xb5\x26\xae\xae\xe0\xd6\xde\x28\x0e\xe1\x1c\x23\x40\x09\x6c\xd9\xa5\x4f\xd4\x66\x3e\x01\xf9\x3a\xc7\x46\x0a\x57\xc0\x6a\x77\xc1\x5b\x99\xfe\xdd\x21\xa5\xc6\x5c\x3e\x60\x54\x03\xd2\xb6\xe9\xe3\x60\xd4\xbe\xbb\x0c\x1a\x2e\x26\x48\x04\x70\x82\x73\x24\x43\x89\xe8\x86\xf1\x44\xcd\x0b\xa4\x20\x6b\xef\x65\x2f\x4a\xfd\x33\x39\x9f\x23\x81\x08\x01\x82\x65\x15\x93\x43\xd3\x02\x08\x3a\x5e\x55\x3e\x1b\xf4\x1d\xc2\xa4\x16\x90\xa1\x5c\x75\x57\xbf\x01\x9f\x4b\x2b\x46\xb1\x62\xce\x0c\x11\xb7\x65\x85\x9e\xb3\x7e\xdb\x06\x24\xd4\xd9\x8c\x9b\xfa\xd8\xc9\x82\xe1\x09\x6d\xe3\x77\x59\x75\x9e\x31\xd1\xb9\xd6\x7b\x3c\xa6\xdf\x71\x22\xba\x00\xa9\x33\xc9\x30\xf8\x09\xe2\x07\x4b\x4b\x77\xca\xc8\x38\x23\x38\x3f\x2e\x25\x61\xce\x68\xab\xe4\x18\x87\xb8\xd1\x03\xb5\x3b\xe8\x56\xa8\xe2\x2a\x18\xac\x0d\xc2\x57\x4c\x0b\xf6\xf5\x82\x0d\x97\x73\x25\x4e\x50\x0e\x56\xbe\xbb\x55\xd1\x52\x09\x84\xa9\xba\xb8\x9c\xdf\x2a\xd6\x0d\xd5\x7c\xf0\xcf\x40\xd6\x1f\xe0\xc2\xf7\xe8\x9e\x4c\x9f\xf3\x3a\x38\x0d\xac\xa0\x62\xc2\xe9\x80\x0b\x3c\xf4\x08\x89\xd8\x83\x2d\x50\xd5\xa2\xc6\xc7\x1d\x54\xc6\xf8\xf2\xa7\x8d\xf0\x88\x78\x13\x4e\x48\x98\xa3\x6a\xa9\xe8\x88\x1e\xa8\xa7\xce\x1a\x9c\xcc\xcf\x2d\x12\xff\xec\x22\xc4\x75\x98\xf7\x0e\x42\xd6\x5b\xea\x19\x21\x4c\x4f\x19\xae\x5b\xfe\xf8\x63\xca\xc9\x7f\x28\xb9\x2d\xe9\xf5\x77\x61\x1e\xab\x3e\x0e\x3d\xf3\x7a\xd0\xd5\x26\xda\xc4\xde\x8b\xa8\xe5\xf8\x6f\xda\x77\x7b\x44\xe0\xea\xf3\x2f\xec\x04\x6f\x48\x2e\xdd\x8b\xa6\x40\x6e\xe9\xa0\xfe\x4f\x2d\xff\x11\x47\xfc\x79\xfe\xd5\x3d\x20\x0b\xbe\xdc\x6a\xa0\xae\x2e\xce\x11\xcf\x95\x5e\x81\xcd\x5e\xda\x14\xe3\xc1\xa2\x61\x92\xe9\x99\x7f\x4e\x93\xd1\xf7\x69\x1d\xc6\x66\xcc\x86\x0d\xe6\x78\xe3\x3b\xae\x90\x73\x83\xa4\x1e\xc4\x73\xbf\x62\x6d\xda\x29\x71\x5e\xf2\x05\x93\xcd\xfd\xbb\x99\x3e\x60\xee\xde\xfb\x07\x15\xd0\x05\x86\x74\x6e\x9b\x5a\x87\x87\x5e\xbb\xd3\x77\x9b\x9e\xf8\x37\xf0\x27\xaf\x38\xb5\x9c\xf4\x38\x99\x49\x7d\x1f\x0f\x5a\xdb\x17\x98\x9b\x91\x7e\x2c\x90\xf6\x15\x89\x91\xdd\x37\xe6\x79\xca\x67\x46\xe7\xdb\x4e\x7b\xcc\xdb\xbf\xb1\xf4\xdc\x6a\xac\xcc\xbf\xcd\x7b\xd8\xd5\x69\xf5\x6f\x00\x00\x00\xff\xff\xfc\xf3\x11\x6a\x88\x2f\x00\x00") func dataConfig_schema_v31JsonBytes() ([]byte, error) { return bindataRead( diff --git a/components/cli/compose/schema/data/config_schema_v3.1.json b/components/cli/compose/schema/data/config_schema_v3.1.json index b67203218a..b7037485f9 100644 --- a/components/cli/compose/schema/data/config_schema_v3.1.json +++ b/components/cli/compose/schema/data/config_schema_v3.1.json @@ -198,8 +198,8 @@ }, "sysctls": {"$ref": "#/definitions/list_or_dict"}, "stdin_open": {"type": "boolean"}, - "stop_signal": {"type": "string"}, "stop_grace_period": {"type": "string", "format": "duration"}, + "stop_signal": {"type": "string"}, "tmpfs": {"$ref": "#/definitions/string_or_list"}, "tty": {"type": "boolean"}, "ulimits": { @@ -231,10 +231,11 @@ "healthcheck": { "id": "#/definitions/healthcheck", - "type": ["object", "null"], + "type": "object", + "additionalProperties": false, "properties": { - "interval": {"type":"string"}, - "timeout": {"type":"string"}, + "disable": {"type": "boolean"}, + "interval": {"type": "string"}, "retries": {"type": "number"}, "test": { "oneOf": [ @@ -242,9 +243,8 @@ {"type": "array", "items": {"type": "string"}} ] }, - "disable": {"type": "boolean"} - }, - "additionalProperties": false + "timeout": {"type": "string"} + } }, "deployment": { "id": "#/definitions/deployment", @@ -337,6 +337,7 @@ }, "additionalProperties": false }, + "internal": {"type": "boolean"}, "labels": {"$ref": "#/definitions/list_or_dict"} }, "additionalProperties": false @@ -357,10 +358,11 @@ "type": ["boolean", "object"], "properties": { "name": {"type": "string"} - } - } + }, + "additionalProperties": false + }, + "labels": {"$ref": "#/definitions/list_or_dict"} }, - "labels": {"$ref": "#/definitions/list_or_dict"}, "additionalProperties": false }, From 0e5b51f734be9827afa6adb6d217bf88bb9eac06 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 13 Jan 2017 11:26:29 -0500 Subject: [PATCH 555/978] Add integration test for stack deploy with secrets. Signed-off-by: Daniel Nephin Upstream-commit: 26816a911a5e12b2b78c66b958030fbc18193b6f Component: cli --- components/cli/secret_update.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/components/cli/secret_update.go b/components/cli/secret_update.go index b94e24aab0..42cdbbe176 100644 --- a/components/cli/secret_update.go +++ b/components/cli/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 b4ed9a1ae0d5ae6e5ea53572ae6c4abe65b38332 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 26 Jan 2017 12:00:46 -0500 Subject: [PATCH 556/978] Set default values for uid and gid to prevent errors when starting a service. Signed-off-by: Daniel Nephin Upstream-commit: 485a2b2b2fd11c93853fdedbf8434e3c6868dda2 Component: cli --- components/cli/compose/convert/service.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/components/cli/compose/convert/service.go b/components/cli/compose/convert/service.go index 573f7723fd..f23df26127 100644 --- a/components/cli/compose/convert/service.go +++ b/components/cli/compose/convert/service.go @@ -196,11 +196,20 @@ func convertServiceSecrets( source = secretSpec.External.Name } + uid := secret.UID + gid := secret.GID + if uid == "" { + uid = "0" + } + if gid == "" { + gid = "0" + } + opts = append(opts, &types.SecretRequestOption{ Source: source, Target: target, - UID: secret.UID, - GID: secret.GID, + UID: uid, + GID: gid, Mode: os.FileMode(secret.Mode), }) } From 57501b17a7d62e1abdee787ced7f5c1562366426 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 26 Jan 2017 14:07:44 -0500 Subject: [PATCH 557/978] Fix ImageDelete type Signed-off-by: Daniel Nephin Upstream-commit: 2d7a37e91cf4498b94b537221aaed780bec22378 Component: cli --- components/cli/image_prune_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/image_prune_test.go b/components/cli/image_prune_test.go index 61cf18ef35..68cd995d37 100644 --- a/components/cli/image_prune_test.go +++ b/components/cli/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 e8abd684ad21dcc903596210dbe6d0c8668e4a06 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 23 Nov 2016 04:58:15 -0800 Subject: [PATCH 558/978] 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 Upstream-commit: 9a06063feab153887029d22242b37ae8397df7aa Component: cli --- components/cli/interface.go | 2 +- components/cli/plugin_list.go | 15 ++++- components/cli/plugin_list_test.go | 92 +++++++++++++++++++++--------- 3 files changed, 78 insertions(+), 31 deletions(-) diff --git a/components/cli/interface.go b/components/cli/interface.go index 771a3d9a06..d30ba5f705 100644 --- a/components/cli/interface.go +++ b/components/cli/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/components/cli/plugin_list.go b/components/cli/plugin_list.go index 88c480a3e1..3acde3b966 100644 --- a/components/cli/plugin_list.go +++ b/components/cli/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/components/cli/plugin_list_test.go b/components/cli/plugin_list_test.go index 173e4b87f5..6a0e9844fc 100644 --- a/components/cli/plugin_list_test.go +++ b/components/cli/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 d883a70a2c2a1dfb9bf5eb82358bb6609aaa6122 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 23 Nov 2016 04:58:15 -0800 Subject: [PATCH 559/978] 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 Upstream-commit: 2a949b557446f784b654191056ae4bac57fbccc4 Component: cli --- components/cli/command/plugin/list.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/components/cli/command/plugin/list.go b/components/cli/command/plugin/list.go index 51590224b0..a1b231f570 100644 --- a/components/cli/command/plugin/list.go +++ b/components/cli/command/plugin/list.go @@ -4,6 +4,7 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/formatter" + "github.com/docker/docker/opts" "github.com/spf13/cobra" "golang.org/x/net/context" ) @@ -12,10 +13,11 @@ type listOptions struct { quiet bool noTrunc bool format string + filter opts.FilterOpt } func newListCommand(dockerCli *command.DockerCli) *cobra.Command { - var opts listOptions + opts := listOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ Use: "ls [OPTIONS]", @@ -32,12 +34,13 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command { flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display plugin IDs") flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output") flags.StringVar(&opts.format, "format", "", "Pretty-print plugins using a Go template") + flags.VarP(&opts.filter, "filter", "f", "Provide filter values (e.g. 'enabled=true')") return cmd } func runList(dockerCli *command.DockerCli, opts listOptions) error { - plugins, err := dockerCli.Client().PluginList(context.Background()) + plugins, err := dockerCli.Client().PluginList(context.Background(), opts.filter.Value()) if err != nil { return err } From 30585d157d815679a84178abc4e5b8233ad7796f Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 27 Jan 2017 16:09:02 +0100 Subject: [PATCH 560/978] Make docker stack deploy a little bit more indempotent Sort some slice fields before sending them to the swarm api so that it won't trigger an update. Signed-off-by: Vincent Demeester Upstream-commit: 3df952523cd3508dcb5d0a96e7eb994b3f9f891e Component: cli --- components/cli/compose/convert/service.go | 25 +++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/components/cli/compose/convert/service.go b/components/cli/compose/convert/service.go index f23df26127..a8613c0878 100644 --- a/components/cli/compose/convert/service.go +++ b/components/cli/compose/convert/service.go @@ -14,6 +14,7 @@ import ( "github.com/docker/docker/opts" runconfigopts "github.com/docker/docker/runconfig/opts" "github.com/docker/go-connections/nat" + "sort" ) // Services from compose-file types to engine API types @@ -110,9 +111,9 @@ func convertService( Command: service.Entrypoint, Args: service.Command, Hostname: service.Hostname, - Hosts: convertExtraHosts(service.ExtraHosts), + Hosts: sortStrings(convertExtraHosts(service.ExtraHosts)), Healthcheck: healthcheck, - Env: convertEnvironment(service.Environment), + Env: sortStrings(convertEnvironment(service.Environment)), Labels: AddStackLabel(namespace, service.Labels), Dir: service.WorkingDir, User: service.User, @@ -138,6 +139,17 @@ func convertService( return serviceSpec, nil } +func sortStrings(strs []string) []string { + sort.Strings(strs) + return strs +} + +type byNetworkTarget []swarm.NetworkAttachmentConfig + +func (a byNetworkTarget) Len() int { return len(a) } +func (a byNetworkTarget) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a byNetworkTarget) Less(i, j int) bool { return a[i].Target < a[j].Target } + func convertServiceNetworks( networks map[string]*composetypes.ServiceNetworkConfig, networkConfigs networkMap, @@ -173,6 +185,8 @@ func convertServiceNetworks( Aliases: append(aliases, name), }) } + + sort.Sort(byNetworkTarget(nets)) return nets, nil } @@ -347,6 +361,12 @@ func convertResources(source composetypes.Resources) (*swarm.ResourceRequirement return resources, nil } +type byPublishedPort []swarm.PortConfig + +func (a byPublishedPort) Len() int { return len(a) } +func (a byPublishedPort) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a byPublishedPort) Less(i, j int) bool { return a[i].PublishedPort < a[j].PublishedPort } + func convertEndpointSpec(source []string) (*swarm.EndpointSpec, error) { portConfigs := []swarm.PortConfig{} ports, portBindings, err := nat.ParsePortSpecs(source) @@ -362,6 +382,7 @@ func convertEndpointSpec(source []string) (*swarm.EndpointSpec, error) { portConfigs = append(portConfigs, portConfig...) } + sort.Sort(byPublishedPort(portConfigs)) return &swarm.EndpointSpec{Ports: portConfigs}, nil } From c62b9c86df50539ecac7f50d0bc58ea7c9ace5b0 Mon Sep 17 00:00:00 2001 From: Harald Albers Date: Fri, 27 Jan 2017 16:17:02 +0100 Subject: [PATCH 561/978] Add [OPTIONS] to usage of `plugin disable|push` Signed-off-by: Harald Albers Upstream-commit: 0e9401f84b29cde01b24f71158154b0d1e7f688e Component: cli --- components/cli/command/plugin/disable.go | 2 +- components/cli/command/plugin/push.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/command/plugin/disable.go b/components/cli/command/plugin/disable.go index c3d36e20af..07b0ec2288 100644 --- a/components/cli/command/plugin/disable.go +++ b/components/cli/command/plugin/disable.go @@ -14,7 +14,7 @@ func newDisableCommand(dockerCli *command.DockerCli) *cobra.Command { var force bool cmd := &cobra.Command{ - Use: "disable PLUGIN", + Use: "disable [OPTIONS] PLUGIN", Short: "Disable a plugin", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { diff --git a/components/cli/command/plugin/push.go b/components/cli/command/plugin/push.go index c5c906b821..6b826dce68 100644 --- a/components/cli/command/plugin/push.go +++ b/components/cli/command/plugin/push.go @@ -16,7 +16,7 @@ import ( func newPushCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := &cobra.Command{ - Use: "push PLUGIN[:TAG]", + Use: "push [OPTIONS] PLUGIN[:TAG]", Short: "Push a plugin to a registry", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { From e34b24e14372c46ac8d4645768f513e6bbc47b0e Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 23 Nov 2016 05:27:09 -0800 Subject: [PATCH 562/978] 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 Upstream-commit: e38bc0d03e9447733287b839139b3b883c8dafdc Component: cli --- components/cli/plugin_list_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/components/cli/plugin_list_test.go b/components/cli/plugin_list_test.go index 6a0e9844fc..6887079b42 100644 --- a/components/cli/plugin_list_test.go +++ b/components/cli/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 25a9f2a83bfd5029ba5d6190e0e36dc71c6ed0a7 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 7 Dec 2016 11:06:07 -0800 Subject: [PATCH 563/978] Move secret name or ID prefix resolving from client to daemon This fix is a follow up for comment: https://github.com/docker/docker/pull/28896#issuecomment-265392703 Currently secret name or ID prefix resolving is done at the client side, which means different behavior of API and CMD. This fix moves the resolving from client to daemon, with exactly the same rule: - Full ID - Full Name - Partial ID (prefix) All existing tests should pass. This fix is related to #288896, #28884 and may be related to #29125. Signed-off-by: Yong Tang Upstream-commit: 383ed6f121abd078d4e3c8677f7cb1e54893505e Component: cli --- components/cli/command/secret/inspect.go | 6 +----- components/cli/command/secret/remove.go | 11 +++-------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/components/cli/command/secret/inspect.go b/components/cli/command/secret/inspect.go index 0a8bd4a23f..fb694c5fbe 100644 --- a/components/cli/command/secret/inspect.go +++ b/components/cli/command/secret/inspect.go @@ -33,13 +33,9 @@ func runSecretInspect(dockerCli *command.DockerCli, opts inspectOptions) error { client := dockerCli.Client() ctx := context.Background() - ids, err := getCliRequestedSecretIDs(ctx, client, opts.names) - if err != nil { - return err - } getRef := func(id string) (interface{}, []byte, error) { return client.SecretInspectWithRaw(ctx, id) } - return inspect.Inspect(dockerCli.Out(), ids, opts.format, getRef) + return inspect.Inspect(dockerCli.Out(), opts.names, opts.format, getRef) } diff --git a/components/cli/command/secret/remove.go b/components/cli/command/secret/remove.go index f45a619f6a..91ca4388f0 100644 --- a/components/cli/command/secret/remove.go +++ b/components/cli/command/secret/remove.go @@ -33,20 +33,15 @@ func runSecretRemove(dockerCli *command.DockerCli, opts removeOptions) error { client := dockerCli.Client() ctx := context.Background() - ids, err := getCliRequestedSecretIDs(ctx, client, opts.names) - if err != nil { - return err - } - var errs []string - for _, id := range ids { - if err := client.SecretRemove(ctx, id); err != nil { + for _, name := range opts.names { + if err := client.SecretRemove(ctx, name); err != nil { errs = append(errs, err.Error()) continue } - fmt.Fprintln(dockerCli.Out(), id) + fmt.Fprintln(dockerCli.Out(), name) } if len(errs) > 0 { From e94be9a069d13a2486e844deae2204b93c7891de Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 25 Dec 2016 01:11:12 -0800 Subject: [PATCH 564/978] Add daemon option --default-shm-size This fix fixes issue raised in 29492 where it was not possible to specify a default `--default-shm-size` in daemon configuration for each `docker run``. The flag `--default-shm-size` which is reloadable, has been added to the daemon configuation. Related docs has been updated. This fix fixes 29492. Signed-off-by: Yong Tang Upstream-commit: ab794c55793096c9ef7330fb2203bf2808c419fa Component: cli --- components/cli/command/service/opts.go | 25 ++------------------- components/cli/command/service/opts_test.go | 4 ++-- 2 files changed, 4 insertions(+), 25 deletions(-) diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index b794b07a30..742c02eee8 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -11,7 +11,6 @@ import ( "github.com/docker/docker/opts" runconfigopts "github.com/docker/docker/runconfig/opts" "github.com/docker/go-connections/nat" - units "github.com/docker/go-units" "github.com/spf13/cobra" ) @@ -19,26 +18,6 @@ type int64Value interface { Value() int64 } -type memBytes int64 - -func (m *memBytes) String() string { - return units.BytesSize(float64(m.Value())) -} - -func (m *memBytes) Set(value string) error { - val, err := units.RAMInBytes(value) - *m = memBytes(val) - return err -} - -func (m *memBytes) Type() string { - return "bytes" -} - -func (m *memBytes) Value() int64 { - return int64(*m) -} - // PositiveDurationOpt is an option type for time.Duration that uses a pointer. // It bahave similarly to DurationOpt but only allows positive duration values. type PositiveDurationOpt struct { @@ -149,9 +128,9 @@ type updateOptions struct { type resourceOptions struct { limitCPU opts.NanoCPUs - limitMemBytes memBytes + limitMemBytes opts.MemBytes resCPU opts.NanoCPUs - resMemBytes memBytes + resMemBytes opts.MemBytes } func (r *resourceOptions) ToResourceRequirements() *swarm.ResourceRequirements { diff --git a/components/cli/command/service/opts_test.go b/components/cli/command/service/opts_test.go index 78b956ad67..4031d6f251 100644 --- a/components/cli/command/service/opts_test.go +++ b/components/cli/command/service/opts_test.go @@ -11,12 +11,12 @@ import ( ) func TestMemBytesString(t *testing.T) { - var mem memBytes = 1048576 + var mem opts.MemBytes = 1048576 assert.Equal(t, mem.String(), "1 MiB") } func TestMemBytesSetAndValue(t *testing.T) { - var mem memBytes + var mem opts.MemBytes assert.NilError(t, mem.Set("5kb")) assert.Equal(t, mem.Value(), int64(5120)) } From 6300064c81eff0bba6700bc09e22770cd1aa1a3c Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 28 Dec 2016 14:44:07 -0800 Subject: [PATCH 565/978] Update opts.MemBytes to disable default, and move `docker run/create/build` to use opts.MemBytes This fix made several updates: 1. Update opts.MemBytes so that default value will not show up. The reason is that in case a default value is decided by daemon, instead of client, we actually want to not show default value. 2. Move `docker run/create/build` to use opts.MemBytes for `--shm-size` This is to bring consistency between daemon and docker run 3. docs updates. Signed-off-by: Yong Tang Upstream-commit: f75ecc5ad224548e3e4105efb2d54dbdfe1fdcb7 Component: cli --- components/cli/command/container/opts.go | 14 +++----------- components/cli/command/container/opts_test.go | 5 +++-- components/cli/command/image/build.go | 14 +++----------- 3 files changed, 9 insertions(+), 24 deletions(-) diff --git a/components/cli/command/container/opts.go b/components/cli/command/container/opts.go index 55cc3c3b29..4f70c7a921 100644 --- a/components/cli/command/container/opts.go +++ b/components/cli/command/container/opts.go @@ -100,7 +100,7 @@ type containerOptions struct { stopSignal string stopTimeout int isolation string - shmSize string + shmSize opts.MemBytes noHealthcheck bool healthCmd string healthInterval time.Duration @@ -259,7 +259,7 @@ func addFlags(flags *pflag.FlagSet) *containerOptions { flags.StringVar(&copts.ipcMode, "ipc", "", "IPC namespace to use") flags.StringVar(&copts.isolation, "isolation", "", "Container isolation technology") flags.StringVar(&copts.pidMode, "pid", "", "PID namespace to use") - flags.StringVar(&copts.shmSize, "shm-size", "", "Size of /dev/shm, default value is 64MB") + flags.Var(&copts.shmSize, "shm-size", "Size of /dev/shm") flags.StringVar(&copts.utsMode, "uts", "", "UTS namespace to use") flags.StringVar(&copts.runtime, "runtime", "", "Runtime to use for this container") @@ -336,14 +336,6 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*container.Config, *c return nil, nil, nil, fmt.Errorf("invalid value: %d. Valid memory swappiness range is 0-100", swappiness) } - var shmSize int64 - if copts.shmSize != "" { - shmSize, err = units.RAMInBytes(copts.shmSize) - if err != nil { - return nil, nil, nil, err - } - } - // TODO FIXME units.RAMInBytes should have a uint64 version var maxIOBandwidth int64 if copts.ioMaxBandwidth != "" { @@ -615,7 +607,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*container.Config, *c LogConfig: container.LogConfig{Type: copts.loggingDriver, Config: loggingOpts}, VolumeDriver: copts.volumeDriver, Isolation: container.Isolation(copts.isolation), - ShmSize: shmSize, + ShmSize: copts.shmSize.Value(), Resources: resources, Tmpfs: tmpfs, Sysctls: copts.sysctls.GetAll(), diff --git a/components/cli/command/container/opts_test.go b/components/cli/command/container/opts_test.go index ce3bb21b41..d0655069e9 100644 --- a/components/cli/command/container/opts_test.go +++ b/components/cli/command/container/opts_test.go @@ -411,8 +411,9 @@ func TestParseModes(t *testing.T) { t.Fatalf("Expected a valid UTSMode, got %v", hostconfig.UTSMode) } // shm-size ko - if _, _, _, err = parseRun([]string{"--shm-size=a128m", "img", "cmd"}); err == nil || err.Error() != "invalid size: 'a128m'" { - t.Fatalf("Expected an error with message 'invalid size: a128m', got %v", err) + expectedErr := `invalid argument "a128m" for --shm-size=a128m: invalid size: 'a128m'` + if _, _, _, err = parseRun([]string{"--shm-size=a128m", "img", "cmd"}); err == nil || err.Error() != expectedErr { + t.Fatalf("Expected an error with message '%v', got %v", expectedErr, err) } // shm-size ok _, hostconfig, _, err = parseRun([]string{"--shm-size=128m", "img", "cmd"}) diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go index 67753bea14..09625d5b2c 100644 --- a/components/cli/command/image/build.go +++ b/components/cli/command/image/build.go @@ -41,7 +41,7 @@ type buildOptions struct { ulimits *opts.UlimitOpt memory string memorySwap string - shmSize string + shmSize opts.MemBytes cpuShares int64 cpuPeriod int64 cpuQuota int64 @@ -89,7 +89,7 @@ func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command { flags.StringVarP(&options.dockerfileName, "file", "f", "", "Name of the Dockerfile (Default is 'PATH/Dockerfile')") flags.StringVarP(&options.memory, "memory", "m", "", "Memory limit") flags.StringVar(&options.memorySwap, "memory-swap", "", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap") - flags.StringVar(&options.shmSize, "shm-size", "", "Size of /dev/shm, default value is 64MB") + flags.Var(&options.shmSize, "shm-size", "Size of /dev/shm") flags.Int64VarP(&options.cpuShares, "cpu-shares", "c", 0, "CPU shares (relative weight)") flags.Int64Var(&options.cpuPeriod, "cpu-period", 0, "Limit the CPU CFS (Completely Fair Scheduler) period") flags.Int64Var(&options.cpuQuota, "cpu-quota", 0, "Limit the CPU CFS (Completely Fair Scheduler) quota") @@ -271,14 +271,6 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { } } - var shmSize int64 - if options.shmSize != "" { - shmSize, err = units.RAMInBytes(options.shmSize) - if err != nil { - return err - } - } - authConfigs, _ := dockerCli.GetAllCredentials() buildOptions := types.ImageBuildOptions{ Memory: memory, @@ -297,7 +289,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { CPUPeriod: options.cpuPeriod, CgroupParent: options.cgroupParent, Dockerfile: relDockerfile, - ShmSize: shmSize, + ShmSize: options.shmSize.Value(), Ulimits: options.ulimits.GetList(), BuildArgs: runconfigopts.ConvertKVStringsToMapWithNil(options.buildArgs.GetAll()), AuthConfigs: authConfigs, From d1dc107dc15c9d026a9b247fc099f9cab5239d76 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 18 Jan 2017 15:27:02 -0500 Subject: [PATCH 566/978] Remove the old loading system from compose config loading The original Compose config loading used the `compose` tag, which was replaced by mapstructure. Some fields were left on the old tag. This commit removes the old tag and uses types and mapstructure. Signed-off-by: Daniel Nephin Upstream-commit: 1eefdba226988149f3191ad071a10aa62c1d3fbf Component: cli --- components/cli/compose/loader/loader.go | 175 ++++++------------- components/cli/compose/loader/loader_test.go | 13 +- components/cli/compose/types/types.go | 66 ++++--- 3 files changed, 106 insertions(+), 148 deletions(-) diff --git a/components/cli/compose/loader/loader.go b/components/cli/compose/loader/loader.go index 39f69a03ff..2c92666c51 100644 --- a/components/cli/compose/loader/loader.go +++ b/components/cli/compose/loader/loader.go @@ -225,18 +225,28 @@ func transformHook( switch target { case reflect.TypeOf(types.External{}): return transformExternal(data) - case reflect.TypeOf(make(map[string]string, 0)): - return transformMapStringString(source, target, data) + case reflect.TypeOf(types.HealthCheckTest{}): + return transformHealthCheckTest(data) + case reflect.TypeOf(types.ShellCommand{}): + return transformShellCommand(data) + case reflect.TypeOf(types.StringList{}): + return transformStringList(data) + case reflect.TypeOf(map[string]string{}): + return transformMapStringString(data) case reflect.TypeOf(types.UlimitsConfig{}): return transformUlimits(data) case reflect.TypeOf(types.UnitBytes(0)): - return loadSize(data) + return transformSize(data) case reflect.TypeOf(types.ServiceSecretConfig{}): return transformServiceSecret(data) - } - switch target.Kind() { - case reflect.Struct: - return transformStruct(source, target, data) + case reflect.TypeOf(types.StringOrNumberList{}): + return transformStringOrNumberList(data) + case reflect.TypeOf(map[string]*types.ServiceNetworkConfig{}): + return transformServiceNetworkMap(data) + case reflect.TypeOf(types.MappingWithEquals{}): + return transformMappingOrList(data, "="), nil + case reflect.TypeOf(types.MappingWithColon{}): + return transformMappingOrList(data, ":"), nil } return data, nil } @@ -249,13 +259,7 @@ func convertToStringKeysRecursive(value interface{}, keyPrefix string) (interfac for key, entry := range mapping { str, ok := key.(string) if !ok { - var location string - if keyPrefix == "" { - location = "at top level" - } else { - location = fmt.Sprintf("in %s", keyPrefix) - } - return nil, fmt.Errorf("Non-string key %s: %#v", location, key) + return nil, formatInvalidKeyError(keyPrefix, key) } var newKeyPrefix string if keyPrefix == "" { @@ -286,6 +290,16 @@ func convertToStringKeysRecursive(value interface{}, keyPrefix string) (interfac return value, nil } +func formatInvalidKeyError(keyPrefix string, key interface{}) error { + var location string + if keyPrefix == "" { + location = "at top level" + } else { + location = fmt.Sprintf("in %s", keyPrefix) + } + return fmt.Errorf("Non-string key %s: %#v", location, key) +} + func loadServices(servicesDict types.Dict, workingDir string) ([]types.ServiceConfig, error) { var services []types.ServiceConfig @@ -307,7 +321,7 @@ func loadService(name string, serviceDict types.Dict, workingDir string) (*types } serviceConfig.Name = name - if err := resolveEnvironment(serviceConfig, serviceDict, workingDir); err != nil { + if err := resolveEnvironment(serviceConfig, workingDir); err != nil { return nil, err } @@ -318,15 +332,13 @@ func loadService(name string, serviceDict types.Dict, workingDir string) (*types return serviceConfig, nil } -func resolveEnvironment(serviceConfig *types.ServiceConfig, serviceDict types.Dict, workingDir string) error { +func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string) error { environment := make(map[string]string) - if envFileVal, ok := serviceDict["env_file"]; ok { - envFiles := loadStringOrListOfStrings(envFileVal) - + if len(serviceConfig.EnvFile) > 0 { var envVars []string - for _, file := range envFiles { + for _, file := range serviceConfig.EnvFile { filePath := absPath(workingDir, file) fileVars, err := opts.ParseEnvFile(filePath) if err != nil { @@ -419,7 +431,6 @@ func loadVolumes(source types.Dict) (map[string]types.VolumeConfig, error) { return volumes, nil } -// TODO: remove duplicate with networks/volumes func loadSecrets(source types.Dict, workingDir string) (map[string]types.SecretConfig, error) { secrets := make(map[string]types.SecretConfig) if err := transform(source, &secrets); err != nil { @@ -444,46 +455,7 @@ func absPath(workingDir string, filepath string) string { return path.Join(workingDir, filepath) } -func transformStruct( - source reflect.Type, - target reflect.Type, - data interface{}, -) (interface{}, error) { - structValue, ok := data.(map[string]interface{}) - if !ok { - // FIXME: this is necessary because of convertToStringKeysRecursive - structValue, ok = data.(types.Dict) - if !ok { - panic(fmt.Sprintf( - "transformStruct called with non-map type: %T, %s", data, data)) - } - } - - var err error - for i := 0; i < target.NumField(); i++ { - field := target.Field(i) - fieldTag := field.Tag.Get("compose") - - yamlName := toYAMLName(field.Name) - value, ok := structValue[yamlName] - if !ok { - continue - } - - structValue[yamlName], err = convertField( - fieldTag, reflect.TypeOf(value), field.Type, value) - if err != nil { - return nil, fmt.Errorf("field %s: %s", yamlName, err.Error()) - } - } - return structValue, nil -} - -func transformMapStringString( - source reflect.Type, - target reflect.Type, - data interface{}, -) (interface{}, error) { +func transformMapStringString(data interface{}) (interface{}, error) { switch value := data.(type) { case map[string]interface{}: return toMapStringString(value), nil @@ -496,37 +468,6 @@ func transformMapStringString( } } -func convertField( - fieldTag string, - source reflect.Type, - target reflect.Type, - data interface{}, -) (interface{}, error) { - switch fieldTag { - case "": - return data, nil - case "healthcheck": - return loadHealthcheck(data) - case "list_or_dict_equals": - return loadMappingOrList(data, "="), nil - case "list_or_dict_colon": - return loadMappingOrList(data, ":"), nil - case "list_or_struct_map": - return loadListOrStructMap(data, target) - case "string_or_list": - return loadStringOrListOfStrings(data), nil - case "list_of_strings_or_numbers": - return loadListOfStringsOrNumbers(data), nil - case "shell_command": - return loadShellCommand(data) - case "size": - return loadSize(data) - case "-": - return nil, nil - } - return data, nil -} - func transformExternal(data interface{}) (interface{}, error) { switch value := data.(type) { case bool: @@ -551,18 +492,9 @@ func transformServiceSecret(data interface{}) (interface{}, error) { default: return data, fmt.Errorf("invalid type %T for external", value) } - } -func toYAMLName(name string) string { - nameParts := fieldNameRegexp.FindAllString(name, -1) - for i, p := range nameParts { - nameParts[i] = strings.ToLower(p) - } - return strings.Join(nameParts, "_") -} - -func loadListOrStructMap(value interface{}, target reflect.Type) (interface{}, error) { +func transformServiceNetworkMap(value interface{}) (interface{}, error) { if list, ok := value.([]interface{}); ok { mapValue := map[interface{}]interface{}{} for _, name := range list { @@ -570,31 +502,30 @@ func loadListOrStructMap(value interface{}, target reflect.Type) (interface{}, e } return mapValue, nil } - return value, nil } -func loadListOfStringsOrNumbers(value interface{}) []string { +func transformStringOrNumberList(value interface{}) (interface{}, error) { list := value.([]interface{}) result := make([]string, len(list)) for i, item := range list { result[i] = fmt.Sprint(item) } - return result + return result, nil } -func loadStringOrListOfStrings(value interface{}) []string { - if list, ok := value.([]interface{}); ok { - result := make([]string, len(list)) - for i, item := range list { - result[i] = fmt.Sprint(item) - } - return result +func transformStringList(data interface{}) (interface{}, error) { + switch value := data.(type) { + case string: + return []string{value}, nil + case []interface{}: + return value, nil + default: + return data, fmt.Errorf("invalid type %T for string list", value) } - return []string{value.(string)} } -func loadMappingOrList(mappingOrList interface{}, sep string) map[string]string { +func transformMappingOrList(mappingOrList interface{}, sep string) map[string]string { if mapping, ok := mappingOrList.(types.Dict); ok { return toMapStringString(mapping) } @@ -613,21 +544,25 @@ func loadMappingOrList(mappingOrList interface{}, sep string) map[string]string panic(fmt.Errorf("expected a map or a slice, got: %#v", mappingOrList)) } -func loadShellCommand(value interface{}) (interface{}, error) { +func transformShellCommand(value interface{}) (interface{}, error) { if str, ok := value.(string); ok { return shellwords.Parse(str) } return value, nil } -func loadHealthcheck(value interface{}) (interface{}, error) { - if str, ok := value.(string); ok { - return append([]string{"CMD-SHELL"}, str), nil +func transformHealthCheckTest(data interface{}) (interface{}, error) { + switch value := data.(type) { + case string: + return append([]string{"CMD-SHELL"}, value), nil + case []interface{}: + return value, nil + default: + return value, fmt.Errorf("invalid type %T for healthcheck.test", value) } - return value, nil } -func loadSize(value interface{}) (int64, error) { +func transformSize(value interface{}) (int64, error) { switch value := value.(type) { case int: return int64(value), nil diff --git a/components/cli/compose/loader/loader_test.go b/components/cli/compose/loader/loader_test.go index f7fee89ede..bb5d3ecc06 100644 --- a/components/cli/compose/loader/loader_test.go +++ b/components/cli/compose/loader/loader_test.go @@ -394,7 +394,7 @@ services: `) assert.NoError(t, err) - expected := map[string]string{ + expected := types.MappingWithEquals{ "FOO": "1", "BAR": "2", "BAZ": "2.5", @@ -456,7 +456,7 @@ volumes: home := os.Getenv("HOME") - expectedLabels := map[string]string{ + expectedLabels := types.MappingWithEquals{ "home1": home, "home2": home, "nonexistent": "", @@ -621,6 +621,10 @@ func TestFullExample(t *testing.T) { "BAR": "2", "BAZ": "3", }, + EnvFile: []string{ + "./example1.env", + "./example2.env", + }, Expose: []string{"3000", "8000"}, ExternalLinks: []string{ "redis_1", @@ -632,10 +636,7 @@ func TestFullExample(t *testing.T) { "somehost": "162.242.195.82", }, HealthCheck: &types.HealthCheckConfig{ - Test: []string{ - "CMD-SHELL", - "echo \"hello world\"", - }, + Test: types.HealthCheckTest([]string{"CMD-SHELL", "echo \"hello world\""}), Interval: "10s", Timeout: "1s", Retries: uint64Ptr(5), diff --git a/components/cli/compose/types/types.go b/components/cli/compose/types/types.go index d70d01ed29..3b9a2b2a08 100644 --- a/components/cli/compose/types/types.go +++ b/components/cli/compose/types/types.go @@ -81,31 +81,32 @@ type ServiceConfig struct { CapAdd []string `mapstructure:"cap_add"` CapDrop []string `mapstructure:"cap_drop"` CgroupParent string `mapstructure:"cgroup_parent"` - Command []string `compose:"shell_command"` + Command ShellCommand ContainerName string `mapstructure:"container_name"` DependsOn []string `mapstructure:"depends_on"` Deploy DeployConfig Devices []string - DNS []string `compose:"string_or_list"` - DNSSearch []string `mapstructure:"dns_search" compose:"string_or_list"` - DomainName string `mapstructure:"domainname"` - Entrypoint []string `compose:"shell_command"` - Environment map[string]string `compose:"list_or_dict_equals"` - Expose []string `compose:"list_of_strings_or_numbers"` - ExternalLinks []string `mapstructure:"external_links"` - ExtraHosts map[string]string `mapstructure:"extra_hosts" compose:"list_or_dict_colon"` + DNS StringList + DNSSearch StringList `mapstructure:"dns_search"` + DomainName string `mapstructure:"domainname"` + Entrypoint ShellCommand + Environment MappingWithEquals + EnvFile StringList `mapstructure:"env_file"` + Expose StringOrNumberList + ExternalLinks []string `mapstructure:"external_links"` + ExtraHosts MappingWithColon `mapstructure:"extra_hosts"` Hostname string HealthCheck *HealthCheckConfig Image string Ipc string - Labels map[string]string `compose:"list_or_dict_equals"` + Labels MappingWithEquals Links []string Logging *LoggingConfig - MacAddress string `mapstructure:"mac_address"` - NetworkMode string `mapstructure:"network_mode"` - Networks map[string]*ServiceNetworkConfig `compose:"list_or_struct_map"` + MacAddress string `mapstructure:"mac_address"` + NetworkMode string `mapstructure:"network_mode"` + Networks map[string]*ServiceNetworkConfig Pid string - Ports []string `compose:"list_of_strings_or_numbers"` + Ports StringOrNumberList Privileged bool ReadOnly bool `mapstructure:"read_only"` Restart string @@ -114,14 +115,32 @@ type ServiceConfig struct { StdinOpen bool `mapstructure:"stdin_open"` StopGracePeriod *time.Duration `mapstructure:"stop_grace_period"` StopSignal string `mapstructure:"stop_signal"` - Tmpfs []string `compose:"string_or_list"` - Tty bool `mapstructure:"tty"` + Tmpfs StringList + Tty bool `mapstructure:"tty"` Ulimits map[string]*UlimitsConfig User string Volumes []string WorkingDir string `mapstructure:"working_dir"` } +// ShellCommand is a string or list of string args +type ShellCommand []string + +// StringList is a type for fields that can be a string or list of strings +type StringList []string + +// StringOrNumberList is a type for fields that can be a list of strings or +// numbers +type StringOrNumberList []string + +// MappingWithEquals is a mapping type that can be converted from a list of +// key=value strings +type MappingWithEquals map[string]string + +// MappingWithColon is a mapping type that can be converted from alist of +// 'key: value' strings +type MappingWithColon map[string]string + // LoggingConfig the logging configuration for a service type LoggingConfig struct { Driver string @@ -132,8 +151,8 @@ type LoggingConfig struct { type DeployConfig struct { Mode string Replicas *uint64 - Labels map[string]string `compose:"list_or_dict_equals"` - UpdateConfig *UpdateConfig `mapstructure:"update_config"` + Labels MappingWithEquals + UpdateConfig *UpdateConfig `mapstructure:"update_config"` Resources Resources RestartPolicy *RestartPolicy `mapstructure:"restart_policy"` Placement Placement @@ -141,13 +160,16 @@ type DeployConfig struct { // HealthCheckConfig the healthcheck configuration for a service type HealthCheckConfig struct { - Test []string `compose:"healthcheck"` + Test HealthCheckTest Timeout string Interval string Retries *uint64 Disable bool } +// HealthCheckTest is the command run to test the health of a service +type HealthCheckTest []string + // UpdateConfig the service update configuration type UpdateConfig struct { Parallelism *uint64 @@ -216,7 +238,7 @@ type NetworkConfig struct { Ipam IPAMConfig External External Internal bool - Labels map[string]string `compose:"list_or_dict_equals"` + Labels MappingWithEquals } // IPAMConfig for a network @@ -235,7 +257,7 @@ type VolumeConfig struct { Driver string DriverOpts map[string]string `mapstructure:"driver_opts"` External External - Labels map[string]string `compose:"list_or_dict_equals"` + Labels MappingWithEquals } // External identifies a Volume or Network as a reference to a resource that is @@ -249,5 +271,5 @@ type External struct { type SecretConfig struct { File string External External - Labels map[string]string `compose:"list_or_dict_equals"` + Labels MappingWithEquals } From 43d29b15902ab57b43a3d124f93c2d7408fc45f8 Mon Sep 17 00:00:00 2001 From: allencloud Date: Mon, 16 Jan 2017 16:58:23 +0800 Subject: [PATCH 567/978] add endpoint mode a default value Signed-off-by: allencloud Upstream-commit: 67db25f42e61c72d24d9159a3eaf6d598a5de129 Component: cli --- components/cli/command/service/opts.go | 54 +++++++++++++++----------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index b794b07a30..292aa66d5e 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -346,6 +346,25 @@ func newServiceOptions() *serviceOptions { } } +func (opts *serviceOptions) ToServiceMode() (swarm.ServiceMode, error) { + serviceMode := swarm.ServiceMode{} + switch opts.mode { + case "global": + if opts.replicas.Value() != nil { + return serviceMode, fmt.Errorf("replicas can only be used with replicated mode") + } + + serviceMode.Global = &swarm.GlobalService{} + case "replicated": + serviceMode.Replicated = &swarm.ReplicatedService{ + Replicas: opts.replicas.Value(), + } + default: + return serviceMode, fmt.Errorf("Unknown mode: %s, only replicated and global supported", opts.mode) + } + return serviceMode, nil +} + func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { var service swarm.ServiceSpec @@ -368,6 +387,16 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { currentEnv = append(currentEnv, env) } + healthConfig, err := opts.healthcheck.toHealthConfig() + if err != nil { + return service, err + } + + serviceMode, err := opts.ToServiceMode() + if err != nil { + return service, err + } + service = swarm.ServiceSpec{ Annotations: swarm.Annotations{ Name: opts.name, @@ -393,6 +422,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { Hosts: convertExtraHostsToSwarmHosts(opts.hosts.GetAll()), StopGracePeriod: opts.stopGrace.Value(), Secrets: nil, + Healthcheck: healthConfig, }, Networks: convertNetworks(opts.networks.GetAll()), Resources: opts.resources.ToResourceRequirements(), @@ -403,7 +433,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { LogDriver: opts.logDriver.toLogDriver(), }, Networks: convertNetworks(opts.networks.GetAll()), - Mode: swarm.ServiceMode{}, + Mode: serviceMode, UpdateConfig: &swarm.UpdateConfig{ Parallelism: opts.update.parallelism, Delay: opts.update.delay, @@ -414,26 +444,6 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { EndpointSpec: opts.endpoint.ToEndpointSpec(), } - healthConfig, err := opts.healthcheck.toHealthConfig() - if err != nil { - return service, err - } - service.TaskTemplate.ContainerSpec.Healthcheck = healthConfig - - switch opts.mode { - case "global": - if opts.replicas.Value() != nil { - return service, fmt.Errorf("replicas can only be used with replicated mode") - } - - service.Mode.Global = &swarm.GlobalService{} - case "replicated": - service.Mode.Replicated = &swarm.ReplicatedService{ - Replicas: opts.replicas.Value(), - } - default: - return service, fmt.Errorf("Unknown mode: %s", opts.mode) - } return service, nil } @@ -465,7 +475,7 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { flags.StringVar(&opts.update.onFailure, flagUpdateFailureAction, "pause", "Action on update failure (pause|continue)") flags.Var(&opts.update.maxFailureRatio, flagUpdateMaxFailureRatio, "Failure rate to tolerate during an update") - flags.StringVar(&opts.endpoint.mode, flagEndpointMode, "", "Endpoint mode (vip or dnsrr)") + flags.StringVar(&opts.endpoint.mode, flagEndpointMode, "vip", "Endpoint mode (vip or dnsrr)") flags.BoolVar(&opts.registryAuth, flagRegistryAuth, false, "Send registry authentication details to swarm agents") From c3e631a48593ba810825153ca2db435d02d3c994 Mon Sep 17 00:00:00 2001 From: allencloud Date: Sun, 29 Jan 2017 01:04:10 +0800 Subject: [PATCH 568/978] remove cli/command/secrets/utils.go Signed-off-by: allencloud Upstream-commit: ca1e5ffeea109b96d0cdae9b5a645ea6b3cd5138 Component: cli --- components/cli/command/secret/utils.go | 76 -------------------------- components/cli/command/stack/deploy.go | 30 +++++----- 2 files changed, 13 insertions(+), 93 deletions(-) delete mode 100644 components/cli/command/secret/utils.go diff --git a/components/cli/command/secret/utils.go b/components/cli/command/secret/utils.go deleted file mode 100644 index 11d31ffd16..0000000000 --- a/components/cli/command/secret/utils.go +++ /dev/null @@ -1,76 +0,0 @@ -package secret - -import ( - "fmt" - "strings" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/client" - "golang.org/x/net/context" -) - -// GetSecretsByNameOrIDPrefixes returns secrets given a list of ids or names -func GetSecretsByNameOrIDPrefixes(ctx context.Context, client client.APIClient, terms []string) ([]swarm.Secret, error) { - args := filters.NewArgs() - for _, n := range terms { - args.Add("names", n) - args.Add("id", n) - } - - return client.SecretList(ctx, types.SecretListOptions{ - Filters: args, - }) -} - -func getCliRequestedSecretIDs(ctx context.Context, client client.APIClient, terms []string) ([]string, error) { - secrets, err := GetSecretsByNameOrIDPrefixes(ctx, client, terms) - if err != nil { - return nil, err - } - - if len(secrets) > 0 { - found := make(map[string]struct{}) - next: - for _, term := range terms { - // attempt to lookup secret by full ID - for _, s := range secrets { - if s.ID == term { - found[s.ID] = struct{}{} - continue next - } - } - // attempt to lookup secret by full name - for _, s := range secrets { - if s.Spec.Annotations.Name == term { - found[s.ID] = struct{}{} - continue next - } - } - // attempt to lookup secret by partial ID (prefix) - // return error if more than one matches found (ambiguous) - n := 0 - for _, s := range secrets { - if strings.HasPrefix(s.ID, term) { - found[s.ID] = struct{}{} - n++ - } - } - if n > 1 { - return nil, fmt.Errorf("secret %s is ambiguous (%d matches found)", term, n) - } - } - - // We already collected all the IDs found. - // Now we will remove duplicates by converting the map to slice - ids := []string{} - for id := range found { - ids = append(ids, id) - } - - return ids, nil - } - - return terms, nil -} diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index 203ae6d39c..753b1503b1 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -11,10 +11,10 @@ import ( "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" - secretcli "github.com/docker/docker/cli/command/secret" "github.com/docker/docker/cli/compose/convert" "github.com/docker/docker/cli/compose/loader" composetypes "github.com/docker/docker/cli/compose/types" + apiclient "github.com/docker/docker/client" dockerclient "github.com/docker/docker/client" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -229,22 +229,18 @@ func createSecrets( client := dockerCli.Client() for _, secretSpec := range secrets { - // TODO: fix this after https://github.com/docker/docker/pull/29218 - secrets, err := secretcli.GetSecretsByNameOrIDPrefixes(ctx, client, []string{secretSpec.Name}) - switch { - case err != nil: - return err - case len(secrets) > 1: - return errors.Errorf("ambiguous secret name: %s", secretSpec.Name) - case len(secrets) == 0: - fmt.Fprintf(dockerCli.Out(), "Creating secret %s\n", secretSpec.Name) - _, err = client.SecretCreate(ctx, secretSpec) - default: - secret := secrets[0] - // Update secret to ensure that the local data hasn't changed - err = client.SecretUpdate(ctx, secret.ID, secret.Meta.Version, secretSpec) - } - if err != nil { + secret, _, err := client.SecretInspectWithRaw(ctx, secretSpec.Name) + if err == nil { + // secret already exists, then we update that + if err := client.SecretUpdate(ctx, secret.ID, secret.Meta.Version, secretSpec); err != nil { + return err + } + } else if apiclient.IsErrSecretNotFound(err) { + // secret does not exist, then we create a new one. + if _, err := client.SecretCreate(ctx, secretSpec); err != nil { + return err + } + } else { return err } } From 2874513efc72e5530ff9935d6ba27f70605f0387 Mon Sep 17 00:00:00 2001 From: allencloud Date: Tue, 17 Jan 2017 15:55:45 +0800 Subject: [PATCH 569/978] validate healthcheck params in daemon side Signed-off-by: allencloud Upstream-commit: 406c6348b62fd709cedea3d9878fb23db4e6f1ad Component: cli --- components/cli/command/container/opts.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/components/cli/command/container/opts.go b/components/cli/command/container/opts.go index 55cc3c3b29..245d8e856d 100644 --- a/components/cli/command/container/opts.go +++ b/components/cli/command/container/opts.go @@ -511,6 +511,9 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*container.Config, *c if copts.healthTimeout < 0 { return nil, nil, nil, fmt.Errorf("--health-timeout cannot be negative") } + if copts.healthRetries < 0 { + return nil, nil, nil, fmt.Errorf("--health-retries cannot be negative") + } healthConfig = &container.HealthConfig{ Test: probe, From 9bdd0ca52ff5a0a1550d2093c6bf00ad12dec0dd Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sat, 14 Jan 2017 00:12:19 -0800 Subject: [PATCH 570/978] Add `--read-only` for `service create` and `service update` This fix tries to address the issue raised in 29972 where it was not possible to specify `--read-only` for `docker service create` and `docker service update`, in order to have the container's root file system to be read only. This fix adds `--read-only` and update the `ReadonlyRootfs` in `HostConfig` through `service create` and `service update`. Related docs has been updated. Integration test has been added. This fix fixes 29972. Signed-off-by: Yong Tang Upstream-commit: b849aa6b95e14450c90657cfe9668dfd5704a464 Component: cli --- components/cli/command/service/opts.go | 6 +++++ components/cli/command/service/update.go | 8 +++++++ components/cli/command/service/update_test.go | 22 +++++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index 2218890aa3..dcd52ac7a3 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -303,6 +303,7 @@ type serviceOptions struct { user string groups opts.ListOpts tty bool + readOnly bool mounts opts.MountOpt dns opts.ListOpts dnsSearch opts.ListOpts @@ -384,6 +385,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { User: opts.user, Groups: opts.groups.GetAll(), TTY: opts.tty, + ReadOnly: opts.readOnly, Mounts: opts.mounts.Value(), DNSConfig: &swarm.DNSConfig{ Nameservers: opts.dns.GetAll(), @@ -488,6 +490,9 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { flags.BoolVarP(&opts.tty, flagTTY, "t", false, "Allocate a pseudo-TTY") flags.SetAnnotation(flagTTY, "version", []string{"1.25"}) + + flags.BoolVar(&opts.readOnly, flagReadOnly, false, "Mount the container's root filesystem as read only") + flags.SetAnnotation(flagReadOnly, "version", []string{"1.26"}) } const ( @@ -532,6 +537,7 @@ const ( flagPublish = "publish" flagPublishRemove = "publish-rm" flagPublishAdd = "publish-add" + flagReadOnly = "read-only" flagReplicas = "replicas" flagReserveCPU = "reserve-cpu" flagReserveMemory = "reserve-memory" diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index 66002b69e0..57a4577f88 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -341,6 +341,14 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error { cspec.TTY = tty } + if flags.Changed(flagReadOnly) { + readOnly, err := flags.GetBool(flagReadOnly) + if err != nil { + return err + } + cspec.ReadOnly = readOnly + } + return nil } diff --git a/components/cli/command/service/update_test.go b/components/cli/command/service/update_test.go index bb931929c0..992ae9ef3b 100644 --- a/components/cli/command/service/update_test.go +++ b/components/cli/command/service/update_test.go @@ -442,3 +442,25 @@ func TestUpdateSecretUpdateInPlace(t *testing.T) { assert.Equal(t, updatedSecrets[0].SecretName, "foo") assert.Equal(t, updatedSecrets[0].File.Name, "foo2") } + +func TestUpdateReadOnly(t *testing.T) { + spec := &swarm.ServiceSpec{} + cspec := &spec.TaskTemplate.ContainerSpec + + // Update with --read-only=true, changed to true + flags := newUpdateCommand(nil).Flags() + flags.Set("read-only", "true") + updateService(flags, spec) + assert.Equal(t, cspec.ReadOnly, true) + + // Update without --read-only, no change + flags = newUpdateCommand(nil).Flags() + updateService(flags, spec) + assert.Equal(t, cspec.ReadOnly, true) + + // Update with --read-only=false, changed to false + flags = newUpdateCommand(nil).Flags() + flags.Set("read-only", "false") + updateService(flags, spec) + assert.Equal(t, cspec.ReadOnly, false) +} From c340c8f7672b53a510ce1206dafcadecfa31446e Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Thu, 26 Jan 2017 13:08:07 -0800 Subject: [PATCH 571/978] Add `--format` to `docker service ls` This fix tries to improve the display of `docker service ls` and adds `--format` flag to `docker service ls`. In addition to `--format` flag, several other improvement: 1. Updates `docker stacks service`. 2. Adds `servicesFormat` to config file. Related docs has been updated. Signed-off-by: Yong Tang Upstream-commit: 31fb756bb6ac30c5815a5e4410624ba1c6a2244d Component: cli --- components/cli/command/formatter/service.go | 92 +++++++++ .../cli/command/formatter/service_test.go | 177 ++++++++++++++++++ components/cli/command/service/list.go | 98 ++++------ components/cli/command/stack/services.go | 28 ++- components/cli/config/configfile/file.go | 1 + 5 files changed, 327 insertions(+), 69 deletions(-) create mode 100644 components/cli/command/formatter/service_test.go diff --git a/components/cli/command/formatter/service.go b/components/cli/command/formatter/service.go index 8242e1cb9e..9d9241b224 100644 --- a/components/cli/command/formatter/service.go +++ b/components/cli/command/formatter/service.go @@ -5,9 +5,11 @@ import ( "strings" "time" + distreference "github.com/docker/distribution/reference" mounttypes "github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli/command/inspect" + "github.com/docker/docker/pkg/stringid" units "github.com/docker/go-units" ) @@ -327,3 +329,93 @@ func (ctx *serviceInspectContext) EndpointMode() string { func (ctx *serviceInspectContext) Ports() []swarm.PortConfig { return ctx.Service.Endpoint.Ports } + +const ( + defaultServiceTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Mode}}\t{{.Replicas}}\t{{.Image}}" + + serviceIDHeader = "ID" + modeHeader = "MODE" + replicasHeader = "REPLICAS" +) + +// NewServiceListFormat returns a Format for rendering using a service Context +func NewServiceListFormat(source string, quiet bool) Format { + switch source { + case TableFormatKey: + if quiet { + return defaultQuietFormat + } + return defaultServiceTableFormat + case RawFormatKey: + if quiet { + return `id: {{.ID}}` + } + return `id: {{.ID}}\nname: {{.Name}}\nmode: {{.Mode}}\nreplicas: {{.Replicas}}\nimage: {{.Image}}\n` + } + return Format(source) +} + +// ServiceListInfo stores the information about mode and replicas to be used by template +type ServiceListInfo struct { + Mode string + Replicas string +} + +// ServiceListWrite writes the context +func ServiceListWrite(ctx Context, services []swarm.Service, info map[string]ServiceListInfo) error { + render := func(format func(subContext subContext) error) error { + for _, service := range services { + serviceCtx := &serviceContext{service: service, mode: info[service.ID].Mode, replicas: info[service.ID].Replicas} + if err := format(serviceCtx); err != nil { + return err + } + } + return nil + } + return ctx.Write(&serviceContext{}, render) +} + +type serviceContext struct { + HeaderContext + service swarm.Service + mode string + replicas string +} + +func (c *serviceContext) MarshalJSON() ([]byte, error) { + return marshalJSON(c) +} + +func (c *serviceContext) ID() string { + c.AddHeader(serviceIDHeader) + return stringid.TruncateID(c.service.ID) +} + +func (c *serviceContext) Name() string { + c.AddHeader(nameHeader) + return c.service.Spec.Name +} + +func (c *serviceContext) Mode() string { + c.AddHeader(modeHeader) + return c.mode +} + +func (c *serviceContext) Replicas() string { + c.AddHeader(replicasHeader) + return c.replicas +} + +func (c *serviceContext) Image() string { + c.AddHeader(imageHeader) + image := c.service.Spec.TaskTemplate.ContainerSpec.Image + if ref, err := distreference.ParseNamed(image); err == nil { + // update image string for display + namedTagged, ok := ref.(distreference.NamedTagged) + if ok { + image = namedTagged.Name() + ":" + namedTagged.Tag() + } + } + + return image +} diff --git a/components/cli/command/formatter/service_test.go b/components/cli/command/formatter/service_test.go new file mode 100644 index 0000000000..d4474297db --- /dev/null +++ b/components/cli/command/formatter/service_test.go @@ -0,0 +1,177 @@ +package formatter + +import ( + "bytes" + "encoding/json" + "strings" + "testing" + + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestServiceContextWrite(t *testing.T) { + cases := []struct { + context Context + expected string + }{ + // Errors + { + Context{Format: "{{InvalidFunction}}"}, + `Template parsing error: template: :1: function "InvalidFunction" not defined +`, + }, + { + Context{Format: "{{nil}}"}, + `Template parsing error: template: :1:2: executing "" at : nil is not a command +`, + }, + // Table format + { + Context{Format: NewServiceListFormat("table", false)}, + `ID NAME MODE REPLICAS IMAGE +id_baz baz global 2/4 +id_bar bar replicated 2/4 +`, + }, + { + Context{Format: NewServiceListFormat("table", true)}, + `id_baz +id_bar +`, + }, + { + Context{Format: NewServiceListFormat("table {{.Name}}", false)}, + `NAME +baz +bar +`, + }, + { + Context{Format: NewServiceListFormat("table {{.Name}}", true)}, + `NAME +baz +bar +`, + }, + // Raw Format + { + Context{Format: NewServiceListFormat("raw", false)}, + `id: id_baz +name: baz +mode: global +replicas: 2/4 +image: + +id: id_bar +name: bar +mode: replicated +replicas: 2/4 +image: + +`, + }, + { + Context{Format: NewServiceListFormat("raw", true)}, + `id: id_baz +id: id_bar +`, + }, + // Custom Format + { + Context{Format: NewServiceListFormat("{{.Name}}", false)}, + `baz +bar +`, + }, + } + + for _, testcase := range cases { + services := []swarm.Service{ + {ID: "id_baz", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "baz"}}}, + {ID: "id_bar", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "bar"}}}, + } + info := map[string]ServiceListInfo{ + "id_baz": { + Mode: "global", + Replicas: "2/4", + }, + "id_bar": { + Mode: "replicated", + Replicas: "2/4", + }, + } + out := bytes.NewBufferString("") + testcase.context.Output = out + err := ServiceListWrite(testcase.context, services, info) + if err != nil { + assert.Error(t, err, testcase.expected) + } else { + assert.Equal(t, out.String(), testcase.expected) + } + } +} + +func TestServiceContextWriteJSON(t *testing.T) { + services := []swarm.Service{ + {ID: "id_baz", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "baz"}}}, + {ID: "id_bar", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "bar"}}}, + } + info := map[string]ServiceListInfo{ + "id_baz": { + Mode: "global", + Replicas: "2/4", + }, + "id_bar": { + Mode: "replicated", + Replicas: "2/4", + }, + } + expectedJSONs := []map[string]interface{}{ + {"ID": "id_baz", "Name": "baz", "Mode": "global", "Replicas": "2/4", "Image": ""}, + {"ID": "id_bar", "Name": "bar", "Mode": "replicated", "Replicas": "2/4", "Image": ""}, + } + + out := bytes.NewBufferString("") + err := ServiceListWrite(Context{Format: "{{json .}}", Output: out}, services, info) + if err != nil { + t.Fatal(err) + } + for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { + t.Logf("Output: line %d: %s", i, line) + var m map[string]interface{} + if err := json.Unmarshal([]byte(line), &m); err != nil { + t.Fatal(err) + } + assert.DeepEqual(t, m, expectedJSONs[i]) + } +} +func TestServiceContextWriteJSONField(t *testing.T) { + services := []swarm.Service{ + {ID: "id_baz", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "baz"}}}, + {ID: "id_bar", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "bar"}}}, + } + info := map[string]ServiceListInfo{ + "id_baz": { + Mode: "global", + Replicas: "2/4", + }, + "id_bar": { + Mode: "replicated", + Replicas: "2/4", + }, + } + out := bytes.NewBufferString("") + err := ServiceListWrite(Context{Format: "{{json .Name}}", Output: out}, services, info) + if err != nil { + t.Fatal(err) + } + for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { + t.Logf("Output: line %d: %s", i, line) + var s string + if err := json.Unmarshal([]byte(line), &s); err != nil { + t.Fatal(err) + } + assert.Equal(t, s, services[i].Spec.Name) + } +} diff --git a/components/cli/command/service/list.go b/components/cli/command/service/list.go index 724126079c..ca3e741fab 100644 --- a/components/cli/command/service/list.go +++ b/components/cli/command/service/list.go @@ -2,27 +2,21 @@ package service import ( "fmt" - "io" - "text/tabwriter" - distreference "github.com/docker/distribution/reference" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/formatter" "github.com/docker/docker/opts" - "github.com/docker/docker/pkg/stringid" "github.com/spf13/cobra" "golang.org/x/net/context" ) -const ( - listItemFmt = "%s\t%s\t%s\t%s\t%s\n" -) - type listOptions struct { quiet bool + format string filter opts.FilterOpt } @@ -41,6 +35,7 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs") + flags.StringVar(&opts.format, "format", "", "Pretty-print services using a Go template") flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") return cmd @@ -49,13 +44,13 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command { func runList(dockerCli *command.DockerCli, opts listOptions) error { ctx := context.Background() client := dockerCli.Client() - out := dockerCli.Out() services, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: opts.filter.Value()}) if err != nil { return err } + info := map[string]formatter.ServiceListInfo{} if len(services) > 0 && !opts.quiet { // only non-empty services and not quiet, should we call TaskList and NodeList api taskFilter := filters.NewArgs() @@ -73,20 +68,30 @@ func runList(dockerCli *command.DockerCli, opts listOptions) error { return err } - PrintNotQuiet(out, services, nodes, tasks) - } else if !opts.quiet { - // no services and not quiet, print only one line with columns ID, NAME, MODE, REPLICAS... - PrintNotQuiet(out, services, []swarm.Node{}, []swarm.Task{}) - } else { - PrintQuiet(out, services) + info = GetServicesStatus(services, nodes, tasks) } - return nil + format := opts.format + if len(format) == 0 { + if len(dockerCli.ConfigFile().ServicesFormat) > 0 && !opts.quiet { + format = dockerCli.ConfigFile().ServicesFormat + } else { + format = formatter.TableFormatKey + } + } + + servicesCtx := formatter.Context{ + Output: dockerCli.Out(), + Format: formatter.NewServiceListFormat(format, opts.quiet), + } + return formatter.ServiceListWrite(servicesCtx, services, info) } -// PrintNotQuiet shows service list in a non-quiet way. -// Besides this, command `docker stack services xxx` will call this, too. -func PrintNotQuiet(out io.Writer, services []swarm.Service, nodes []swarm.Node, tasks []swarm.Task) { +// GetServicesStatus returns a map of mode and replicas +func GetServicesStatus(services []swarm.Service, nodes []swarm.Node, tasks []swarm.Task) map[string]formatter.ServiceListInfo { + running := map[string]int{} + tasksNoShutdown := map[string]int{} + activeNodes := make(map[string]struct{}) for _, n := range nodes { if n.Status.State != swarm.NodeStateDown { @@ -94,9 +99,6 @@ func PrintNotQuiet(out io.Writer, services []swarm.Service, nodes []swarm.Node, } } - running := map[string]int{} - tasksNoShutdown := map[string]int{} - for _, task := range tasks { if task.DesiredState != swarm.TaskStateShutdown { tasksNoShutdown[task.ServiceID]++ @@ -107,52 +109,20 @@ func PrintNotQuiet(out io.Writer, services []swarm.Service, nodes []swarm.Node, } } - printTable(out, services, running, tasksNoShutdown) -} - -func printTable(out io.Writer, services []swarm.Service, running, tasksNoShutdown map[string]int) { - writer := tabwriter.NewWriter(out, 0, 4, 2, ' ', 0) - - // Ignore flushing errors - defer writer.Flush() - - fmt.Fprintf(writer, listItemFmt, "ID", "NAME", "MODE", "REPLICAS", "IMAGE") - + info := map[string]formatter.ServiceListInfo{} for _, service := range services { - mode := "" - replicas := "" + info[service.ID] = formatter.ServiceListInfo{} if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil { - mode = "replicated" - replicas = fmt.Sprintf("%d/%d", running[service.ID], *service.Spec.Mode.Replicated.Replicas) + info[service.ID] = formatter.ServiceListInfo{ + Mode: "replicated", + Replicas: fmt.Sprintf("%d/%d", running[service.ID], *service.Spec.Mode.Replicated.Replicas), + } } else if service.Spec.Mode.Global != nil { - mode = "global" - replicas = fmt.Sprintf("%d/%d", running[service.ID], tasksNoShutdown[service.ID]) - } - image := service.Spec.TaskTemplate.ContainerSpec.Image - ref, err := distreference.ParseNamed(image) - if err == nil { - // update image string for display - namedTagged, ok := ref.(distreference.NamedTagged) - if ok { - image = namedTagged.Name() + ":" + namedTagged.Tag() + info[service.ID] = formatter.ServiceListInfo{ + Mode: "global", + Replicas: fmt.Sprintf("%d/%d", running[service.ID], tasksNoShutdown[service.ID]), } } - - fmt.Fprintf( - writer, - listItemFmt, - stringid.TruncateID(service.ID), - service.Spec.Name, - mode, - replicas, - image) - } -} - -// PrintQuiet shows service list in a quiet way. -// Besides this, command `docker stack services xxx` will call this, too. -func PrintQuiet(out io.Writer, services []swarm.Service) { - for _, service := range services { - fmt.Fprintln(out, service.ID) } + return info } diff --git a/components/cli/command/stack/services.go b/components/cli/command/stack/services.go index a46652df7c..78ddd399ce 100644 --- a/components/cli/command/stack/services.go +++ b/components/cli/command/stack/services.go @@ -9,6 +9,7 @@ import ( "github.com/docker/docker/api/types/filters" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/formatter" "github.com/docker/docker/cli/command/service" "github.com/docker/docker/opts" "github.com/spf13/cobra" @@ -16,6 +17,7 @@ import ( type servicesOptions struct { quiet bool + format string filter opts.FilterOpt namespace string } @@ -34,6 +36,7 @@ func newServicesCommand(dockerCli *command.DockerCli) *cobra.Command { } flags := cmd.Flags() flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs") + flags.StringVar(&opts.format, "format", "", "Pretty-print services using a Go template") flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") return cmd @@ -57,9 +60,8 @@ func runServices(dockerCli *command.DockerCli, opts servicesOptions) error { return nil } - if opts.quiet { - service.PrintQuiet(out, services) - } else { + info := map[string]formatter.ServiceListInfo{} + if !opts.quiet { taskFilter := filters.NewArgs() for _, service := range services { taskFilter.Add("service", service.ID) @@ -69,11 +71,27 @@ func runServices(dockerCli *command.DockerCli, opts servicesOptions) error { if err != nil { return err } + nodes, err := client.NodeList(ctx, types.NodeListOptions{}) if err != nil { return err } - service.PrintNotQuiet(out, services, nodes, tasks) + + info = service.GetServicesStatus(services, nodes, tasks) } - return nil + + format := opts.format + if len(format) == 0 { + if len(dockerCli.ConfigFile().ServicesFormat) > 0 && !opts.quiet { + format = dockerCli.ConfigFile().ServicesFormat + } else { + format = formatter.TableFormatKey + } + } + + servicesCtx := formatter.Context{ + Output: dockerCli.Out(), + Format: formatter.NewServiceListFormat(format, opts.quiet), + } + return formatter.ServiceListWrite(servicesCtx, services, info) } diff --git a/components/cli/config/configfile/file.go b/components/cli/config/configfile/file.go index e8fe96e847..c321b97f2e 100644 --- a/components/cli/config/configfile/file.go +++ b/components/cli/config/configfile/file.go @@ -35,6 +35,7 @@ type ConfigFile struct { CredentialHelpers map[string]string `json:"credHelpers,omitempty"` Filename string `json:"-"` // Note: for internal use only ServiceInspectFormat string `json:"serviceInspectFormat,omitempty"` + ServicesFormat string `json:"servicesFormat,omitempty"` } // LegacyLoadFromReader reads the non-nested configuration data given and sets up the From a5f3b7bb712f7a07adda8323aacf66f6b922953d Mon Sep 17 00:00:00 2001 From: Krasi Georgiev Date: Thu, 2 Feb 2017 00:40:43 +0200 Subject: [PATCH 572/978] more descriptive error fo checkpoint ls for non existent containers Signed-off-by: Krasi Georgiev Upstream-commit: ba79205e30b6626623c0985bb9ad71026e1a62b8 Component: cli --- components/cli/checkpoint_list.go | 4 ++++ components/cli/checkpoint_list_test.go | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/components/cli/checkpoint_list.go b/components/cli/checkpoint_list.go index 8eb720a6b2..97f2badf76 100644 --- a/components/cli/checkpoint_list.go +++ b/components/cli/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/components/cli/checkpoint_list_test.go b/components/cli/checkpoint_list_test.go index 6c90f61e8c..388465715b 100644 --- a/components/cli/checkpoint_list_test.go +++ b/components/cli/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 7b7e8e4da12d01a5d0a65c9cc6eccc9828e4e172 Mon Sep 17 00:00:00 2001 From: Ian Campbell Date: Wed, 1 Feb 2017 16:20:51 +0000 Subject: [PATCH 573/978] Wrap output of docker cli --help This should go some way to unblocking a solution to #18797, #18385 etc by removing the current rather restrictive constraints on help text length. Signed-off-by: Ian Campbell Upstream-commit: 2469463a227703c1aa922135f73af51aa9b2e731 Component: cli --- components/cli/cobra.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/components/cli/cobra.go b/components/cli/cobra.go index 139845cb1b..962b314412 100644 --- a/components/cli/cobra.go +++ b/components/cli/cobra.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/docker/docker/pkg/term" "github.com/spf13/cobra" ) @@ -14,6 +15,7 @@ func SetupRootCommand(rootCmd *cobra.Command) { cobra.AddTemplateFunc("hasManagementSubCommands", hasManagementSubCommands) cobra.AddTemplateFunc("operationSubCommands", operationSubCommands) cobra.AddTemplateFunc("managementSubCommands", managementSubCommands) + cobra.AddTemplateFunc("wrappedFlagUsages", wrappedFlagUsages) rootCmd.SetUsageTemplate(usageTemplate) rootCmd.SetHelpTemplate(helpTemplate) @@ -76,6 +78,14 @@ func operationSubCommands(cmd *cobra.Command) []*cobra.Command { return cmds } +func wrappedFlagUsages(cmd *cobra.Command) string { + width := 80 + if ws, err := term.GetWinsize(0); err == nil { + width = int(ws.Width) + } + return cmd.Flags().FlagUsagesWrapped(width - 1) +} + func managementSubCommands(cmd *cobra.Command) []*cobra.Command { cmds := []*cobra.Command{} for _, sub := range cmd.Commands() { @@ -108,7 +118,7 @@ Examples: {{- if .HasFlags}} Options: -{{.Flags.FlagUsages | trimRightSpace}} +{{ wrappedFlagUsages . | trimRightSpace}} {{- end}} {{- if hasManagementSubCommands . }} From 34cc546fd54cb36650cef4c441f2445709616473 Mon Sep 17 00:00:00 2001 From: allencloud Date: Sat, 4 Feb 2017 00:41:35 +0800 Subject: [PATCH 574/978] update incorrect comments of CheckpointList Signed-off-by: allencloud Upstream-commit: 9d18236794e0f115c5ff8c43dcf8267e8b7213eb Component: cli --- components/cli/checkpoint_list.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/checkpoint_list.go b/components/cli/checkpoint_list.go index 97f2badf76..ffe44bc976 100644 --- a/components/cli/checkpoint_list.go +++ b/components/cli/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 b725dc869d765ab3d53b63e4637b39611d7bb083 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Sat, 28 Jan 2017 16:54:32 -0800 Subject: [PATCH 575/978] 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 Upstream-commit: e301053ff54a44f8324d7008bf70a19700a15230 Component: cli --- components/cli/interface.go | 1 + components/cli/plugin_install.go | 68 ++++++++++++++++++-------------- components/cli/plugin_upgrade.go | 37 +++++++++++++++++ 3 files changed, 76 insertions(+), 30 deletions(-) create mode 100644 components/cli/plugin_upgrade.go diff --git a/components/cli/interface.go b/components/cli/interface.go index d30ba5f705..5823eed883 100644 --- a/components/cli/interface.go +++ b/components/cli/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/components/cli/plugin_install.go b/components/cli/plugin_install.go index b305780cfb..3217c4cf39 100644 --- a/components/cli/plugin_install.go +++ b/components/cli/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/components/cli/plugin_upgrade.go b/components/cli/plugin_upgrade.go new file mode 100644 index 0000000000..95a4356b97 --- /dev/null +++ b/components/cli/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 1ecc83f981f920fda9e66139669581e9706a0abb Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Sat, 28 Jan 2017 16:54:32 -0800 Subject: [PATCH 576/978] 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 Upstream-commit: d98ab3d3ab63706e1d731581cdbce286de20d60a Component: cli --- components/cli/command/formatter/plugin.go | 5 + .../cli/command/formatter/plugin_test.go | 4 +- components/cli/command/plugin/cmd.go | 1 + components/cli/command/plugin/install.go | 89 +++++++++------- components/cli/command/plugin/upgrade.go | 100 ++++++++++++++++++ 5 files changed, 156 insertions(+), 43 deletions(-) create mode 100644 components/cli/command/plugin/upgrade.go diff --git a/components/cli/command/formatter/plugin.go b/components/cli/command/formatter/plugin.go index 5f94714a6b..00bdf3d0f4 100644 --- a/components/cli/command/formatter/plugin.go +++ b/components/cli/command/formatter/plugin.go @@ -85,3 +85,8 @@ func (c *pluginContext) Enabled() bool { c.AddHeader(enabledHeader) return c.p.Enabled } + +func (c *pluginContext) PluginReference() string { + c.AddHeader(imageHeader) + return c.p.PluginReference +} diff --git a/components/cli/command/formatter/plugin_test.go b/components/cli/command/formatter/plugin_test.go index 9ddbe11dff..a6c8f7e6c1 100644 --- a/components/cli/command/formatter/plugin_test.go +++ b/components/cli/command/formatter/plugin_test.go @@ -150,8 +150,8 @@ func TestPluginContextWriteJSON(t *testing.T) { {ID: "pluginID2", Name: "foobar_bar"}, } expectedJSONs := []map[string]interface{}{ - {"Description": "", "Enabled": false, "ID": "pluginID1", "Name": "foobar_baz"}, - {"Description": "", "Enabled": false, "ID": "pluginID2", "Name": "foobar_bar"}, + {"Description": "", "Enabled": false, "ID": "pluginID1", "Name": "foobar_baz", "PluginReference": ""}, + {"Description": "", "Enabled": false, "ID": "pluginID2", "Name": "foobar_bar", "PluginReference": ""}, } out := bytes.NewBufferString("") diff --git a/components/cli/command/plugin/cmd.go b/components/cli/command/plugin/cmd.go index 2173943f89..92c990a975 100644 --- a/components/cli/command/plugin/cmd.go +++ b/components/cli/command/plugin/cmd.go @@ -25,6 +25,7 @@ func NewPluginCommand(dockerCli *command.DockerCli) *cobra.Command { newSetCommand(dockerCli), newPushCommand(dockerCli), newCreateCommand(dockerCli), + newUpgradeCommand(dockerCli), ) return cmd } diff --git a/components/cli/command/plugin/install.go b/components/cli/command/plugin/install.go index ebfe1f1eec..631917a07c 100644 --- a/components/cli/command/plugin/install.go +++ b/components/cli/command/plugin/install.go @@ -15,15 +15,22 @@ import ( "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/registry" "github.com/spf13/cobra" + "github.com/spf13/pflag" "golang.org/x/net/context" ) type pluginOptions struct { - name string - alias string - grantPerms bool - disable bool - args []string + remote string + localName string + grantPerms bool + disable bool + args []string + skipRemoteCheck bool +} + +func loadPullFlags(opts *pluginOptions, flags *pflag.FlagSet) { + flags.BoolVar(&opts.grantPerms, "grant-all-permissions", false, "Grant all permissions necessary to run the plugin") + command.AddTrustVerificationFlags(flags) } func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command { @@ -33,7 +40,7 @@ func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Install a plugin", Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - options.name = args[0] + options.remote = args[0] if len(args) > 1 { options.args = args[1:] } @@ -42,12 +49,9 @@ func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command { } flags := cmd.Flags() - flags.BoolVar(&options.grantPerms, "grant-all-permissions", false, "Grant all permissions necessary to run the plugin") + loadPullFlags(&options, flags) flags.BoolVar(&options.disable, "disable", false, "Do not enable the plugin on install") - flags.StringVar(&options.alias, "alias", "", "Local name for plugin") - - command.AddTrustVerificationFlags(flags) - + flags.StringVar(&options.localName, "alias", "", "Local name for plugin") return cmd } @@ -83,49 +87,33 @@ func newRegistryService() registry.Service { } } -func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error { +func buildPullConfig(ctx context.Context, dockerCli *command.DockerCli, opts pluginOptions, cmdName string) (types.PluginInstallOptions, error) { // Names with both tag and digest will be treated by the daemon - // as a pull by digest with an alias for the tag - // (if no alias is provided). - ref, err := reference.ParseNormalizedNamed(opts.name) + // as a pull by digest with a local name for the tag + // (if no local name is provided). + ref, err := reference.ParseNormalizedNamed(opts.remote) if err != nil { - return err + return types.PluginInstallOptions{}, err } - alias := "" - if opts.alias != "" { - aref, err := reference.ParseNormalizedNamed(opts.alias) - if err != nil { - return err - } - if _, ok := aref.(reference.Canonical); ok { - return fmt.Errorf("invalid name: %s", opts.alias) - } - alias = reference.FamiliarString(reference.EnsureTagged(aref)) - } - ctx := context.Background() - repoInfo, err := registry.ParseRepositoryInfo(ref) if err != nil { - return err + return types.PluginInstallOptions{}, err } remote := ref.String() _, isCanonical := ref.(reference.Canonical) if command.IsTrusted() && !isCanonical { - if alias == "" { - alias = reference.FamiliarString(ref) - } - nt, ok := ref.(reference.NamedTagged) if !ok { nt = reference.EnsureTagged(ref) } + ctx := context.Background() trusted, err := image.TrustedReference(ctx, dockerCli, nt, newRegistryService()) if err != nil { - return err + return types.PluginInstallOptions{}, err } remote = reference.FamiliarString(trusted) } @@ -134,23 +122,42 @@ func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error { encodedAuth, err := command.EncodeAuthToBase64(authConfig) if err != nil { - return err + return types.PluginInstallOptions{}, err } - - registryAuthFunc := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, "plugin install") + registryAuthFunc := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, cmdName) options := types.PluginInstallOptions{ RegistryAuth: encodedAuth, RemoteRef: remote, Disabled: opts.disable, AcceptAllPermissions: opts.grantPerms, - AcceptPermissionsFunc: acceptPrivileges(dockerCli, opts.name), + AcceptPermissionsFunc: acceptPrivileges(dockerCli, opts.remote), // TODO: Rename PrivilegeFunc, it has nothing to do with privileges PrivilegeFunc: registryAuthFunc, Args: opts.args, } + return options, nil +} - responseBody, err := dockerCli.Client().PluginInstall(ctx, alias, options) +func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error { + var localName string + if opts.localName != "" { + aref, err := reference.ParseNormalizedNamed(opts.localName) + if err != nil { + return err + } + if _, ok := aref.(reference.Canonical); ok { + return fmt.Errorf("invalid name: %s", opts.localName) + } + localName = reference.FamiliarString(reference.EnsureTagged(aref)) + } + + ctx := context.Background() + options, err := buildPullConfig(ctx, dockerCli, opts, "plugin install") + if err != nil { + return err + } + responseBody, err := dockerCli.Client().PluginInstall(ctx, localName, options) if err != nil { if strings.Contains(err.Error(), "target is image") { return errors.New(err.Error() + " - Use `docker image pull`") @@ -161,7 +168,7 @@ func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error { if err := jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil); err != nil { return err } - fmt.Fprintf(dockerCli.Out(), "Installed plugin %s\n", opts.name) // todo: return proper values from the API for this result + fmt.Fprintf(dockerCli.Out(), "Installed plugin %s\n", opts.remote) // todo: return proper values from the API for this result return nil } diff --git a/components/cli/command/plugin/upgrade.go b/components/cli/command/plugin/upgrade.go new file mode 100644 index 0000000000..d212cd7e52 --- /dev/null +++ b/components/cli/command/plugin/upgrade.go @@ -0,0 +1,100 @@ +package plugin + +import ( + "bufio" + "context" + "fmt" + "strings" + + "github.com/docker/docker/cli" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/pkg/jsonmessage" + "github.com/docker/docker/reference" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +func newUpgradeCommand(dockerCli *command.DockerCli) *cobra.Command { + var options pluginOptions + cmd := &cobra.Command{ + Use: "upgrade [OPTIONS] PLUGIN [REMOTE]", + Short: "Upgrade an existing plugin", + Args: cli.RequiresRangeArgs(1, 2), + RunE: func(cmd *cobra.Command, args []string) error { + options.localName = args[0] + if len(args) == 2 { + options.remote = args[1] + } + return runUpgrade(dockerCli, options) + }, + } + + flags := cmd.Flags() + loadPullFlags(&options, flags) + flags.BoolVar(&options.skipRemoteCheck, "skip-remote-check", false, "Do not check if specified remote plugin matches existing plugin image") + return cmd +} + +func runUpgrade(dockerCli *command.DockerCli, opts pluginOptions) error { + ctx := context.Background() + p, _, err := dockerCli.Client().PluginInspectWithRaw(ctx, opts.localName) + if err != nil { + return fmt.Errorf("error reading plugin data: %v", err) + } + + if p.Enabled { + return fmt.Errorf("the plugin must be disabled before upgrading") + } + + opts.localName = p.Name + if opts.remote == "" { + opts.remote = p.PluginReference + } + remote, err := reference.ParseNamed(opts.remote) + if err != nil { + return errors.Wrap(err, "error parsing remote upgrade image reference") + } + remote = reference.WithDefaultTag(remote) + + old, err := reference.ParseNamed(p.PluginReference) + if err != nil { + return errors.Wrap(err, "error parsing current image reference") + } + old = reference.WithDefaultTag(old) + + fmt.Fprintf(dockerCli.Out(), "Upgrading plugin %s from %s to %s\n", p.Name, old, remote) + if !opts.skipRemoteCheck && remote.String() != old.String() { + _, err := fmt.Fprint(dockerCli.Out(), "Plugin images do not match, are you sure? ") + if err != nil { + return errors.Wrap(err, "error writing to stdout") + } + + rdr := bufio.NewReader(dockerCli.In()) + line, _, err := rdr.ReadLine() + if err != nil { + return errors.Wrap(err, "error reading from stdin") + } + if strings.ToLower(string(line)) != "y" { + return errors.New("canceling upgrade request") + } + } + + options, err := buildPullConfig(ctx, dockerCli, opts, "plugin upgrade") + if err != nil { + return err + } + + responseBody, err := dockerCli.Client().PluginUpgrade(ctx, opts.localName, options) + if err != nil { + if strings.Contains(err.Error(), "target is image") { + return errors.New(err.Error() + " - Use `docker image pull`") + } + return err + } + defer responseBody.Close() + if err := jsonmessage.DisplayJSONMessagesToStream(responseBody, dockerCli.Out(), nil); err != nil { + return err + } + fmt.Fprintf(dockerCli.Out(), "Upgraded plugin %s to %s\n", opts.localName, opts.remote) // todo: return proper values from the API for this result + return nil +} From b1407415266f4af5bacd8310b837a5743f9f2435 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 16 Jan 2017 15:35:27 +0100 Subject: [PATCH 577/978] 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 Upstream-commit: 0ae3a20be60e81f1f98e1398958fd4627cb6a31c Component: cli --- components/cli/errors.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/errors.go b/components/cli/errors.go index 2912692ec1..4f767bd8d3 100644 --- a/components/cli/errors.go +++ b/components/cli/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 1855724a2add3d3df375ccdeeda2640cd235916f Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 16 Jan 2017 15:35:27 +0100 Subject: [PATCH 578/978] 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 Upstream-commit: 0c71f3602737cf2df0d76467054835c93e284a47 Component: cli --- components/cli/docker.go | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/components/cli/docker.go b/components/cli/docker.go index 685f565c8d..f992b08469 100644 --- a/components/cli/docker.go +++ b/components/cli/docker.go @@ -17,6 +17,7 @@ import ( "github.com/docker/docker/pkg/term" "github.com/spf13/cobra" "github.com/spf13/pflag" + "strings" ) func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command { @@ -144,7 +145,7 @@ func hideUnsupportedFeatures(cmd *cobra.Command, clientVersion string, hasExperi } // hide flags not supported by the server - if flagVersion, ok := f.Annotations["version"]; ok && len(flagVersion) == 1 && versions.LessThan(clientVersion, flagVersion[0]) { + if !isFlagSupported(f, clientVersion) { f.Hidden = true } @@ -168,13 +169,44 @@ func hideUnsupportedFeatures(cmd *cobra.Command, clientVersion string, hasExperi func isSupported(cmd *cobra.Command, clientVersion string, hasExperimental bool) error { if !hasExperimental { if _, ok := cmd.Tags["experimental"]; ok { - return errors.New("only supported with experimental daemon") + return errors.New("only supported on a Docker daemon with experimental features enabled") } } if cmdVersion, ok := cmd.Tags["version"]; ok && versions.LessThan(clientVersion, cmdVersion) { - return fmt.Errorf("only supported with daemon version >= %s", cmdVersion) + return fmt.Errorf("requires API version %s, but the Docker daemon API version is %s", cmdVersion, clientVersion) + } + + errs := []string{} + + cmd.Flags().VisitAll(func(f *pflag.Flag) { + if f.Changed { + if !isFlagSupported(f, clientVersion) { + errs = append(errs, fmt.Sprintf("\"--%s\" requires API version %s, but the Docker daemon API version is %s", f.Name, getFlagVersion(f), clientVersion)) + return + } + if _, ok := f.Annotations["experimental"]; ok && !hasExperimental { + errs = append(errs, fmt.Sprintf("\"--%s\" is only supported on a Docker daemon with experimental features enabled", f.Name)) + } + } + }) + if len(errs) > 0 { + return errors.New(strings.Join(errs, "\n")) } return nil } + +func getFlagVersion(f *pflag.Flag) string { + if flagVersion, ok := f.Annotations["version"]; ok && len(flagVersion) == 1 { + return flagVersion[0] + } + return "" +} + +func isFlagSupported(f *pflag.Flag, clientVersion string) bool { + if v := getFlagVersion(f); v != "" { + return versions.GreaterThanOrEqualTo(clientVersion, v) + } + return true +} From 857a9c87228b5ca8fb3ca88f07f03c4801933796 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sat, 4 Feb 2017 13:55:28 -0800 Subject: [PATCH 579/978] Add compose support of `attachable` in networks This fix tries to address the issue raised in 29975 where it was not possible to specify `attachable` flag for networks in compose format. NOTE: Compose format aleady supports `labels` in networks. This fix adds the support of `attachable` for compose v3.1 format. Additiona unit tests have been updated and added. This fix fixes 29975. Signed-off-by: Yong Tang Upstream-commit: 1a677699aede7b68ebcc7fc1f6c0b693e00f075f Component: cli --- components/cli/compose/convert/compose.go | 9 ++++--- .../cli/compose/convert/compose_test.go | 18 ++++++++++--- components/cli/compose/loader/loader_test.go | 26 +++++++++++++++++++ components/cli/compose/schema/bindata.go | 4 +-- .../schema/data/config_schema_v3.1.json | 1 + components/cli/compose/types/types.go | 1 + 6 files changed, 50 insertions(+), 9 deletions(-) diff --git a/components/cli/compose/convert/compose.go b/components/cli/compose/convert/compose.go index efcf8a6979..a4571df02f 100644 --- a/components/cli/compose/convert/compose.go +++ b/components/cli/compose/convert/compose.go @@ -61,10 +61,11 @@ func Networks(namespace Namespace, networks networkMap, servicesNetworks map[str } createOpts := types.NetworkCreate{ - Labels: AddStackLabel(namespace, network.Labels), - Driver: network.Driver, - Options: network.DriverOpts, - Internal: network.Internal, + Labels: AddStackLabel(namespace, network.Labels), + Driver: network.Driver, + Options: network.DriverOpts, + Internal: network.Internal, + Attachable: network.Attachable, } if network.Ipam.Driver != "" || len(network.Ipam.Config) > 0 { diff --git a/components/cli/compose/convert/compose_test.go b/components/cli/compose/convert/compose_test.go index 18c7aac938..c267820956 100644 --- a/components/cli/compose/convert/compose_test.go +++ b/components/cli/compose/convert/compose_test.go @@ -30,9 +30,10 @@ func TestAddStackLabel(t *testing.T) { func TestNetworks(t *testing.T) { namespace := Namespace{name: "foo"} serviceNetworks := map[string]struct{}{ - "normal": {}, - "outside": {}, - "default": {}, + "normal": {}, + "outside": {}, + "default": {}, + "attachablenet": {}, } source := networkMap{ "normal": composetypes.NetworkConfig{ @@ -58,6 +59,10 @@ func TestNetworks(t *testing.T) { Name: "special", }, }, + "attachablenet": composetypes.NetworkConfig{ + Driver: "overlay", + Attachable: true, + }, } expected := map[string]types.NetworkCreate{ "default": { @@ -83,6 +88,13 @@ func TestNetworks(t *testing.T) { "something": "labeled", }, }, + "attachablenet": { + Driver: "overlay", + Attachable: true, + Labels: map[string]string{ + LabelNamespace: "foo", + }, + }, } networks, externals := Networks(namespace, source, serviceNetworks) diff --git a/components/cli/compose/loader/loader_test.go b/components/cli/compose/loader/loader_test.go index bb5d3ecc06..3a2f27204d 100644 --- a/components/cli/compose/loader/loader_test.go +++ b/components/cli/compose/loader/loader_test.go @@ -799,3 +799,29 @@ type servicesByName []types.ServiceConfig func (sbn servicesByName) Len() int { return len(sbn) } func (sbn servicesByName) Swap(i, j int) { sbn[i], sbn[j] = sbn[j], sbn[i] } func (sbn servicesByName) Less(i, j int) bool { return sbn[i].Name < sbn[j].Name } + +func TestLoadAttachableNetwork(t *testing.T) { + config, err := loadYAML(` +version: "3.1" +networks: + mynet1: + driver: overlay + attachable: true + mynet2: + driver: bridge +`) + assert.NoError(t, err) + + expected := map[string]types.NetworkConfig{ + "mynet1": { + Driver: "overlay", + Attachable: true, + }, + "mynet2": { + Driver: "bridge", + Attachable: false, + }, + } + + assert.Equal(t, expected, config.Networks) +} diff --git a/components/cli/compose/schema/bindata.go b/components/cli/compose/schema/bindata.go index 9486e91ae0..bb91fbfa5f 100644 --- a/components/cli/compose/schema/bindata.go +++ b/components/cli/compose/schema/bindata.go @@ -69,7 +69,7 @@ func (fi bindataFileInfo) Sys() interface{} { return nil } -var _dataConfig_schema_v30Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x5a\x4f\x8f\xdb\xb8\x0e\xbf\xe7\x53\x18\x6e\x6f\xcd\xcc\x14\x78\xc5\x03\x5e\x6f\xef\xb8\xa7\xdd\xf3\x0e\x5c\x43\xb1\x99\x44\x1d\x59\x52\x29\x39\x9d\xb4\xc8\x77\x5f\xc8\xff\x22\x2b\x92\xe5\x24\xee\xb6\x87\x9e\x66\x62\x91\x14\xff\xe9\x47\x8a\xf6\xf7\x55\x92\xa4\x6f\x55\xb1\x87\x8a\xa4\x1f\x93\x74\xaf\xb5\xfc\xf8\xf4\xf4\x59\x09\xfe\xd0\x3e\x7d\x14\xb8\x7b\x2a\x91\x6c\xf5\xc3\xfb\x0f\x4f\xed\xb3\x37\xe9\xda\xf0\xd1\xd2\xb0\x14\x82\x6f\xe9\x2e\x6f\x57\xf2\xc3\x7f\x1e\xdf\x3f\x1a\xf6\x96\x44\x1f\x25\x18\x22\xb1\xf9\x0c\x85\x6e\x9f\x21\x7c\xa9\x29\x82\x61\x7e\x4e\x0f\x80\x8a\x0a\x9e\x66\xeb\x95\x59\x93\x28\x24\xa0\xa6\xa0\xd2\x8f\x89\x51\x2e\x49\x06\x92\xfe\x81\x25\x56\x69\xa4\x7c\x97\x36\x8f\x4f\x8d\x84\x24\x49\x15\xe0\x81\x16\x96\x84\x41\xd5\x37\x4f\x67\xf9\x4f\x03\xd9\xda\x95\x6a\x29\xdb\x3c\x97\x44\x6b\x40\xfe\xd7\xa5\x6e\xcd\xf2\xa7\x67\xf2\xf0\xed\xff\x0f\x7f\xbf\x7f\xf8\xdf\x63\xfe\x90\xbd\x7b\x3b\x5a\x36\xfe\x45\xd8\xb6\xdb\x97\xb0\xa5\x9c\x6a\x2a\xf8\xb0\x7f\x3a\x50\x9e\xba\xff\x4e\xc3\xc6\xa4\x2c\x1b\x62\xc2\x46\x7b\x6f\x09\x53\x30\xb6\x99\x83\xfe\x2a\xf0\x25\x66\xf3\x40\xf6\x93\x6c\xee\xf6\xf7\xd8\x3c\x36\xe7\x20\x58\x5d\x45\x23\xd8\x53\xfd\x24\x63\xda\xed\xef\x8b\xdf\xaa\x37\x7a\x92\xb6\xa5\xb0\xf6\x6e\x14\x1c\x65\xbb\xcf\x55\xbe\x6c\x0b\xfb\x6a\x70\x56\xc0\x4b\x25\x48\x26\x8e\xe6\x59\xc0\x1f\x2d\x41\x05\x5c\xa7\x83\x0b\x92\x24\xdd\xd4\x94\x95\xae\x47\x05\x87\x3f\x8d\x88\x67\xeb\x61\x92\x7c\x77\x0f\xb6\x25\xa7\x59\x1f\xfd\x0a\x07\x7c\x58\x0f\xd8\x32\xac\x17\x82\x6b\x78\xd5\x8d\x51\xd3\x5b\xb7\x2e\x10\xc5\x0b\xe0\x96\x32\x98\xcb\x41\x70\xa7\x26\x5c\xc6\xa8\xd2\xb9\xc0\xbc\xa4\x85\x4e\x4f\x0e\xfb\x85\xbc\x78\x3e\x0d\xac\xd6\xaf\x6c\xe5\x11\x98\x16\x44\xe6\xa4\x2c\x47\x76\x10\x44\x72\x4c\xd7\x49\x4a\x35\x54\xca\x6f\x62\x92\xd6\x9c\x7e\xa9\xe1\x8f\x8e\x44\x63\x0d\xae\xdc\x12\x85\x5c\x5e\xf0\x0e\x45\x2d\x73\x49\xd0\x24\xd8\xb4\xfb\xd3\x42\x54\x15\xe1\x4b\x65\xdd\x35\x76\xcc\xf0\xbc\xe0\x9a\x50\x0e\x98\x73\x52\xc5\x12\xc9\x9c\x3a\xe0\xa5\xca\xdb\xfa\x37\x99\x46\xdb\xbc\xe5\x57\x8e\x80\xa1\x18\x2e\x1a\x8f\x92\x4f\x25\x76\x2b\xc6\xa4\xb6\xd1\x2d\x75\x18\x73\x05\x04\x8b\xfd\x8d\xfc\xa2\x22\x94\xcf\xf1\x1d\x70\x8d\x47\x29\x68\x9b\x2f\xbf\x5c\x22\x00\x3f\xe4\x03\x96\x5c\xed\x06\xe0\x07\x8a\x82\x57\xfd\x69\x98\x03\x30\x03\xc8\x1b\xfe\x57\x29\x14\xb8\x8e\x71\x0c\xb4\x97\x06\x53\x47\x3e\xe9\x39\x9e\x7b\xc3\xd7\x49\xca\xeb\x6a\x03\x68\x5a\xba\x11\xe5\x56\x60\x45\x8c\xb2\xfd\xde\xd6\xf2\xc8\xd3\x9e\xcc\xb3\x1d\x68\xdb\x60\xca\x3a\x61\x39\xa3\xfc\x65\xf9\x14\x87\x57\x8d\x24\xdf\x0b\xa5\xe7\x63\xb8\xc5\xbe\x07\xc2\xf4\xbe\xd8\x43\xf1\x32\xc1\x6e\x53\x8d\xb8\x85\xd2\x73\x92\x9c\x56\x64\x17\x27\x92\x45\x8c\x84\x91\x0d\xb0\x9b\xec\x5c\xd4\xf9\x96\x58\xb1\xdb\x19\xd2\x50\xc6\x5d\x74\x2e\xdd\x72\xac\xe6\x97\x48\x0f\x80\x73\x0b\xb8\x90\xe7\x86\xcb\x5d\x8c\x37\x20\x49\xbc\xfb\x1c\x91\x7e\x7a\x6c\x9b\xcf\x89\x53\xd5\xfc\xc7\x58\x9a\xb9\xed\x42\xe2\xd4\x7d\xdf\x13\xc7\xc2\x79\x0d\xc5\x28\x2a\x15\x29\x4c\xdf\x80\xa0\x02\x71\x3d\x93\x76\xcd\x7e\x5e\x89\x32\x94\xa0\x17\xc4\xae\x6f\x82\x48\x7d\x75\x21\x4c\x6e\xea\x1f\x67\x85\x2e\x7a\x81\x88\x58\x13\x52\x6f\xae\x9a\x67\x75\xe3\x29\xd6\xd0\x11\x46\x89\x82\xf8\x61\x0f\x3a\x72\x24\x8d\xca\xc3\x87\x99\x39\xe1\xe3\xfd\xef\x24\x6f\x80\x35\x28\x73\x7e\x8f\x1c\x11\x75\x56\xa5\x39\x6e\x3e\x45\xb2\xc8\x69\xfb\xc1\x2d\xbc\xa4\x65\x18\x2b\x1a\x84\xb0\x0f\x98\x14\xa8\x2f\x4e\xd7\xbf\x53\xee\xdb\xad\xef\xae\xf6\x12\xe9\x81\x32\xd8\xc1\xf8\xd6\xb2\x11\x82\x01\xe1\x23\xe8\x41\x20\x65\x2e\x38\x3b\xce\xa0\x54\x9a\x60\xf4\x42\xa1\xa0\xa8\x91\xea\x63\x2e\xa4\x5e\xbc\xcf\x50\xfb\x2a\x57\xf4\x1b\x8c\xa3\x79\xc6\xfb\x4e\x50\x36\xe2\x39\xaa\x42\xdf\x56\xaf\x95\x2e\x29\xcf\x85\x04\x1e\xf5\x8e\xd2\x42\xe6\x3b\x24\x05\xe4\x12\x90\x8a\xd2\x67\xe0\xda\x8e\x75\x59\x23\x31\xfb\x5f\x8a\x51\x74\xc7\x09\x8b\x39\x5a\x57\x72\x7b\xe3\xc5\x42\xeb\x78\xb8\x6b\x46\x2b\x1a\x3e\x07\x1e\x80\x9d\x51\x03\x5a\xfc\xf7\xc3\xfe\x04\xe4\x9f\x35\xa5\x5c\xc3\x0e\xd0\x87\x94\x13\x5d\xc7\x74\xd3\x31\xa3\xdb\xd8\x13\x1c\x07\x74\x42\x8f\x86\x41\x89\xad\xf6\x33\xf8\x7a\x11\xaf\x5e\xa3\xe1\x6f\x23\x6f\xdd\x29\x92\x79\xe9\xaf\x82\x73\x57\x8d\x2c\x88\xa8\x27\x2f\xa2\xd6\x2a\xda\x18\x36\x34\x5c\x4d\x35\x35\x03\xa9\x35\xc5\x5c\x14\x2f\x4c\xa3\x64\x0e\x41\x49\xfd\xda\xae\x1c\xcb\xae\x98\x23\x3b\x77\x96\x5e\x80\x6f\xa2\x68\x93\x46\x27\xb0\xd3\xd3\xcd\x8e\x28\x38\x79\xa4\x8a\x6c\x9c\x99\x9b\xef\x70\x9b\x6c\xc4\x43\x1c\x63\x10\x34\x52\x27\x2e\x1d\xda\x8e\xf0\x04\xd4\xaf\x39\x38\xd0\xb4\x02\x51\xfb\x6b\xd6\xca\xce\xef\x8e\x29\xb5\x26\xb3\x91\xa0\x5a\x94\x6e\x4c\x9f\x87\xa0\xf6\xfd\x45\x34\x70\x73\x0e\x09\x82\x64\xb4\x20\x2a\x06\x44\x77\x5c\x50\x6b\x59\x12\x0d\x79\xfb\xa2\xea\x2a\xe8\x9f\xc0\x7c\x49\x90\x30\x06\x8c\xaa\x6a\x0e\x86\xa6\x25\x30\x72\xbc\xa9\x7c\x36\xec\x5b\x42\x59\x8d\x90\x93\x42\x77\xef\xc2\x22\x39\x97\x56\x82\x53\x2d\xbc\x08\x31\x6f\xcb\x8a\xbc\xe6\xfd\xb6\x0d\x89\xf7\xc0\x04\xdb\xba\xb9\x77\x4b\x2b\x13\x94\xa8\xb1\xb8\x70\xf6\xcd\x21\x3a\xd7\xfa\x40\xc6\xf4\x3b\x5e\x98\x8e\xa0\x0c\x92\x0c\x57\xff\x28\x7f\xb4\xb4\x74\x7d\x66\x2e\x05\xa3\xc5\x71\x29\x0b\x0b\xc1\x5b\x27\xcf\x49\x88\x3b\x33\xd0\xa4\x83\x69\x85\x2a\xa9\xa3\x87\xb5\x61\xf8\x4a\x79\x29\xbe\x5e\xb1\xe1\x72\xa9\x24\x19\x29\xc0\xc1\xbb\x7b\x1d\xad\x34\x12\xca\xf5\xd5\xe5\xfc\x5e\xb3\xee\xa8\xe6\x43\x7e\x46\x50\x7f\xa0\x8b\xbf\x49\x0d\x20\x7d\x21\xeb\xe8\x3c\xa8\x82\x4a\xa0\x37\x01\x17\x78\xf3\x1d\x33\xb1\x27\x5b\xa0\xaa\xcd\x1a\x20\x76\x54\xe6\xbe\xb8\xf8\x6d\x23\x3e\x24\xcc\xe2\x80\x44\x25\xa9\x96\x3a\x1d\xb3\x47\xaa\xa9\xb7\x06\x27\xd3\xa3\x88\x24\x3c\x8e\x88\x69\x1d\xd7\xbd\xa3\x50\xf5\x86\xc3\x64\x47\x65\xf9\xd3\xf7\x9e\x77\xfe\x35\xe5\x14\xbe\x94\xdc\x07\x7a\xfd\xdb\x90\x40\x54\x9f\x87\x9e\x79\x3d\xf8\x2a\x9b\x1d\xe2\xe0\xab\x88\xe5\xf4\xbf\xb2\xc1\xbb\x03\x33\xba\x2f\x37\x22\x90\xd1\x51\xfd\x46\x8c\xdf\xf9\x75\x65\x7e\x39\x43\x2a\x2b\xcf\x2e\xef\x8f\x53\x29\x31\x7b\x3a\xdf\x71\x64\x63\x35\x5c\x32\xcf\x07\x74\x63\xb4\x9d\x1a\x4a\xf4\x24\x81\x69\xad\xb3\x69\xe7\xc4\x69\xcb\x17\xcc\xf0\xc7\x77\x13\x35\x65\xea\x2d\xda\x0f\x02\xe3\x05\x06\x3e\xfe\x98\x3a\x8d\x68\xef\xdd\xcb\xaf\xc0\x02\xa0\x66\xf1\x5f\x7c\x13\x66\xec\xe4\xc7\x8b\xf9\xc6\xf7\xf1\xd0\xae\xfd\x9e\x2b\x1b\xf9\xc7\x21\x69\xdf\x49\x5b\x90\x92\xd9\xbd\x79\x28\x8c\xde\x2f\xc5\xdc\x91\x61\xff\xc5\x56\xe6\x87\xab\x95\xfd\xb7\xf9\xba\x6e\x75\x5a\xfd\x13\x00\x00\xff\xff\x46\xf7\x7b\x23\xe5\x2a\x00\x00") +var _dataConfig_schema_v30Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x5a\x4b\x8f\xdb\x38\x12\xbe\xfb\x57\x08\x4a\x6e\x71\x77\x07\xd8\x60\x81\xcd\x6d\x8f\x7b\xda\x39\x4f\x43\x11\x68\xa9\x6c\x33\x4d\x91\x4c\x91\x72\xda\x09\xfc\xdf\x07\xd4\xcb\x14\x4d\x8a\xb2\xad\x3c\x30\x98\x53\xb7\xc5\xaa\x62\xbd\xf8\x55\xb1\xa4\xef\xab\x24\x49\xdf\xaa\x62\x0f\x15\x49\x3f\x26\xe9\x5e\x6b\xf9\xf1\xe9\xe9\xb3\x12\xfc\xa1\x7d\xfa\x28\x70\xf7\x54\x22\xd9\xea\x87\xf7\x1f\x9e\xda\x67\x6f\xd2\xb5\xe1\xa3\xa5\x61\x29\x04\xdf\xd2\x5d\xde\xae\xe4\x87\x7f\x3d\xbe\x7f\x34\xec\x2d\x89\x3e\x4a\x30\x44\x62\xf3\x19\x0a\xdd\x3e\x43\xf8\x52\x53\x04\xc3\xfc\x9c\x1e\x00\x15\x15\x3c\xcd\xd6\x2b\xb3\x26\x51\x48\x40\x4d\x41\xa5\x1f\x13\xa3\x5c\x92\x0c\x24\xfd\x03\x4b\xac\xd2\x48\xf9\x2e\x6d\x1e\x9f\x1a\x09\x49\x92\x2a\xc0\x03\x2d\x2c\x09\x83\xaa\x6f\x9e\xce\xf2\x9f\x06\xb2\xb5\x2b\xd5\x52\xb6\x79\x2e\x89\xd6\x80\xfc\x8f\x4b\xdd\x9a\xe5\x4f\xcf\xe4\xe1\xdb\x7f\x1f\xfe\x7c\xff\xf0\x9f\xc7\xfc\x21\x7b\xf7\x76\xb4\x6c\xfc\x8b\xb0\x6d\xb7\x2f\x61\x4b\x39\xd5\x54\xf0\x61\xff\x74\xa0\x3c\x75\xff\x9d\x86\x8d\x49\x59\x36\xc4\x84\x8d\xf6\xde\x12\xa6\x60\x6c\x33\x07\xfd\x55\xe0\x4b\xcc\xe6\x81\xec\x17\xd9\xdc\xed\xef\xb1\x79\x6c\xce\x41\xb0\xba\x8a\x46\xb0\xa7\xfa\x45\xc6\xb4\xdb\xdf\x17\xbf\x55\x6f\xf4\x24\x6d\x4b\x61\xed\xdd\x28\x38\xca\x76\x9f\xab\x7c\xd9\x16\xf6\xd5\xe0\xac\x80\x97\x4a\x90\x4c\x1c\xcd\xb3\x80\x3f\x5a\x82\x0a\xb8\x4e\x07\x17\x24\x49\xba\xa9\x29\x2b\x5d\x8f\x0a\x0e\xff\x37\x22\x9e\xad\x87\x49\xf2\xdd\x3d\xd8\x96\x9c\x66\x7d\xf4\x2b\x1c\xf0\x61\x3d\x60\xcb\xb0\x5e\x08\xae\xe1\x55\x37\x46\x4d\x6f\xdd\xba\x40\x14\x2f\x80\x5b\xca\x60\x2e\x07\xc1\x9d\x9a\x70\x19\xa3\x4a\xe7\x02\xf3\x92\x16\x3a\x3d\x39\xec\x17\xf2\xe2\xf9\x34\xb0\x5a\xbf\xb2\x95\x47\x60\x5a\x10\x99\x93\xb2\x1c\xd9\x41\x10\xc9\x31\x5d\x27\x29\xd5\x50\x29\xbf\x89\x49\x5a\x73\xfa\xa5\x86\xff\x75\x24\x1a\x6b\x70\xe5\x96\x28\xe4\xf2\x82\x77\x28\x6a\x99\x4b\x82\x26\xc1\xa6\xdd\x9f\x16\xa2\xaa\x08\x5f\x2a\xeb\xae\xb1\x63\x86\xe7\x05\xd7\x84\x72\xc0\x9c\x93\x2a\x96\x48\xe6\xd4\x01\x2f\x55\xde\xd6\xbf\xc9\x34\xda\xe6\x2d\xbf\x72\x04\x0c\xc5\x70\xd1\x78\x94\x7c\x2a\xb1\x5b\x31\x26\xb5\x8d\x6e\xa9\xc3\x98\x2b\x20\x58\xec\x6f\xe4\x17\x15\xa1\x7c\x8e\xef\x80\x6b\x3c\x4a\x41\xdb\x7c\xf9\xed\x12\x01\xf8\x21\x1f\xb0\xe4\x6a\x37\x00\x3f\x50\x14\xbc\xea\x4f\xc3\x1c\x80\x19\x40\xde\xf0\xbf\x4a\xa1\xc0\x75\x8c\x63\xa0\xbd\x34\x98\x3a\xf2\x49\xcf\xf1\xdc\x1b\xbe\x4e\x52\x5e\x57\x1b\x40\xd3\xd2\x8d\x28\xb7\x02\x2b\x62\x94\xed\xf7\xb6\x96\x47\x9e\xf6\x64\x9e\xed\x40\xdb\x06\x53\xd6\x09\xcb\x19\xe5\x2f\xcb\xa7\x38\xbc\x6a\x24\xf9\x5e\x28\x3d\x1f\xc3\x2d\xf6\x3d\x10\xa6\xf7\xc5\x1e\x8a\x97\x09\x76\x9b\x6a\xc4\x2d\x94\x9e\x93\xe4\xb4\x22\xbb\x38\x91\x2c\x62\x24\x8c\x6c\x80\xdd\x64\xe7\xa2\xce\xb7\xc4\x8a\xdd\xce\x90\x86\x32\xee\xa2\x73\xe9\x96\x63\x35\xbf\x44\x7a\x00\x9c\x5b\xc0\x85\x3c\x37\x5c\xee\x62\xbc\x01\x49\xe2\xdd\xe7\x88\xf4\xd3\x63\xdb\x7c\x4e\x9c\xaa\xe6\x3f\xc6\xd2\xcc\x6d\x17\x12\xa7\xee\xfb\x9e\x38\x16\xce\x6b\x28\x46\x51\xa9\x48\x61\xfa\x06\x04\x15\x88\xeb\x99\xb4\x6b\xf6\xf3\x4a\x94\xa1\x04\xbd\x20\x76\x7d\x13\x44\xea\xab\x0b\x61\x72\x53\xff\x38\x2b\x74\xd1\x0b\x44\xc4\x9a\x90\x7a\x73\xd5\x3c\xab\x1b\x4f\xb1\x86\x8e\x30\x4a\x14\xc4\x0f\x7b\xd0\x91\x23\x69\x54\x1e\x3e\xcc\xcc\x09\x1f\xef\xbf\x27\x79\x03\xac\x41\x99\xf3\x7b\xe4\x88\xa8\xb3\x2a\xcd\x71\xf3\x29\x92\x45\x4e\xdb\x0f\x6e\xe1\x25\x2d\xc3\x58\xd1\x20\x84\x7d\xc0\xa4\x40\x7d\x71\xba\x7e\x4e\xb9\x6f\xb7\xbe\xbb\xda\x4b\xa4\x07\xca\x60\x07\xe3\x5b\xcb\x46\x08\x06\x84\x8f\xa0\x07\x81\x94\xb9\xe0\xec\x38\x83\x52\x69\x82\xd1\x0b\x85\x82\xa2\x46\xaa\x8f\xb9\x90\x7a\xf1\x3e\x43\xed\xab\x5c\xd1\x6f\x30\x8e\xe6\x19\xef\x3b\x41\xd9\x88\xe7\xa8\x0a\x7d\x5b\xbd\x56\xba\xa4\x3c\x17\x12\x78\xd4\x3b\x4a\x0b\x99\xef\x90\x14\x90\x4b\x40\x2a\x4a\x9f\x81\x6b\x3b\xd6\x65\x8d\xc4\xec\x7f\x29\x46\xd1\x1d\x27\x2c\xe6\x68\x5d\xc9\xed\x8d\x17\x0b\xad\xe3\xe1\xae\x19\xad\x68\xf8\x1c\x78\x00\x76\x46\x0d\x68\xf1\xdf\x0f\xfb\x13\x90\x7f\xd6\x94\x72\x0d\x3b\x40\x1f\x52\x4e\x74\x1d\xd3\x4d\xc7\x8c\x6e\x63\x4f\x70\x1c\xd0\x09\x3d\x1a\x06\x25\xb6\xda\xcf\xe0\xeb\x45\xbc\x7a\x8d\x86\xbf\x8d\xbc\x75\xa7\x48\xe6\xa5\xbf\x0a\xce\x5d\x35\xb2\x20\xa2\x9e\xbc\x88\x5a\xab\x68\x63\xd8\xd0\x70\x35\xd5\xd4\x0c\xa4\xd6\x14\x73\x51\xbc\x30\x8d\x92\x39\x04\x25\xf5\x6b\xbb\x72\x2c\xbb\x62\x8e\xec\xdc\x59\x7a\x01\xbe\x89\xa2\x4d\x1a\x9d\xc0\x4e\x4f\x37\x3b\xa2\xe0\xe4\x91\x2a\xb2\x71\x66\x6e\xbe\xc3\x6d\xb2\x11\x0f\x71\x8c\x41\xd0\x48\x9d\xb8\x74\x68\x3b\xc2\x13\x50\xbf\xe7\xe0\x40\xd3\x0a\x44\xed\xaf\x59\x2b\x3b\xbf\x3b\xa6\xd4\x9a\xcc\x46\x82\x6a\x51\xba\x31\x7d\x1e\x82\xda\xf7\x17\xd1\xc0\xcd\x39\x24\x08\x92\xd1\x82\xa8\x18\x10\xdd\x71\x41\xad\x65\x49\x34\xe4\xed\x8b\xaa\xab\xa0\x7f\x02\xf3\x25\x41\xc2\x18\x30\xaa\xaa\x39\x18\x9a\x96\xc0\xc8\xf1\xa6\xf2\xd9\xb0\x6f\x09\x65\x35\x42\x4e\x0a\xdd\xbd\x0b\x8b\xe4\x5c\x5a\x09\x4e\xb5\xf0\x22\xc4\xbc\x2d\x2b\xf2\x9a\xf7\xdb\x36\x24\xde\x03\x13\x6c\xeb\xe6\xde\x2d\xad\x4c\x50\xa2\xc6\xe2\xc2\xd9\x37\x87\xe8\x5c\xeb\x03\x19\xd3\xef\x78\x61\x3a\x82\x32\x48\x32\x5c\xfd\xa3\xfc\xd1\xd2\xd2\xf5\x99\xb9\x14\x8c\x16\xc7\xa5\x2c\x2c\x04\x6f\x9d\x3c\x27\x21\xee\xcc\x40\x93\x0e\xa6\x15\xaa\xa4\x8e\x1e\xd6\x86\xe1\x2b\xe5\xa5\xf8\x7a\xc5\x86\xcb\xa5\x92\x64\xa4\x00\x07\xef\xee\x75\xb4\xd2\x48\x28\xd7\x57\x97\xf3\x7b\xcd\xba\xa3\x9a\x0f\xf9\x19\x41\xfd\x81\x2e\xfe\x26\x35\x80\xf4\x85\xac\xa3\xf3\xa0\x0a\x2a\x81\xde\x04\x5c\xe0\xcd\x77\xcc\xc4\x9e\x6c\x81\xaa\x36\x6b\x80\xd8\x51\x99\xfb\xe2\xe2\xb7\x8d\xf8\x90\x30\x8b\x03\x12\x95\xa4\x5a\xea\x74\xcc\x1e\xa9\xa6\xde\x1a\x9c\x4c\x8f\x22\x92\xf0\x38\x22\xa6\x75\x5c\xf7\x8e\x42\xd5\x1b\x0e\x93\x1d\x95\xe5\x4f\xdf\x7b\xde\xf9\xd7\x94\x53\xf8\x52\x72\x1f\xe8\xf5\x6f\x43\x02\x51\x7d\x1e\x7a\xe6\xf5\xe0\xab\x6c\x76\x88\x83\xaf\x22\x96\xd3\xbf\x69\xdf\xdd\x11\x81\xaf\xcf\xbf\xb2\x13\xbc\x03\x5c\xba\x4f\x3c\x22\xd8\xd2\x51\xfd\x03\x2d\x7f\x93\x44\xfc\x79\xf9\xe5\x4c\xb3\xac\x3c\xbb\xbc\x68\x4e\xa5\xc4\xec\x31\x7e\xc7\x91\x8d\xd5\x70\xc9\x3c\x5f\xda\x8d\x61\x79\x6a\x7a\xd1\x93\x04\xc6\xba\xce\xa6\x9d\x13\xa7\x2d\x5f\x30\xc3\x1f\xdf\x4d\x14\x9f\xa9\xd7\x6d\x3f\x08\xb5\x17\x98\x0c\xf9\x63\xea\x74\xac\xbd\x77\x2f\x3f\x17\x0b\x80\x9a\xc5\x7f\xf1\xf1\x98\xb1\x93\x1f\x2f\x06\x21\xdf\xc7\xd3\xbd\xf6\xc3\xaf\x6c\xe4\x1f\x87\xa4\x7d\x79\x6d\x41\x4a\x66\x37\xf1\xa1\x30\x7a\x3f\x29\x73\x67\x8b\xfd\xa7\x5d\x99\x1f\xae\x56\xf6\xdf\xe6\x33\xbc\xd5\x69\xf5\x57\x00\x00\x00\xff\xff\x78\x30\xec\x51\x0e\x2b\x00\x00") func dataConfig_schema_v30JsonBytes() ([]byte, error) { return bindataRead( @@ -89,7 +89,7 @@ func dataConfig_schema_v30Json() (*asset, error) { return a, nil } -var _dataConfig_schema_v31Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x1a\xcb\x8e\xdb\x36\xf0\xee\xaf\x10\x94\xdc\xe2\xdd\x4d\xd1\xa0\x40\x73\xeb\xb1\xa7\xf6\xdc\x85\x23\xd0\xd2\x58\x66\x96\x22\x19\x92\x72\xd6\x09\xfc\xef\x05\xf5\x32\x45\x91\x22\x6d\x2b\xd9\x45\xd1\xd3\xae\xc5\x99\xe1\xbc\x67\x38\xe4\xf7\x55\x92\xa4\x6f\x65\xbe\x87\x0a\xa5\x1f\x93\x74\xaf\x14\xff\xf8\xf0\xf0\x59\x32\x7a\xd7\x7e\xbd\x67\xa2\x7c\x28\x04\xda\xa9\xbb\xf7\x1f\x1e\xda\x6f\x6f\xd2\xb5\xc6\xc3\x85\x46\xc9\x19\xdd\xe1\x32\x6b\x57\xb2\xc3\xaf\xf7\xbf\xdc\x6b\xf4\x16\x44\x1d\x39\x68\x20\xb6\xfd\x0c\xb9\x6a\xbf\x09\xf8\x52\x63\x01\x1a\xf9\x31\x3d\x80\x90\x98\xd1\x74\xb3\x5e\xe9\x35\x2e\x18\x07\xa1\x30\xc8\xf4\x63\xa2\x99\x4b\x92\x01\xa4\xff\x60\x90\x95\x4a\x60\x5a\xa6\xcd\xe7\x53\x43\x21\x49\x52\x09\xe2\x80\x73\x83\xc2\xc0\xea\x9b\x87\x33\xfd\x87\x01\x6c\x6d\x53\x35\x98\x6d\xbe\x73\xa4\x14\x08\xfa\xf7\x94\xb7\x66\xf9\xd3\x23\xba\xfb\xf6\xc7\xdd\x3f\xef\xef\x7e\xbf\xcf\xee\x36\xef\xde\x8e\x96\xb5\x7e\x05\xec\xda\xed\x0b\xd8\x61\x8a\x15\x66\x74\xd8\x3f\x1d\x20\x4f\xdd\x7f\xa7\x61\x63\x54\x14\x0d\x30\x22\xa3\xbd\x77\x88\x48\x18\xcb\x4c\x41\x7d\x65\xe2\x29\x24\xf3\x00\xf6\x42\x32\x77\xfb\x3b\x64\x1e\x8b\x73\x60\xa4\xae\x82\x16\xec\xa1\x5e\x48\x98\x76\xfb\x65\xec\x27\x21\x17\xa0\xc2\x2e\xdb\x42\xbd\x98\xc7\xea\xed\x6f\x13\x78\xd5\x0b\x3d\x0b\xdb\x42\x18\x7b\x37\x0c\x8e\xc2\xdb\xa5\x2a\x57\x78\xf9\x75\x35\x28\xcb\xa3\xa5\x02\x38\x61\x47\xfd\xcd\xa3\x8f\x16\xa0\x02\xaa\xd2\x41\x05\x49\x92\x6e\x6b\x4c\x0a\x5b\xa3\x8c\xc2\x5f\x9a\xc4\xa3\xf1\x31\x49\xbe\xdb\x99\xcc\xa0\xd3\xac\x8f\x7e\xf9\x0d\x3e\xac\x7b\x64\x19\xd6\x73\x46\x15\x3c\xab\x46\xa8\xf9\xad\x5b\x15\xb0\xfc\x09\xc4\x0e\x13\x88\xc5\x40\xa2\x94\x33\x2a\x23\x58\xaa\x8c\x89\xac\xc0\xb9\x4a\x4f\x16\xfa\x84\x5e\xd8\x9f\x06\x54\xe3\xd7\x66\xe5\x20\x98\xe6\x88\x67\xa8\x28\x46\x72\x20\x21\xd0\x31\x5d\x27\x29\x56\x50\x49\xb7\x88\x49\x5a\x53\xfc\xa5\x86\x3f\x3b\x10\x25\x6a\xb0\xe9\x16\x82\xf1\xe5\x09\x97\x82\xd5\x3c\xe3\x48\x68\x07\x9b\x57\x7f\x9a\xb3\xaa\x42\x74\x29\xaf\xbb\x44\x8e\x08\xcd\x33\xaa\x10\xa6\x20\x32\x8a\xaa\x90\x23\xe9\xa8\x03\x5a\xc8\xac\x2d\xf8\xb3\x6e\xb4\xcb\x5a\x7c\x69\x11\x18\xaa\xff\xa2\xf6\x28\xe8\x9c\x63\xb7\x64\xb4\x6b\x6b\xde\x52\x0b\x31\x93\x80\x44\xbe\xbf\x12\x9f\x55\x08\xd3\x18\xdd\x01\x55\xe2\xc8\x19\x6e\xfd\xe5\xd5\x39\x02\xd0\x43\x36\xe4\x92\x8b\xd5\x00\xf4\x80\x05\xa3\x55\x1f\x0d\x31\x09\x66\x48\xf2\x1a\xff\x99\x33\x09\xb6\x62\x2c\x01\xcd\xa5\x41\xd4\x91\x4e\x7a\x8c\xc7\x5e\xf0\x75\x92\xd2\xba\xda\x82\xd0\x3d\xec\x08\x72\xc7\x44\x85\x34\xb3\xfd\xde\xc6\xf2\x48\xd3\x0e\xcf\x33\x15\x68\xca\xa0\xcb\x3a\x22\x19\xc1\xf4\x69\x79\x17\x87\x67\x25\x50\xb6\x67\x52\xc5\xe7\x70\x03\x7d\x0f\x88\xa8\x7d\xbe\x87\xfc\x69\x06\xdd\x84\x1a\x61\x33\xa9\x62\x9c\x1c\x57\xa8\x0c\x03\xf1\x3c\x04\x42\xd0\x16\xc8\x55\x72\x2e\xaa\x7c\x83\x2c\x2b\x4b\x0d\xea\xf3\xb8\x49\xe7\xd2\x2d\x87\x6a\x7e\x21\xf0\x01\x44\x6c\x01\x67\xfc\xdc\x70\xd9\x8b\xe1\x06\x24\x09\x77\x9f\x23\xd0\x4f\xf7\x6d\xf3\x39\x13\x55\xcd\x7f\x84\xa4\x1b\xbb\x5d\x48\xac\xba\xef\xfa\x62\x49\x18\xd7\x50\x8c\xac\x52\xa1\x5c\xf7\x0d\x02\xa4\xc7\xae\x67\xd0\xee\x74\x93\x55\xac\xf0\x39\xe8\x04\xd8\xd6\x8d\x37\x53\x5f\x5c\x08\x93\xab\xfa\xc7\x28\xd3\x05\x0f\x10\x01\x69\x7c\xec\xc5\xb2\x79\x66\x37\xec\x62\x0d\x1c\x22\x18\x49\x08\x07\xbb\x57\x91\x23\x6a\x98\x1f\x3e\x44\xfa\x84\x0b\xf7\xb7\x59\x5c\x0f\xaa\x97\x66\x7c\x8f\x1c\x20\x75\x66\xa5\x09\x37\x17\x23\x9b\x40\xb4\xfd\xe0\x16\x9e\xe3\xc2\x9f\x2b\x9a\x0c\x61\x06\x18\x67\x42\x4d\xa2\xeb\xe7\x94\xfb\x76\xeb\x9b\xab\x3d\x17\xf8\x80\x09\x94\x30\x3e\xb5\x6c\x19\x23\x80\xe8\x28\xf5\x08\x40\x45\xc6\x28\x39\x46\x40\x4a\x85\x44\xf0\x40\x21\x21\xaf\x05\x56\xc7\x8c\x71\xb5\x78\x9f\x21\xf7\x55\x26\xf1\x37\x18\x5b\xf3\x9c\xef\x3b\x42\x1b\x8b\x21\x6b\x42\x72\xa5\x41\x7d\x29\x29\x1c\xc6\x8e\x44\x18\x4c\x54\xe1\x14\x95\x4a\x56\x8b\x3c\xf6\x80\xad\xf7\x44\xa2\x84\xd8\x23\xbc\x76\xb7\x71\xd8\xcc\x03\x97\x97\x00\x4f\x0a\x5d\x67\xc2\x50\x55\xb6\x7f\x9b\x79\xe5\xe4\x0c\x7d\x79\x94\xb9\xba\xae\x5b\x93\xaa\xc0\x34\x63\x1c\x68\x30\x36\xa4\x62\x3c\x2b\x05\xca\x21\xe3\x20\x30\x73\xaa\x62\x6d\x46\x7a\x51\x0b\xa4\xf7\x9f\x92\x91\xb8\xa4\x88\x84\xc2\x4c\x55\x7c\x77\xe5\xb1\x52\xa9\x70\xb0\xd7\x04\x57\xd8\x1f\x34\x0e\xaf\x8d\xe8\x00\xda\xea\xef\x2e\xfa\x33\x05\xff\xcc\x29\xa6\x0a\x4a\xed\x26\x53\xa7\x9a\xe9\x39\xe7\x5b\xce\x88\x5e\x73\x8f\xc4\xd8\xa0\x33\x7c\x24\x6d\x60\xee\x94\x1b\xc1\xd5\x89\x3a\xf9\x1a\xdd\x75\x34\xf4\xd6\x1d\x23\x1b\x27\xfc\x45\xc5\xdc\x66\x63\xe3\xad\xa7\xee\xa0\xaa\x65\xf0\x58\xd0\xc0\x50\x39\xd7\xd2\x0e\xa0\xc6\xd0\x7e\xd1\x6a\xa1\xdb\x64\x1d\x04\x05\x76\x73\xbb\xb2\x24\xbb\x60\xec\x6e\x9d\x58\x7b\x02\xae\x79\xb2\x09\x1a\x9c\xbf\xcf\xcf\xb6\x3b\x20\xef\xdc\x19\x4b\xb4\xb5\x26\xae\xae\xe0\xd6\xde\x28\x0e\xe1\x1c\x23\x40\x09\x6c\xd9\xa5\x4f\xd4\x66\x3e\x01\xf9\x3a\xc7\x46\x0a\x57\xc0\x6a\x77\xc1\x5b\x99\xfe\xdd\x21\xa5\xc6\x5c\x3e\x60\x54\x03\xd2\xb6\xe9\xe3\x60\xd4\xbe\xbb\x0c\x1a\x2e\x26\x48\x04\x70\x82\x73\x24\x43\x89\xe8\x86\xf1\x44\xcd\x0b\xa4\x20\x6b\xef\x65\x2f\x4a\xfd\x33\x39\x9f\x23\x81\x08\x01\x82\x65\x15\x93\x43\xd3\x02\x08\x3a\x5e\x55\x3e\x1b\xf4\x1d\xc2\xa4\x16\x90\xa1\x5c\x75\x57\xbf\x01\x9f\x4b\x2b\x46\xb1\x62\xce\x0c\x11\xb7\x65\x85\x9e\xb3\x7e\xdb\x06\x24\xd4\xd9\x8c\x9b\xfa\xd8\xc9\x82\xe1\x09\x6d\xe3\x77\x59\x75\x9e\x31\xd1\xb9\xd6\x7b\x3c\xa6\xdf\x71\x22\xba\x00\xa9\x33\xc9\x30\xf8\x09\xe2\x07\x4b\x4b\x77\xca\xc8\x38\x23\x38\x3f\x2e\x25\x61\xce\x68\xab\xe4\x18\x87\xb8\xd1\x03\xb5\x3b\xe8\x56\xa8\xe2\x2a\x18\xac\x0d\xc2\x57\x4c\x0b\xf6\xf5\x82\x0d\x97\x73\x25\x4e\x50\x0e\x56\xbe\xbb\x55\xd1\x52\x09\x84\xa9\xba\xb8\x9c\xdf\x2a\xd6\x0d\xd5\x7c\xf0\xcf\x40\xd6\x1f\xe0\xc2\xf7\xe8\x9e\x4c\x9f\xf3\x3a\x38\x0d\xac\xa0\x62\xc2\xe9\x80\x0b\x3c\xf4\x08\x89\xd8\x83\x2d\x50\xd5\xa2\xc6\xc7\x1d\x54\xc6\xf8\xf2\xa7\x8d\xf0\x88\x78\x13\x4e\x48\x98\xa3\x6a\xa9\xe8\x88\x1e\xa8\xa7\xce\x1a\x9c\xcc\xcf\x2d\x12\xff\xec\x22\xc4\x75\x98\xf7\x0e\x42\xd6\x5b\xea\x19\x21\x4c\x4f\x19\xae\x5b\xfe\xf8\x63\xca\xc9\x7f\x28\xb9\x2d\xe9\xf5\x77\x61\x1e\xab\x3e\x0e\x3d\xf3\x7a\xd0\xd5\x26\xda\xc4\xde\x8b\xa8\xe5\xf8\x6f\xda\x77\x7b\x44\xe0\xea\xf3\x2f\xec\x04\x6f\x48\x2e\xdd\x8b\xa6\x40\x6e\xe9\xa0\xfe\x4f\x2d\xff\x11\x47\xfc\x79\xfe\xd5\x3d\x20\x0b\xbe\xdc\x6a\xa0\xae\x2e\xce\x11\xcf\x95\x5e\x81\xcd\x5e\xda\x14\xe3\xc1\xa2\x61\x92\xe9\x99\x7f\x4e\x93\xd1\xf7\x69\x1d\xc6\x66\xcc\x86\x0d\xe6\x78\xe3\x3b\xae\x90\x73\x83\xa4\x1e\xc4\x73\xbf\x62\x6d\xda\x29\x71\x5e\xf2\x05\x93\xcd\xfd\xbb\x99\x3e\x60\xee\xde\xfb\x07\x15\xd0\x05\x86\x74\x6e\x9b\x5a\x87\x87\x5e\xbb\xd3\x77\x9b\x9e\xf8\x37\xf0\x27\xaf\x38\xb5\x9c\xf4\x38\x99\x49\x7d\x1f\x0f\x5a\xdb\x17\x98\x9b\x91\x7e\x2c\x90\xf6\x15\x89\x91\xdd\x37\xe6\x79\xca\x67\x46\xe7\xdb\x4e\x7b\xcc\xdb\xbf\xb1\xf4\xdc\x6a\xac\xcc\xbf\xcd\x7b\xd8\xd5\x69\xf5\x6f\x00\x00\x00\xff\xff\xfc\xf3\x11\x6a\x88\x2f\x00\x00") +var _dataConfig_schema_v31Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x1a\xcb\x8e\xdb\x36\xf0\xee\xaf\x10\x94\xdc\xe2\xdd\x4d\xd1\xa0\x40\x73\xeb\xb1\xa7\xf6\xdc\x85\x23\xd0\xd2\x58\x66\x96\x22\x19\x92\x72\xd6\x09\xfc\xef\x05\xf5\x32\x45\x91\x22\x6d\x2b\xd9\x45\xd1\xd3\xae\xc5\x99\xe1\xbc\x67\x38\xe4\xf7\x55\x92\xa4\x6f\x65\xbe\x87\x0a\xa5\x1f\x93\x74\xaf\x14\xff\xf8\xf0\xf0\x59\x32\x7a\xd7\x7e\xbd\x67\xa2\x7c\x28\x04\xda\xa9\xbb\xf7\x1f\x1e\xda\x6f\x6f\xd2\xb5\xc6\xc3\x85\x46\xc9\x19\xdd\xe1\x32\x6b\x57\xb2\xc3\xaf\xf7\xbf\xdc\x6b\xf4\x16\x44\x1d\x39\x68\x20\xb6\xfd\x0c\xb9\x6a\xbf\x09\xf8\x52\x63\x01\x1a\xf9\x31\x3d\x80\x90\x98\xd1\x74\xb3\x5e\xe9\x35\x2e\x18\x07\xa1\x30\xc8\xf4\x63\xa2\x99\x4b\x92\x01\xa4\xff\x60\x90\x95\x4a\x60\x5a\xa6\xcd\xe7\x53\x43\x21\x49\x52\x09\xe2\x80\x73\x83\xc2\xc0\xea\x9b\x87\x33\xfd\x87\x01\x6c\x6d\x53\x35\x98\x6d\xbe\x73\xa4\x14\x08\xfa\xf7\x94\xb7\x66\xf9\xd3\x23\xba\xfb\xf6\xc7\xdd\x3f\xef\xef\x7e\xbf\xcf\xee\x36\xef\xde\x8e\x96\xb5\x7e\x05\xec\xda\xed\x0b\xd8\x61\x8a\x15\x66\x74\xd8\x3f\x1d\x20\x4f\xdd\x7f\xa7\x61\x63\x54\x14\x0d\x30\x22\xa3\xbd\x77\x88\x48\x18\xcb\x4c\x41\x7d\x65\xe2\x29\x24\xf3\x00\xf6\x42\x32\x77\xfb\x3b\x64\x1e\x8b\x73\x60\xa4\xae\x82\x16\xec\xa1\x5e\x48\x98\x76\xfb\x65\xec\x27\x21\x17\xa0\xc2\x2e\xdb\x42\xbd\x98\xc7\xea\xed\x6f\x13\x78\xd5\x0b\x3d\x0b\xdb\x42\x18\x7b\x37\x0c\x8e\xc2\xdb\xa5\x2a\x57\x78\xf9\x75\x35\x28\xcb\xa3\xa5\x02\x38\x61\x47\xfd\xcd\xa3\x8f\x16\xa0\x02\xaa\xd2\x41\x05\x49\x92\x6e\x6b\x4c\x0a\x5b\xa3\x8c\xc2\x5f\x9a\xc4\xa3\xf1\x31\x49\xbe\xdb\x99\xcc\xa0\xd3\xac\x8f\x7e\xf9\x0d\x3e\xac\x7b\x64\x19\xd6\x73\x46\x15\x3c\xab\x46\xa8\xf9\xad\x5b\x15\xb0\xfc\x09\xc4\x0e\x13\x88\xc5\x40\xa2\x94\x33\x2a\x23\x58\xaa\x8c\x89\xac\xc0\xb9\x4a\x4f\x16\xfa\x84\x5e\xd8\x9f\x06\x54\xe3\xd7\x66\xe5\x20\x98\xe6\x88\x67\xa8\x28\x46\x72\x20\x21\xd0\x31\x5d\x27\x29\x56\x50\x49\xb7\x88\x49\x5a\x53\xfc\xa5\x86\x3f\x3b\x10\x25\x6a\xb0\xe9\x16\x82\xf1\xe5\x09\x97\x82\xd5\x3c\xe3\x48\x68\x07\x9b\x57\x7f\x9a\xb3\xaa\x42\x74\x29\xaf\xbb\x44\x8e\x08\xcd\x33\xaa\x10\xa6\x20\x32\x8a\xaa\x90\x23\xe9\xa8\x03\x5a\xc8\xac\x2d\xf8\xb3\x6e\xb4\xcb\x5a\x7c\x69\x11\x18\xaa\xff\xa2\xf6\x28\xe8\x9c\x63\xb7\x64\xb4\x6b\x6b\xde\x52\x0b\x31\x93\x80\x44\xbe\xbf\x12\x9f\x55\x08\xd3\x18\xdd\x01\x55\xe2\xc8\x19\x6e\xfd\xe5\xd5\x39\x02\xd0\x43\x36\xe4\x92\x8b\xd5\x00\xf4\x80\x05\xa3\x55\x1f\x0d\x31\x09\x66\x48\xf2\x1a\xff\x99\x33\x09\xb6\x62\x2c\x01\xcd\xa5\x41\xd4\x91\x4e\x7a\x8c\xc7\x5e\xf0\x75\x92\xd2\xba\xda\x82\xd0\x3d\xec\x08\x72\xc7\x44\x85\x34\xb3\xfd\xde\xc6\xf2\x48\xd3\x0e\xcf\x33\x15\x68\xca\xa0\xcb\x3a\x22\x19\xc1\xf4\x69\x79\x17\x87\x67\x25\x50\xb6\x67\x52\xc5\xe7\x70\x03\x7d\x0f\x88\xa8\x7d\xbe\x87\xfc\x69\x06\xdd\x84\x1a\x61\x33\xa9\x62\x9c\x1c\x57\xa8\x0c\x03\xf1\x3c\x04\x42\xd0\x16\xc8\x55\x72\x2e\xaa\x7c\x83\x2c\x2b\x4b\x0d\xea\xf3\xb8\x49\xe7\xd2\x2d\x87\x6a\x7e\x21\xf0\x01\x44\x6c\x01\x67\xfc\xdc\x70\xd9\x8b\xe1\x06\x24\x09\x77\x9f\x23\xd0\x4f\xf7\x6d\xf3\x39\x13\x55\xcd\x7f\x84\xa4\x1b\xbb\x5d\x48\xac\xba\xef\xfa\x62\x49\x18\xd7\x50\x8c\xac\x52\xa1\x5c\xf7\x0d\x02\xa4\xc7\xae\x67\xd0\xee\x74\x93\x55\xac\xf0\x39\xe8\x04\xd8\xd6\x8d\x37\x53\x5f\x5c\x08\x93\xab\xfa\xc7\x28\xd3\x05\x0f\x10\x01\x69\x7c\xec\xc5\xb2\x79\x66\x37\xec\x62\x0d\x1c\x22\x18\x49\x08\x07\xbb\x57\x91\x23\x6a\x98\x1f\x3e\x44\xfa\x84\x0b\xf7\xb7\x59\x5c\x0f\xaa\x97\x66\x7c\x8f\x1c\x20\x75\x66\xa5\x09\x37\x17\x23\x9b\x40\xb4\xfd\xe0\x16\x9e\xe3\xc2\x9f\x2b\x9a\x0c\x61\x06\x18\x67\x42\x4d\xa2\xeb\xe7\x94\xfb\x76\xeb\x9b\xab\x3d\x17\xf8\x80\x09\x94\x30\x3e\xb5\x6c\x19\x23\x80\xe8\x28\xf5\x08\x40\x45\xc6\x28\x39\x46\x40\x4a\x85\x44\xf0\x40\x21\x21\xaf\x05\x56\xc7\x8c\x71\xb5\x78\x9f\x21\xf7\x55\x26\xf1\x37\x18\x5b\xf3\x9c\xef\x3b\x42\x1b\x8b\x21\x6b\x42\x72\xa5\x41\x7d\x29\x29\x1c\xc6\x8e\x44\x18\x4c\x54\xe1\x14\x95\x4a\x56\x8b\x3c\xf6\x80\xad\xf7\x44\xa2\x84\xd8\x23\xbc\x76\xb7\x71\xd8\xcc\x03\x97\x97\x00\x4f\x0a\x5d\x67\xc2\x50\x55\xb6\x7f\x9b\x79\xe5\xe4\x0c\x7d\x79\x94\xb9\xba\xae\x5b\x93\xaa\xc0\x34\x63\x1c\x68\x30\x36\xa4\x62\x3c\x2b\x05\xca\x21\xe3\x20\x30\x73\xaa\x62\x6d\x46\x7a\x51\x0b\xa4\xf7\x9f\x92\x91\xb8\xa4\x88\x84\xc2\x4c\x55\x7c\x77\xe5\xb1\x52\xa9\x70\xb0\xd7\x04\x57\xd8\x1f\x34\x0e\xaf\x8d\xe8\x00\xda\xea\xef\x2e\xfa\x33\x05\xff\xcc\x29\xa6\x0a\x4a\xed\x26\x53\xa7\x9a\xe9\x39\xe7\x5b\xce\x88\x5e\x73\x8f\xc4\xd8\xa0\x33\x7c\x24\x6d\x60\xee\x94\x1b\xc1\xd5\x89\x3a\xf9\x1a\xdd\x75\x34\xf4\xd6\x1d\x23\x1b\x27\xfc\x45\xc5\xdc\x66\x63\xe3\xad\xa7\xee\xa0\xaa\x65\xf0\x58\xd0\xc0\x50\x39\xd7\xd2\x0e\xa0\xc6\xd0\x7e\xd1\x6a\xa1\xdb\x64\x1d\x04\x05\x76\x73\xbb\xb2\x24\xbb\x60\xec\x6e\x9d\x58\x7b\x02\xae\x79\xb2\x09\x1a\x9c\xbf\xcf\xcf\xb6\x3b\x20\xef\xdc\x19\x4b\xb4\xb5\x26\xae\xae\xe0\xd6\xde\x28\x0e\xe1\x1c\x23\x40\x09\x6c\xd9\xa5\x4f\xd4\x66\x3e\x01\xf9\x3a\xc7\x46\x0a\x57\xc0\x6a\x77\xc1\x5b\x99\xfe\xdd\x21\xa5\xc6\x5c\x3e\x60\x54\x03\xd2\xb6\xe9\xe3\x60\xd4\xbe\xbb\x0c\x1a\x2e\x26\x48\x04\x70\x82\x73\x24\x43\x89\xe8\x86\xf1\x44\xcd\x0b\xa4\x20\x6b\xef\x65\x2f\x4a\xfd\x33\x39\x9f\x23\x81\x08\x01\x82\x65\x15\x93\x43\xd3\x02\x08\x3a\x5e\x55\x3e\x1b\xf4\x1d\xc2\xa4\x16\x90\xa1\x5c\x75\x57\xbf\x01\x9f\x4b\x2b\x46\xb1\x62\xce\x0c\x11\xb7\x65\x85\x9e\xb3\x7e\xdb\x06\x24\xd4\xd9\x8c\x9b\xfa\xd8\xc9\x82\xe1\x09\x6d\xe3\x77\x59\x75\x9e\x31\xd1\xb9\xd6\x7b\x3c\xa6\xdf\x71\x22\xba\x00\xa9\x33\xc9\x30\xf8\x09\xe2\x07\x4b\x4b\x77\xca\xc8\x38\x23\x38\x3f\x2e\x25\x61\xce\x68\xab\xe4\x18\x87\xb8\xd1\x03\xb5\x3b\xe8\x56\xa8\xe2\x2a\x18\xac\x0d\xc2\x57\x4c\x0b\xf6\xf5\x82\x0d\x97\x73\x25\x4e\x50\x0e\x56\xbe\xbb\x55\xd1\x52\x09\x84\xa9\xba\xb8\x9c\xdf\x2a\xd6\x0d\xd5\x7c\xf0\xcf\x40\xd6\x1f\xe0\xc2\xf7\xe8\x9e\x4c\x9f\xf3\x3a\x38\x0d\xac\xa0\x62\xc2\xe9\x80\x0b\x3c\xf4\x08\x89\xd8\x83\x2d\x50\xd5\xa2\xc6\xc7\x1d\x54\xc6\xf8\xf2\xa7\x8d\xf0\x88\x78\x13\x4e\x48\x98\xa3\x6a\xa9\xe8\x88\x1e\xa8\xa7\xce\x1a\x9c\xcc\xcf\x2d\x12\xff\xec\x22\xc4\x75\x98\xf7\x0e\x42\xd6\x5b\xea\x19\x21\x4c\x4f\x19\xae\x5b\xfe\xf8\x63\xca\xc9\x7f\x28\xb9\x2d\xe9\xf5\x77\x61\x1e\xab\x3e\x0e\x3d\xf3\x7a\xd0\xd5\x26\xda\xc4\xde\x8b\xa8\xe5\xf8\x6f\xda\x77\x7b\x44\xe0\xea\xf3\x91\x52\x28\xdf\x47\x1d\x09\x2e\x6c\x1a\x6f\xc8\x43\xdd\xe3\xa7\x40\x1a\xea\xa0\xfe\xcf\x42\xff\x11\x9f\xfd\x79\xfe\xd5\xbd\x35\x0b\x3e\xf2\x6a\xa0\xae\xae\xe3\x11\x2f\x9b\x5e\x81\xcd\x5e\xda\x14\xe3\x19\xa4\x61\x92\xe9\x78\x60\x4e\x93\xd1\x57\x6f\x1d\xc6\x66\xcc\x86\x0d\xe6\x78\x0e\x3c\x2e\xa6\x73\x33\xa7\x1e\xc4\x73\x15\x63\x6d\xda\x29\x71\x5e\xf2\x05\x93\xcd\xfd\xbb\x99\x96\x61\xee\x8a\xfc\x07\xd5\xda\x05\xe6\x79\x6e\x9b\x5a\xe7\x8c\x5e\xbb\xd3\x27\x9e\x9e\xf8\x37\xf0\x27\x0f\x3e\xb5\x9c\xf4\x38\x19\x5f\x7d\x1f\xcf\x64\xdb\xc7\x9a\x9b\x91\x7e\x2c\x90\xf6\xc1\x89\x91\xdd\x37\xe6\xd1\xcb\x67\x46\xe7\x33\x50\x7b\x22\xdc\x3f\xc7\xf4\x5c\x80\xac\xcc\xbf\xcd\xd3\xd9\xd5\x69\xf5\x6f\x00\x00\x00\xff\xff\x3e\x1e\x04\x4e\xb3\x2f\x00\x00") func dataConfig_schema_v31JsonBytes() ([]byte, error) { return bindataRead( diff --git a/components/cli/compose/schema/data/config_schema_v3.1.json b/components/cli/compose/schema/data/config_schema_v3.1.json index b7037485f9..f76154ea12 100644 --- a/components/cli/compose/schema/data/config_schema_v3.1.json +++ b/components/cli/compose/schema/data/config_schema_v3.1.json @@ -338,6 +338,7 @@ "additionalProperties": false }, "internal": {"type": "boolean"}, + "attachable": {"type": "boolean"}, "labels": {"$ref": "#/definitions/list_or_dict"} }, "additionalProperties": false diff --git a/components/cli/compose/types/types.go b/components/cli/compose/types/types.go index 3b9a2b2a08..4bb5cb6d2d 100644 --- a/components/cli/compose/types/types.go +++ b/components/cli/compose/types/types.go @@ -238,6 +238,7 @@ type NetworkConfig struct { Ipam IPAMConfig External External Internal bool + Attachable bool Labels MappingWithEquals } From 3a4832fb11cd5d9be77475d783ec41e86322d525 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 5 Feb 2017 08:55:30 -0800 Subject: [PATCH 580/978] Fix Windows `docker stats` showing Linux headers This fix is an attempt to fix issue raised in #28005 where `docker stats` on Windows shows Linux headers if there is no containers in stats. The reason for the issue is that, in case there is no container, a header is faked in: https://github.com/docker/docker/blob/v1.13.0/cli/command/formatter/formatter.go#L74-L78 which does not know OS type information (as OS was stored with container stat entries) This fix tries to fix the issue by moving OS type information to stats context (instead of individual container stats entry). Additional unit tests have been added. This fix fixes #28005. Signed-off-by: Yong Tang Upstream-commit: af80020ef256edda93b223482d05f41fdd10038a Component: cli --- components/cli/command/container/stats.go | 2 +- components/cli/command/formatter/stats.go | 22 ++--- .../cli/command/formatter/stats_test.go | 83 +++++++++++++------ 3 files changed, 71 insertions(+), 36 deletions(-) diff --git a/components/cli/command/container/stats.go b/components/cli/command/container/stats.go index 593db27b2a..940a039143 100644 --- a/components/cli/command/container/stats.go +++ b/components/cli/command/container/stats.go @@ -213,7 +213,7 @@ func runStats(dockerCli *command.DockerCli, opts *statsOptions) error { ccstats = append(ccstats, c.GetStatistics()) } cStats.mu.Unlock() - if err = formatter.ContainerStatsWrite(statsCtx, ccstats); err != nil { + if err = formatter.ContainerStatsWrite(statsCtx, ccstats, daemonOSType); err != nil { break } if len(cStats.cs) == 0 && !showAll { diff --git a/components/cli/command/formatter/stats.go b/components/cli/command/formatter/stats.go index a37e9d7923..7302bca010 100644 --- a/components/cli/command/formatter/stats.go +++ b/components/cli/command/formatter/stats.go @@ -37,7 +37,6 @@ type StatsEntry struct { BlockWrite float64 PidsCurrent uint64 // Not used on Windows IsInvalid bool - OSType string } // ContainerStats represents an entity to store containers statistics synchronously @@ -88,7 +87,6 @@ func (cs *ContainerStats) SetStatistics(s StatsEntry) { cs.mutex.Lock() defer cs.mutex.Unlock() s.Container = cs.Container - s.OSType = cs.OSType cs.StatsEntry = s } @@ -113,16 +111,17 @@ func NewStatsFormat(source, osType string) Format { // NewContainerStats returns a new ContainerStats entity and sets in it the given name func NewContainerStats(container, osType string) *ContainerStats { return &ContainerStats{ - StatsEntry: StatsEntry{Container: container, OSType: osType}, + StatsEntry: StatsEntry{Container: container}, } } // ContainerStatsWrite renders the context for a list of containers statistics -func ContainerStatsWrite(ctx Context, containerStats []StatsEntry) error { +func ContainerStatsWrite(ctx Context, containerStats []StatsEntry, osType string) error { render := func(format func(subContext subContext) error) error { for _, cstats := range containerStats { containerStatsCtx := &containerStatsContext{ - s: cstats, + s: cstats, + os: osType, } if err := format(containerStatsCtx); err != nil { return err @@ -130,12 +129,13 @@ func ContainerStatsWrite(ctx Context, containerStats []StatsEntry) error { } return nil } - return ctx.Write(&containerStatsContext{}, render) + return ctx.Write(&containerStatsContext{os: osType}, render) } type containerStatsContext struct { HeaderContext - s StatsEntry + s StatsEntry + os string } func (c *containerStatsContext) MarshalJSON() ([]byte, error) { @@ -168,14 +168,14 @@ func (c *containerStatsContext) CPUPerc() string { func (c *containerStatsContext) MemUsage() string { header := memUseHeader - if c.s.OSType == winOSType { + if c.os == winOSType { header = winMemUseHeader } c.AddHeader(header) if c.s.IsInvalid { return fmt.Sprintf("-- / --") } - if c.s.OSType == winOSType { + if c.os == winOSType { return fmt.Sprintf("%s", units.BytesSize(c.s.Memory)) } return fmt.Sprintf("%s / %s", units.BytesSize(c.s.Memory), units.BytesSize(c.s.MemoryLimit)) @@ -184,7 +184,7 @@ func (c *containerStatsContext) MemUsage() string { func (c *containerStatsContext) MemPerc() string { header := memPercHeader c.AddHeader(header) - if c.s.IsInvalid || c.s.OSType == winOSType { + if c.s.IsInvalid || c.os == winOSType { return fmt.Sprintf("--") } return fmt.Sprintf("%.2f%%", c.s.MemoryPercentage) @@ -208,7 +208,7 @@ func (c *containerStatsContext) BlockIO() string { func (c *containerStatsContext) PIDs() string { c.AddHeader(pidsHeader) - if c.s.IsInvalid || c.s.OSType == winOSType { + if c.s.IsInvalid || c.os == winOSType { return fmt.Sprintf("--") } return fmt.Sprintf("%d", c.s.PidsCurrent) diff --git a/components/cli/command/formatter/stats_test.go b/components/cli/command/formatter/stats_test.go index d5a17cc70e..f9ecda33ec 100644 --- a/components/cli/command/formatter/stats_test.go +++ b/components/cli/command/formatter/stats_test.go @@ -14,30 +14,31 @@ func TestContainerStatsContext(t *testing.T) { var ctx containerStatsContext tt := []struct { stats StatsEntry + osType string expValue string expHeader string call func() string }{ - {StatsEntry{Container: containerID}, containerID, containerHeader, ctx.Container}, - {StatsEntry{CPUPercentage: 5.5}, "5.50%", cpuPercHeader, ctx.CPUPerc}, - {StatsEntry{CPUPercentage: 5.5, IsInvalid: true}, "--", cpuPercHeader, ctx.CPUPerc}, - {StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3}, "0.31 B / 12.3 B", netIOHeader, ctx.NetIO}, - {StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3, IsInvalid: true}, "--", netIOHeader, ctx.NetIO}, - {StatsEntry{BlockRead: 0.1, BlockWrite: 2.3}, "0.1 B / 2.3 B", blockIOHeader, ctx.BlockIO}, - {StatsEntry{BlockRead: 0.1, BlockWrite: 2.3, IsInvalid: true}, "--", blockIOHeader, ctx.BlockIO}, - {StatsEntry{MemoryPercentage: 10.2}, "10.20%", memPercHeader, ctx.MemPerc}, - {StatsEntry{MemoryPercentage: 10.2, IsInvalid: true}, "--", memPercHeader, ctx.MemPerc}, - {StatsEntry{MemoryPercentage: 10.2, OSType: "windows"}, "--", memPercHeader, ctx.MemPerc}, - {StatsEntry{Memory: 24, MemoryLimit: 30}, "24 B / 30 B", memUseHeader, ctx.MemUsage}, - {StatsEntry{Memory: 24, MemoryLimit: 30, IsInvalid: true}, "-- / --", memUseHeader, ctx.MemUsage}, - {StatsEntry{Memory: 24, MemoryLimit: 30, OSType: "windows"}, "24 B", winMemUseHeader, ctx.MemUsage}, - {StatsEntry{PidsCurrent: 10}, "10", pidsHeader, ctx.PIDs}, - {StatsEntry{PidsCurrent: 10, IsInvalid: true}, "--", pidsHeader, ctx.PIDs}, - {StatsEntry{PidsCurrent: 10, OSType: "windows"}, "--", pidsHeader, ctx.PIDs}, + {StatsEntry{Container: containerID}, "", containerID, containerHeader, ctx.Container}, + {StatsEntry{CPUPercentage: 5.5}, "", "5.50%", cpuPercHeader, ctx.CPUPerc}, + {StatsEntry{CPUPercentage: 5.5, IsInvalid: true}, "", "--", cpuPercHeader, ctx.CPUPerc}, + {StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3}, "", "0.31 B / 12.3 B", netIOHeader, ctx.NetIO}, + {StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3, IsInvalid: true}, "", "--", netIOHeader, ctx.NetIO}, + {StatsEntry{BlockRead: 0.1, BlockWrite: 2.3}, "", "0.1 B / 2.3 B", blockIOHeader, ctx.BlockIO}, + {StatsEntry{BlockRead: 0.1, BlockWrite: 2.3, IsInvalid: true}, "", "--", blockIOHeader, ctx.BlockIO}, + {StatsEntry{MemoryPercentage: 10.2}, "", "10.20%", memPercHeader, ctx.MemPerc}, + {StatsEntry{MemoryPercentage: 10.2, IsInvalid: true}, "", "--", memPercHeader, ctx.MemPerc}, + {StatsEntry{MemoryPercentage: 10.2}, "windows", "--", memPercHeader, ctx.MemPerc}, + {StatsEntry{Memory: 24, MemoryLimit: 30}, "", "24 B / 30 B", memUseHeader, ctx.MemUsage}, + {StatsEntry{Memory: 24, MemoryLimit: 30, IsInvalid: true}, "", "-- / --", memUseHeader, ctx.MemUsage}, + {StatsEntry{Memory: 24, MemoryLimit: 30}, "windows", "24 B", winMemUseHeader, ctx.MemUsage}, + {StatsEntry{PidsCurrent: 10}, "", "10", pidsHeader, ctx.PIDs}, + {StatsEntry{PidsCurrent: 10, IsInvalid: true}, "", "--", pidsHeader, ctx.PIDs}, + {StatsEntry{PidsCurrent: 10}, "windows", "--", pidsHeader, ctx.PIDs}, } for _, te := range tt { - ctx = containerStatsContext{s: te.stats} + ctx = containerStatsContext{s: te.stats, os: te.osType} if v := te.call(); v != te.expValue { t.Fatalf("Expected %q, got %q", te.expValue, v) } @@ -93,7 +94,6 @@ container2 -- BlockWrite: 20, PidsCurrent: 2, IsInvalid: false, - OSType: "linux", }, { Container: "container2", @@ -107,12 +107,11 @@ container2 -- BlockWrite: 30, PidsCurrent: 3, IsInvalid: true, - OSType: "linux", }, } var out bytes.Buffer te.context.Output = &out - err := ContainerStatsWrite(te.context, stats) + err := ContainerStatsWrite(te.context, stats, "linux") if err != nil { assert.Error(t, err, te.expected) } else { @@ -161,7 +160,6 @@ container2 -- -- BlockWrite: 20, PidsCurrent: 2, IsInvalid: false, - OSType: "windows", }, { Container: "container2", @@ -175,12 +173,11 @@ container2 -- -- BlockWrite: 30, PidsCurrent: 3, IsInvalid: true, - OSType: "windows", }, } var out bytes.Buffer te.context.Output = &out - err := ContainerStatsWrite(te.context, stats) + err := ContainerStatsWrite(te.context, stats, "windows") if err != nil { assert.Error(t, err, te.expected) } else { @@ -220,9 +217,47 @@ func TestContainerStatsContextWriteWithNoStats(t *testing.T) { } for _, context := range contexts { - ContainerStatsWrite(context.context, []StatsEntry{}) + ContainerStatsWrite(context.context, []StatsEntry{}, "linux") assert.Equal(t, context.expected, out.String()) // Clean buffer out.Reset() } } + +func TestContainerStatsContextWriteWithNoStatsWindows(t *testing.T) { + var out bytes.Buffer + + contexts := []struct { + context Context + expected string + }{ + { + Context{ + Format: "{{.Container}}", + Output: &out, + }, + "", + }, + { + Context{ + Format: "table {{.Container}}\t{{.MemUsage}}", + Output: &out, + }, + "CONTAINER PRIV WORKING SET\n", + }, + { + Context{ + Format: "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}", + Output: &out, + }, + "CONTAINER CPU % PRIV WORKING SET\n", + }, + } + + for _, context := range contexts { + ContainerStatsWrite(context.context, []StatsEntry{}, "windows") + assert.Equal(t, out.String(), context.expected) + // Clean buffer + out.Reset() + } +} From b7dbd9db5d2a65bb6f5c5e01ed7052e44825aee1 Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Fri, 3 Feb 2017 16:07:04 -0800 Subject: [PATCH 581/978] Fix "command.PromptForConfirmation" to accept "enter" for the "N" default This adjusts `command.PromptForConfirmation` in `cli/command/utils.go` to use `bufio`'s `ReadLine` rather than using `fmt.Fscan` for reading input, which makes `` properly accept the default value of "No" as one would expect. This new code actually came from `cli/command/plugin/install.go`'s `acceptPrivileges` function, which I've also refactored here to use `command.PromptForConfirmation` as it should. Additionally, this updates `cli/command/plugin/upgrade.go`'s `runUpgrade` function to use `command.PromptForConfirmation` for further consistency. Signed-off-by: Andrew "Tianon" Page Upstream-commit: 21c0b6c0e6143d8228ad84d48741a66cac8d32d8 Component: cli --- components/cli/command/plugin/install.go | 10 +--------- components/cli/command/plugin/upgrade.go | 13 +------------ components/cli/command/utils.go | 11 ++++------- 3 files changed, 6 insertions(+), 28 deletions(-) diff --git a/components/cli/command/plugin/install.go b/components/cli/command/plugin/install.go index 631917a07c..15877761af 100644 --- a/components/cli/command/plugin/install.go +++ b/components/cli/command/plugin/install.go @@ -1,7 +1,6 @@ package plugin import ( - "bufio" "errors" "fmt" "strings" @@ -178,13 +177,6 @@ func acceptPrivileges(dockerCli *command.DockerCli, name string) func(privileges for _, privilege := range privileges { fmt.Fprintf(dockerCli.Out(), " - %s: %v\n", privilege.Name, privilege.Value) } - - fmt.Fprint(dockerCli.Out(), "Do you grant the above permissions? [y/N] ") - reader := bufio.NewReader(dockerCli.In()) - line, _, err := reader.ReadLine() - if err != nil { - return false, err - } - return strings.ToLower(string(line)) == "y", nil + return command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), "Do you grant the above permissions?"), nil } } diff --git a/components/cli/command/plugin/upgrade.go b/components/cli/command/plugin/upgrade.go index d212cd7e52..6861aa1b32 100644 --- a/components/cli/command/plugin/upgrade.go +++ b/components/cli/command/plugin/upgrade.go @@ -1,7 +1,6 @@ package plugin import ( - "bufio" "context" "fmt" "strings" @@ -64,17 +63,7 @@ func runUpgrade(dockerCli *command.DockerCli, opts pluginOptions) error { fmt.Fprintf(dockerCli.Out(), "Upgrading plugin %s from %s to %s\n", p.Name, old, remote) if !opts.skipRemoteCheck && remote.String() != old.String() { - _, err := fmt.Fprint(dockerCli.Out(), "Plugin images do not match, are you sure? ") - if err != nil { - return errors.Wrap(err, "error writing to stdout") - } - - rdr := bufio.NewReader(dockerCli.In()) - line, _, err := rdr.ReadLine() - if err != nil { - return errors.Wrap(err, "error reading from stdin") - } - if strings.ToLower(string(line)) != "y" { + if !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), "Plugin images do not match, are you sure?") { return errors.New("canceling upgrade request") } } diff --git a/components/cli/command/utils.go b/components/cli/command/utils.go index f9255cf87f..4c52ce61b2 100644 --- a/components/cli/command/utils.go +++ b/components/cli/command/utils.go @@ -1,6 +1,7 @@ package command import ( + "bufio" "fmt" "io" "os" @@ -80,11 +81,7 @@ func PromptForConfirmation(ins *InStream, outs *OutStream, message string) bool ins = NewInStream(os.Stdin) } - answer := "" - n, _ := fmt.Fscan(ins, &answer) - if n != 1 || (answer != "y" && answer != "Y") { - return false - } - - return true + reader := bufio.NewReader(ins) + answer, _, _ := reader.ReadLine() + return strings.ToLower(string(answer)) == "y" } From 5097259d1912f546d94cddae9dae41d9af07a5f5 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 6 Feb 2017 14:16:03 +0100 Subject: [PATCH 582/978] Make sure we validate simple syntax on service commands We ignored errors for simple syntax in `PortOpt` (missed that in the previous migration of this code). This make sure we don't ignore `nat.Parse` errors. Test has been migrate too (errors are not exactly the same as before though -_-) Signed-off-by: Vincent Demeester Upstream-commit: 6f8f1d20a2a0c87ebf6dcaca6bb1816e286b15e2 Component: cli --- components/cli/command/service/opts.go | 12 ---------- components/cli/command/service/update.go | 18 --------------- components/cli/command/service/update_test.go | 23 ------------------- 3 files changed, 53 deletions(-) diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index f2470673a6..9a0ae64ca9 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -10,7 +10,6 @@ import ( "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/opts" runconfigopts "github.com/docker/docker/runconfig/opts" - "github.com/docker/go-connections/nat" "github.com/spf13/cobra" ) @@ -244,17 +243,6 @@ func (opts *healthCheckOptions) toHealthConfig() (*container.HealthConfig, error return healthConfig, nil } -// ValidatePort validates a string is in the expected format for a port definition -func ValidatePort(value string) (string, error) { - portMappings, err := nat.ParsePortSpec(value) - for _, portMapping := range portMappings { - if portMapping.Binding.HostIP != "" { - return "", fmt.Errorf("HostIP is not supported by a service.") - } - } - return value, err -} - // convertExtraHostsToSwarmHosts converts an array of extra hosts in cli // : // into a swarmkit host format: diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index 57a4577f88..7f461c90a9 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -663,24 +663,6 @@ func portConfigToString(portConfig *swarm.PortConfig) string { return fmt.Sprintf("%v:%v/%s/%s", portConfig.PublishedPort, portConfig.TargetPort, protocol, mode) } -// FIXME(vdemeester) port to opts.PortOpt -// This validation is only used for `--publish-rm`. -// The `--publish-rm` takes: -// [/] (e.g., 80, 80/tcp, 53/udp) -func validatePublishRemove(val string) (string, error) { - proto, port := nat.SplitProtoPort(val) - if proto != "tcp" && proto != "udp" { - return "", fmt.Errorf("invalid protocol '%s' for %s", proto, val) - } - if strings.Contains(port, ":") { - return "", fmt.Errorf("invalid port format: '%s', should be [/] (e.g., 80, 80/tcp, 53/udp)", port) - } - if _, err := nat.ParsePort(port); err != nil { - return "", err - } - return val, nil -} - func updatePorts(flags *pflag.FlagSet, portConfig *[]swarm.PortConfig) error { // The key of the map is `port/protocol`, e.g., `80/tcp` portSet := map[string]swarm.PortConfig{} diff --git a/components/cli/command/service/update_test.go b/components/cli/command/service/update_test.go index 992ae9ef3b..f2887e229d 100644 --- a/components/cli/command/service/update_test.go +++ b/components/cli/command/service/update_test.go @@ -362,29 +362,6 @@ func TestUpdatePortsRmWithProtocol(t *testing.T) { assert.Equal(t, portConfigs[1].TargetPort, uint32(82)) } -// FIXME(vdemeester) port to opts.PortOpt -func TestValidatePort(t *testing.T) { - validPorts := []string{"80/tcp", "80", "80/udp"} - invalidPorts := map[string]string{ - "9999999": "out of range", - "80:80/tcp": "invalid port format", - "53:53/udp": "invalid port format", - "80:80": "invalid port format", - "80/xyz": "invalid protocol", - "tcp": "invalid syntax", - "udp": "invalid syntax", - "": "invalid protocol", - } - for _, port := range validPorts { - _, err := validatePublishRemove(port) - assert.Equal(t, err, nil) - } - for port, e := range invalidPorts { - _, err := validatePublishRemove(port) - assert.Error(t, err, e) - } -} - type secretAPIClientMock struct { listResult []swarm.Secret } From af8335c602e9256f97ca1ea8ac1eee1b58272af9 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Tue, 31 Jan 2017 12:44:05 -0800 Subject: [PATCH 583/978] Add expanded port syntax to Compose schema and types. This commit adds expanded port syntax to Compose schema and types so that it is possible to have ``` ports: - mode: host target: 80 published: 9005 ``` Signed-off-by: Yong Tang Upstream-commit: c69e0f7dd54777a5f348e395284d4aef1fc4cec0 Component: cli --- components/cli/compose/schema/bindata.go | 2 +- .../compose/schema/data/config_schema_v3.1.json | 16 ++++++++++++++-- components/cli/compose/types/types.go | 10 +++++++++- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/components/cli/compose/schema/bindata.go b/components/cli/compose/schema/bindata.go index bb91fbfa5f..0b5aa18b75 100644 --- a/components/cli/compose/schema/bindata.go +++ b/components/cli/compose/schema/bindata.go @@ -89,7 +89,7 @@ func dataConfig_schema_v30Json() (*asset, error) { return a, nil } -var _dataConfig_schema_v31Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x1a\xcb\x8e\xdb\x36\xf0\xee\xaf\x10\x94\xdc\xe2\xdd\x4d\xd1\xa0\x40\x73\xeb\xb1\xa7\xf6\xdc\x85\x23\xd0\xd2\x58\x66\x96\x22\x19\x92\x72\xd6\x09\xfc\xef\x05\xf5\x32\x45\x91\x22\x6d\x2b\xd9\x45\xd1\xd3\xae\xc5\x99\xe1\xbc\x67\x38\xe4\xf7\x55\x92\xa4\x6f\x65\xbe\x87\x0a\xa5\x1f\x93\x74\xaf\x14\xff\xf8\xf0\xf0\x59\x32\x7a\xd7\x7e\xbd\x67\xa2\x7c\x28\x04\xda\xa9\xbb\xf7\x1f\x1e\xda\x6f\x6f\xd2\xb5\xc6\xc3\x85\x46\xc9\x19\xdd\xe1\x32\x6b\x57\xb2\xc3\xaf\xf7\xbf\xdc\x6b\xf4\x16\x44\x1d\x39\x68\x20\xb6\xfd\x0c\xb9\x6a\xbf\x09\xf8\x52\x63\x01\x1a\xf9\x31\x3d\x80\x90\x98\xd1\x74\xb3\x5e\xe9\x35\x2e\x18\x07\xa1\x30\xc8\xf4\x63\xa2\x99\x4b\x92\x01\xa4\xff\x60\x90\x95\x4a\x60\x5a\xa6\xcd\xe7\x53\x43\x21\x49\x52\x09\xe2\x80\x73\x83\xc2\xc0\xea\x9b\x87\x33\xfd\x87\x01\x6c\x6d\x53\x35\x98\x6d\xbe\x73\xa4\x14\x08\xfa\xf7\x94\xb7\x66\xf9\xd3\x23\xba\xfb\xf6\xc7\xdd\x3f\xef\xef\x7e\xbf\xcf\xee\x36\xef\xde\x8e\x96\xb5\x7e\x05\xec\xda\xed\x0b\xd8\x61\x8a\x15\x66\x74\xd8\x3f\x1d\x20\x4f\xdd\x7f\xa7\x61\x63\x54\x14\x0d\x30\x22\xa3\xbd\x77\x88\x48\x18\xcb\x4c\x41\x7d\x65\xe2\x29\x24\xf3\x00\xf6\x42\x32\x77\xfb\x3b\x64\x1e\x8b\x73\x60\xa4\xae\x82\x16\xec\xa1\x5e\x48\x98\x76\xfb\x65\xec\x27\x21\x17\xa0\xc2\x2e\xdb\x42\xbd\x98\xc7\xea\xed\x6f\x13\x78\xd5\x0b\x3d\x0b\xdb\x42\x18\x7b\x37\x0c\x8e\xc2\xdb\xa5\x2a\x57\x78\xf9\x75\x35\x28\xcb\xa3\xa5\x02\x38\x61\x47\xfd\xcd\xa3\x8f\x16\xa0\x02\xaa\xd2\x41\x05\x49\x92\x6e\x6b\x4c\x0a\x5b\xa3\x8c\xc2\x5f\x9a\xc4\xa3\xf1\x31\x49\xbe\xdb\x99\xcc\xa0\xd3\xac\x8f\x7e\xf9\x0d\x3e\xac\x7b\x64\x19\xd6\x73\x46\x15\x3c\xab\x46\xa8\xf9\xad\x5b\x15\xb0\xfc\x09\xc4\x0e\x13\x88\xc5\x40\xa2\x94\x33\x2a\x23\x58\xaa\x8c\x89\xac\xc0\xb9\x4a\x4f\x16\xfa\x84\x5e\xd8\x9f\x06\x54\xe3\xd7\x66\xe5\x20\x98\xe6\x88\x67\xa8\x28\x46\x72\x20\x21\xd0\x31\x5d\x27\x29\x56\x50\x49\xb7\x88\x49\x5a\x53\xfc\xa5\x86\x3f\x3b\x10\x25\x6a\xb0\xe9\x16\x82\xf1\xe5\x09\x97\x82\xd5\x3c\xe3\x48\x68\x07\x9b\x57\x7f\x9a\xb3\xaa\x42\x74\x29\xaf\xbb\x44\x8e\x08\xcd\x33\xaa\x10\xa6\x20\x32\x8a\xaa\x90\x23\xe9\xa8\x03\x5a\xc8\xac\x2d\xf8\xb3\x6e\xb4\xcb\x5a\x7c\x69\x11\x18\xaa\xff\xa2\xf6\x28\xe8\x9c\x63\xb7\x64\xb4\x6b\x6b\xde\x52\x0b\x31\x93\x80\x44\xbe\xbf\x12\x9f\x55\x08\xd3\x18\xdd\x01\x55\xe2\xc8\x19\x6e\xfd\xe5\xd5\x39\x02\xd0\x43\x36\xe4\x92\x8b\xd5\x00\xf4\x80\x05\xa3\x55\x1f\x0d\x31\x09\x66\x48\xf2\x1a\xff\x99\x33\x09\xb6\x62\x2c\x01\xcd\xa5\x41\xd4\x91\x4e\x7a\x8c\xc7\x5e\xf0\x75\x92\xd2\xba\xda\x82\xd0\x3d\xec\x08\x72\xc7\x44\x85\x34\xb3\xfd\xde\xc6\xf2\x48\xd3\x0e\xcf\x33\x15\x68\xca\xa0\xcb\x3a\x22\x19\xc1\xf4\x69\x79\x17\x87\x67\x25\x50\xb6\x67\x52\xc5\xe7\x70\x03\x7d\x0f\x88\xa8\x7d\xbe\x87\xfc\x69\x06\xdd\x84\x1a\x61\x33\xa9\x62\x9c\x1c\x57\xa8\x0c\x03\xf1\x3c\x04\x42\xd0\x16\xc8\x55\x72\x2e\xaa\x7c\x83\x2c\x2b\x4b\x0d\xea\xf3\xb8\x49\xe7\xd2\x2d\x87\x6a\x7e\x21\xf0\x01\x44\x6c\x01\x67\xfc\xdc\x70\xd9\x8b\xe1\x06\x24\x09\x77\x9f\x23\xd0\x4f\xf7\x6d\xf3\x39\x13\x55\xcd\x7f\x84\xa4\x1b\xbb\x5d\x48\xac\xba\xef\xfa\x62\x49\x18\xd7\x50\x8c\xac\x52\xa1\x5c\xf7\x0d\x02\xa4\xc7\xae\x67\xd0\xee\x74\x93\x55\xac\xf0\x39\xe8\x04\xd8\xd6\x8d\x37\x53\x5f\x5c\x08\x93\xab\xfa\xc7\x28\xd3\x05\x0f\x10\x01\x69\x7c\xec\xc5\xb2\x79\x66\x37\xec\x62\x0d\x1c\x22\x18\x49\x08\x07\xbb\x57\x91\x23\x6a\x98\x1f\x3e\x44\xfa\x84\x0b\xf7\xb7\x59\x5c\x0f\xaa\x97\x66\x7c\x8f\x1c\x20\x75\x66\xa5\x09\x37\x17\x23\x9b\x40\xb4\xfd\xe0\x16\x9e\xe3\xc2\x9f\x2b\x9a\x0c\x61\x06\x18\x67\x42\x4d\xa2\xeb\xe7\x94\xfb\x76\xeb\x9b\xab\x3d\x17\xf8\x80\x09\x94\x30\x3e\xb5\x6c\x19\x23\x80\xe8\x28\xf5\x08\x40\x45\xc6\x28\x39\x46\x40\x4a\x85\x44\xf0\x40\x21\x21\xaf\x05\x56\xc7\x8c\x71\xb5\x78\x9f\x21\xf7\x55\x26\xf1\x37\x18\x5b\xf3\x9c\xef\x3b\x42\x1b\x8b\x21\x6b\x42\x72\xa5\x41\x7d\x29\x29\x1c\xc6\x8e\x44\x18\x4c\x54\xe1\x14\x95\x4a\x56\x8b\x3c\xf6\x80\xad\xf7\x44\xa2\x84\xd8\x23\xbc\x76\xb7\x71\xd8\xcc\x03\x97\x97\x00\x4f\x0a\x5d\x67\xc2\x50\x55\xb6\x7f\x9b\x79\xe5\xe4\x0c\x7d\x79\x94\xb9\xba\xae\x5b\x93\xaa\xc0\x34\x63\x1c\x68\x30\x36\xa4\x62\x3c\x2b\x05\xca\x21\xe3\x20\x30\x73\xaa\x62\x6d\x46\x7a\x51\x0b\xa4\xf7\x9f\x92\x91\xb8\xa4\x88\x84\xc2\x4c\x55\x7c\x77\xe5\xb1\x52\xa9\x70\xb0\xd7\x04\x57\xd8\x1f\x34\x0e\xaf\x8d\xe8\x00\xda\xea\xef\x2e\xfa\x33\x05\xff\xcc\x29\xa6\x0a\x4a\xed\x26\x53\xa7\x9a\xe9\x39\xe7\x5b\xce\x88\x5e\x73\x8f\xc4\xd8\xa0\x33\x7c\x24\x6d\x60\xee\x94\x1b\xc1\xd5\x89\x3a\xf9\x1a\xdd\x75\x34\xf4\xd6\x1d\x23\x1b\x27\xfc\x45\xc5\xdc\x66\x63\xe3\xad\xa7\xee\xa0\xaa\x65\xf0\x58\xd0\xc0\x50\x39\xd7\xd2\x0e\xa0\xc6\xd0\x7e\xd1\x6a\xa1\xdb\x64\x1d\x04\x05\x76\x73\xbb\xb2\x24\xbb\x60\xec\x6e\x9d\x58\x7b\x02\xae\x79\xb2\x09\x1a\x9c\xbf\xcf\xcf\xb6\x3b\x20\xef\xdc\x19\x4b\xb4\xb5\x26\xae\xae\xe0\xd6\xde\x28\x0e\xe1\x1c\x23\x40\x09\x6c\xd9\xa5\x4f\xd4\x66\x3e\x01\xf9\x3a\xc7\x46\x0a\x57\xc0\x6a\x77\xc1\x5b\x99\xfe\xdd\x21\xa5\xc6\x5c\x3e\x60\x54\x03\xd2\xb6\xe9\xe3\x60\xd4\xbe\xbb\x0c\x1a\x2e\x26\x48\x04\x70\x82\x73\x24\x43\x89\xe8\x86\xf1\x44\xcd\x0b\xa4\x20\x6b\xef\x65\x2f\x4a\xfd\x33\x39\x9f\x23\x81\x08\x01\x82\x65\x15\x93\x43\xd3\x02\x08\x3a\x5e\x55\x3e\x1b\xf4\x1d\xc2\xa4\x16\x90\xa1\x5c\x75\x57\xbf\x01\x9f\x4b\x2b\x46\xb1\x62\xce\x0c\x11\xb7\x65\x85\x9e\xb3\x7e\xdb\x06\x24\xd4\xd9\x8c\x9b\xfa\xd8\xc9\x82\xe1\x09\x6d\xe3\x77\x59\x75\x9e\x31\xd1\xb9\xd6\x7b\x3c\xa6\xdf\x71\x22\xba\x00\xa9\x33\xc9\x30\xf8\x09\xe2\x07\x4b\x4b\x77\xca\xc8\x38\x23\x38\x3f\x2e\x25\x61\xce\x68\xab\xe4\x18\x87\xb8\xd1\x03\xb5\x3b\xe8\x56\xa8\xe2\x2a\x18\xac\x0d\xc2\x57\x4c\x0b\xf6\xf5\x82\x0d\x97\x73\x25\x4e\x50\x0e\x56\xbe\xbb\x55\xd1\x52\x09\x84\xa9\xba\xb8\x9c\xdf\x2a\xd6\x0d\xd5\x7c\xf0\xcf\x40\xd6\x1f\xe0\xc2\xf7\xe8\x9e\x4c\x9f\xf3\x3a\x38\x0d\xac\xa0\x62\xc2\xe9\x80\x0b\x3c\xf4\x08\x89\xd8\x83\x2d\x50\xd5\xa2\xc6\xc7\x1d\x54\xc6\xf8\xf2\xa7\x8d\xf0\x88\x78\x13\x4e\x48\x98\xa3\x6a\xa9\xe8\x88\x1e\xa8\xa7\xce\x1a\x9c\xcc\xcf\x2d\x12\xff\xec\x22\xc4\x75\x98\xf7\x0e\x42\xd6\x5b\xea\x19\x21\x4c\x4f\x19\xae\x5b\xfe\xf8\x63\xca\xc9\x7f\x28\xb9\x2d\xe9\xf5\x77\x61\x1e\xab\x3e\x0e\x3d\xf3\x7a\xd0\xd5\x26\xda\xc4\xde\x8b\xa8\xe5\xf8\x6f\xda\x77\x7b\x44\xe0\xea\xf3\x91\x52\x28\xdf\x47\x1d\x09\x2e\x6c\x1a\x6f\xc8\x43\xdd\xe3\xa7\x40\x1a\xea\xa0\xfe\xcf\x42\xff\x11\x9f\xfd\x79\xfe\xd5\xbd\x35\x0b\x3e\xf2\x6a\xa0\xae\xae\xe3\x11\x2f\x9b\x5e\x81\xcd\x5e\xda\x14\xe3\x19\xa4\x61\x92\xe9\x78\x60\x4e\x93\xd1\x57\x6f\x1d\xc6\x66\xcc\x86\x0d\xe6\x78\x0e\x3c\x2e\xa6\x73\x33\xa7\x1e\xc4\x73\x15\x63\x6d\xda\x29\x71\x5e\xf2\x05\x93\xcd\xfd\xbb\x99\x96\x61\xee\x8a\xfc\x07\xd5\xda\x05\xe6\x79\x6e\x9b\x5a\xe7\x8c\x5e\xbb\xd3\x27\x9e\x9e\xf8\x37\xf0\x27\x0f\x3e\xb5\x9c\xf4\x38\x19\x5f\x7d\x1f\xcf\x64\xdb\xc7\x9a\x9b\x91\x7e\x2c\x90\xf6\xc1\x89\x91\xdd\x37\xe6\xd1\xcb\x67\x46\xe7\x33\x50\x7b\x22\xdc\x3f\xc7\xf4\x5c\x80\xac\xcc\xbf\xcd\xd3\xd9\xd5\x69\xf5\x6f\x00\x00\x00\xff\xff\x3e\x1e\x04\x4e\xb3\x2f\x00\x00") +var _dataConfig_schema_v31Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x5a\xcd\x8f\xdc\x28\x16\xbf\xd7\x5f\x61\x39\xb9\xa5\x3f\xb2\xda\x68\xa5\xcd\x6d\x8f\x7b\x9a\x39\x4f\xcb\xb1\x28\xfb\x95\x8b\x34\x06\x02\xb8\xd2\x95\xa8\xff\xf7\x11\xfe\x2a\x8c\xc1\xe0\x2e\xf7\x74\x34\x9a\x53\x77\x99\xdf\x03\xde\xf7\xe3\xc1\xcf\x5d\x92\xa4\xef\x65\x71\x84\x1a\xa5\x9f\x93\xf4\xa8\x14\xff\x7c\x7f\xff\x55\x32\x7a\xdb\x7d\xbd\x63\xa2\xba\x2f\x05\x3a\xa8\xdb\x8f\x9f\xee\xbb\x6f\xef\xd2\x1b\x4d\x87\x4b\x4d\x52\x30\x7a\xc0\x55\xde\x8d\xe4\xa7\x7f\xdf\xfd\xeb\x4e\x93\x77\x10\x75\xe6\xa0\x41\x6c\xff\x15\x0a\xd5\x7d\x13\xf0\xad\xc1\x02\x34\xf1\x43\x7a\x02\x21\x31\xa3\x69\x76\xb3\xd3\x63\x5c\x30\x0e\x42\x61\x90\xe9\xe7\x44\x6f\x2e\x49\x46\xc8\xf0\xc1\x98\x56\x2a\x81\x69\x95\xb6\x9f\x9f\xdb\x19\x92\x24\x95\x20\x4e\xb8\x30\x66\x18\xb7\xfa\xee\xfe\x32\xff\xfd\x08\xbb\xb1\x67\x35\x36\xdb\x7e\xe7\x48\x29\x10\xf4\xf7\xf9\xde\xda\xe1\x2f\x0f\xe8\xf6\xc7\xff\x6e\xff\xf8\x78\xfb\xdf\xbb\xfc\x36\xfb\xf0\x7e\x32\xac\xe5\x2b\xe0\xd0\x2d\x5f\xc2\x01\x53\xac\x30\xa3\xe3\xfa\xe9\x88\x7c\xee\xff\x7b\x1e\x17\x46\x65\xd9\x82\x11\x99\xac\x7d\x40\x44\xc2\x94\x67\x0a\xea\x3b\x13\x8f\x21\x9e\x47\xd8\x1b\xf1\xdc\xaf\xef\xe0\x79\xca\xce\x89\x91\xa6\x0e\x6a\x70\x40\xbd\x11\x33\xdd\xf2\xdb\xe8\x4f\x42\x21\x40\x85\x4d\xb6\x43\xbd\x99\xc5\xea\xe5\xaf\x63\x78\x37\x30\xbd\x88\xed\x10\xc6\xda\xed\x06\x27\xee\xed\x12\x95\xcb\xbd\xfc\xb2\x1a\x85\xe5\x91\x52\x09\x9c\xb0\xb3\xfe\xe6\x91\x47\x07\xa8\x81\xaa\x74\x14\x41\x92\xa4\xfb\x06\x93\xd2\x96\x28\xa3\xf0\x9b\x9e\xe2\xc1\xf8\x98\x24\x3f\xed\x48\x66\xcc\xd3\x8e\x4f\x7e\xf9\x15\x3e\x8e\x7b\x78\x19\xc7\x0b\x46\x15\x3c\xa9\x96\xa9\xe5\xa5\x3b\x11\xb0\xe2\x11\xc4\x01\x13\x88\xa5\x40\xa2\x92\x0b\x22\x23\x58\xaa\x9c\x89\xbc\xc4\x85\x4a\x9f\x2d\xf2\xd9\x7c\x61\x7b\x1a\x49\x8d\x5f\xd9\xce\x31\x61\x5a\x20\x9e\xa3\xb2\x9c\xf0\x81\x84\x40\xe7\xf4\x26\x49\xb1\x82\x5a\xba\x59\x4c\xd2\x86\xe2\x6f\x0d\xfc\xbf\x87\x28\xd1\x80\x3d\x6f\x29\x18\xdf\x7e\xe2\x4a\xb0\x86\xe7\x1c\x09\x6d\x60\xcb\xe2\x4f\x0b\x56\xd7\x88\x6e\x65\x75\x6b\xf8\x88\x90\x3c\xa3\x0a\x61\x0a\x22\xa7\xa8\x0e\x19\x92\xf6\x3a\xa0\xa5\xcc\xbb\x84\xbf\x68\x46\x87\xbc\xa3\x97\xd6\x04\x63\xf6\xdf\x54\x1f\x25\x5d\x32\xec\x6e\x1a\x6d\xda\x7a\x6f\xa9\x45\x98\x4b\x40\xa2\x38\xbe\x90\x9e\xd5\x08\xd3\x18\xd9\x01\x55\xe2\xcc\x19\xee\xec\xe5\x97\x33\x04\xa0\xa7\x7c\x8c\x25\xab\xc5\x00\xf4\x84\x05\xa3\xf5\xe0\x0d\x31\x01\x66\x0c\xf2\x9a\xfe\x89\x33\x09\xb6\x60\x2c\x06\xcd\xa1\x91\xd5\x89\x4c\x06\x8a\x87\x81\xf1\x9b\x24\xa5\x4d\xbd\x07\xa1\x6b\xd8\x09\xf2\xc0\x44\x8d\xf4\x66\x87\xb5\x8d\xe1\x89\xa4\x1d\x96\x67\x0a\xd0\xe4\x41\xa7\x75\x44\x72\x82\xe9\xe3\xf6\x26\x0e\x4f\x4a\xa0\xfc\xc8\xa4\x8a\x8f\xe1\x06\xf9\x11\x10\x51\xc7\xe2\x08\xc5\xe3\x02\xb9\x89\x9a\x50\x33\xa9\x62\x8c\x1c\xd7\xa8\x0a\x83\x78\x11\x82\x10\xb4\x07\xf2\x22\x3e\x37\x15\xbe\x31\x2d\xab\x2a\x0d\xf5\x59\xdc\xac\x72\xe9\x87\x43\x39\xbf\x14\xf8\x04\x22\x36\x81\x33\x7e\x29\xb8\xec\xc1\x70\x01\x92\x84\xab\xcf\x09\xf4\xcb\x5d\x57\x7c\x2e\x78\x55\xfb\x1f\x21\x69\x66\x97\x0b\x89\x95\xf7\x5d\x5f\x2c\x0e\xe3\x0a\x8a\x89\x56\x6a\x54\xe8\xba\x41\x80\xf4\xe8\xf5\x02\xed\x4f\x37\x79\xcd\x4a\x9f\x81\xce\xc0\xb6\x6c\xbc\x91\x7a\x75\x22\x4c\x5e\x54\x3f\x46\xa9\x2e\x78\x80\x08\x70\xe3\xdb\x5e\xec\x36\x2f\xdb\x0d\x9b\x58\x8b\x43\x04\x23\x09\x61\x67\xf7\x0a\x72\x32\x1b\xe6\xa7\x4f\x91\x36\xe1\xa2\xfd\xcf\x22\xad\x87\xd4\x3b\x67\x7c\x8d\x1c\x98\xea\xb2\x95\xd6\xdd\x5c\x1b\xc9\x02\xde\xf6\xca\x25\x3c\xc7\xa5\x3f\x56\xb4\x11\xc2\x74\x30\xce\x84\x9a\x79\xd7\xfa\x74\xef\xb3\x60\x53\x5c\x43\x9c\xba\x24\xfc\x6e\xf1\x99\x34\x66\xea\x8e\x22\x9a\xfb\x5f\xd0\x3f\xc2\x9e\x91\x2e\x44\x29\x07\x5a\x21\x51\xc1\xf4\x18\x82\xa9\x82\x0a\x84\x87\x80\x37\x7b\x82\xe5\x11\xca\x35\x34\x82\x29\x56\x30\x12\xe7\x18\xce\xe3\x67\xbc\x33\x4c\x27\xcc\xae\xae\xcd\xb8\xc0\x27\x4c\xa0\xb2\x38\xde\x33\x46\x00\xd1\x49\xa2\x10\x80\xca\x9c\x51\x72\x8e\x40\x4a\x85\x44\xf0\xf8\x27\xa1\x68\x04\x56\xe7\x9c\x71\xb5\x79\x55\x28\x8f\x75\x2e\xf1\x0f\x98\xfa\xde\xc5\xea\xfb\x89\x32\x6b\x43\x56\x3f\x2b\x79\x2d\xf7\xf3\x99\xed\x2b\xb9\x8d\x64\x8d\x28\xae\x73\x9c\x45\x7c\x33\x0d\x72\xcb\xe0\x6a\x0d\x78\xe6\xf0\xbd\x0a\x43\x35\xd4\xa2\xab\x38\x03\xb5\x3c\xcb\x42\xbd\xac\xb6\x96\xaa\xc4\x34\x67\x1c\x68\xd0\x37\xa4\x62\x3c\xaf\x04\x2a\x20\xe7\x20\x30\x73\x8a\x62\x12\x60\xcb\x46\x20\xbd\xfe\x7c\x1a\x89\x2b\x8a\xdc\x71\xc7\x80\xaa\x9a\x1f\x5e\xd8\x04\x50\x2a\xec\xec\x0d\xc1\x35\xf6\x3b\x8d\xc3\x6a\x23\xea\xb5\xae\x56\x73\x97\x68\x0b\xe5\x59\x54\xc8\x5e\x38\x21\x2c\x1f\x10\x22\x4e\x06\x47\x24\x56\xa4\x8e\xd6\x31\x0f\x9e\xfc\xe4\x3a\x37\x38\xf7\x35\xb9\x99\x6a\xe7\xbb\xe9\x37\x92\x39\xf1\xab\x4a\x2f\x7b\x1b\x99\xb7\xfa\x71\x3b\x55\x23\x83\x87\xb8\x16\x43\xe5\xd2\x01\x64\x84\x1a\x57\x2c\x9b\x66\x0b\x7d\xa8\xd1\x4e\x50\x62\xf7\x6e\x77\x16\x67\x2b\x2e\x49\xac\xfe\xc2\x30\x81\xab\xfb\x6f\x42\x83\xb7\x25\xcb\x37\x11\x3d\xc8\x7b\x4b\x80\x25\xda\x5b\xfd\x71\x97\x73\x6b\x6b\x14\xa7\x70\x8c\x11\xa0\x04\xb6\xf4\x32\x04\x6a\x33\x9e\x80\xfc\x35\x9b\x7c\x0a\xd7\xc0\x1a\x77\xc2\xdb\x99\xf6\xdd\x13\xa5\xc6\x2d\x4a\x40\xa9\x06\xd2\xd6\xe9\xc3\xa8\xd4\xe1\x2c\x10\x54\x5c\x8c\x93\x08\xe0\x04\x17\x48\x86\x02\xd1\x15\xcd\xa4\x86\x97\x48\x41\xde\xdd\xa2\xaf\x0a\xfd\x0b\x31\x9f\x23\x81\x08\x01\x82\x65\x1d\x13\x43\xd3\x12\x08\x3a\xbf\x28\x7d\xb6\xe4\x07\x84\x49\x23\x20\x47\x85\xea\x2f\xea\x03\x36\x97\xd6\x8c\x62\xc5\x9c\x11\x22\x6e\xc9\x1a\x3d\xe5\xc3\xb2\x2d\x24\x54\xd9\x4c\x8b\xfa\xd8\x3e\x90\x61\x09\x5d\xe1\xb7\x2e\x3b\x2f\xa8\xe8\x92\xeb\x3d\x16\x33\xac\x38\x63\x5d\x80\xd4\x91\x64\x6c\xd3\x05\xe9\x83\xa9\xa5\x3f\x65\xe4\x9c\x11\x5c\x9c\xb7\xe2\xb0\x60\xb4\x13\x72\x8c\x41\x5c\x69\x81\xda\x1c\x74\x29\x54\x73\x15\x74\xd6\x96\xe0\x3b\xa6\x25\xfb\xbe\x62\xc1\xed\x4c\x89\x13\x54\x80\x15\xef\xae\x15\xb4\x54\x02\x61\xaa\x56\xa7\xf3\x6b\xd9\xba\x22\x9b\x8f\xf6\x19\x88\xfa\x23\x2e\xfc\xea\xc1\x13\xe9\x0b\xde\x04\x7b\xb7\x35\xd4\x4c\x38\x0d\x70\x83\x67\x39\x21\x16\x07\xd8\x06\x59\x2d\xaa\xd9\xdf\xa3\x72\xc6\xb7\x3f\x6d\x84\x1b\xfa\x59\x38\x20\x61\x8e\xea\xad\xbc\x23\xfa\xfa\x23\x75\xe6\xe0\x64\xb9\x6f\x91\xf8\x7b\x17\xa1\x5d\x87\xf7\xde\x23\x64\xb3\xa7\x9e\x16\xc2\xfc\x94\xb1\x65\x53\x6c\xc3\xa0\x37\xdc\x5c\x7a\xb4\xfa\x30\xd6\xcc\x37\xa3\xac\xb2\x68\x15\x7b\xaf\x0d\xb7\xdb\x7f\x5b\xbe\xdb\x2d\x02\x57\x9d\x8f\x94\x42\xc5\x31\xea\x48\xb0\xb2\x68\xbc\x22\x0e\xf5\x4f\xd5\x02\x61\xa8\x47\xfd\x13\x85\xfe\x26\x36\xfb\xd7\xd9\x57\xff\x32\x30\xf8\x24\xaf\x45\xbd\x38\x8f\x47\xbc\x43\xfb\x05\x74\xf6\xd6\xaa\x98\xf6\x20\x0d\x95\xcc\xdb\x03\x4b\x92\x8c\xbe\x28\xed\x29\xb2\xe9\x36\x6c\x98\xe3\xf1\xf6\x34\x99\x2e\xf5\x9c\x06\x88\xe7\x2a\xc6\x5a\xb4\x17\xe2\x32\xe7\x1b\x06\x9b\xbb\x0f\x0b\x25\xc3\xd2\x83\x86\x57\xca\xb5\x1b\xf4\xf3\xdc\x3a\xb5\xce\x19\x83\x74\xe7\x0f\x72\x3d\xfe\x6f\xd0\xcf\x9e\xe7\x6a\x3e\xe9\x79\xd6\xbe\xfa\x39\xed\xc9\x76\x4f\x6b\xb3\x89\x7c\x2c\x48\xf7\x3c\xc8\x88\xee\x99\x79\xf4\xf2\xa9\xd1\xf9\x68\xd7\xee\x08\x0f\x8f\x67\x3d\x17\x20\x3b\xf3\x6f\xfb\xd0\x79\xf7\xbc\xfb\x33\x00\x00\xff\xff\xfa\xcc\x57\x15\x61\x31\x00\x00") func dataConfig_schema_v31JsonBytes() ([]byte, error) { return bindataRead( diff --git a/components/cli/compose/schema/data/config_schema_v3.1.json b/components/cli/compose/schema/data/config_schema_v3.1.json index f76154ea12..b9d4221995 100644 --- a/components/cli/compose/schema/data/config_schema_v3.1.json +++ b/components/cli/compose/schema/data/config_schema_v3.1.json @@ -167,8 +167,20 @@ "ports": { "type": "array", "items": { - "type": ["string", "number"], - "format": "ports" + "oneOf": [ + {"type": "number", "format": "ports"}, + {"type": "string", "format": "ports"}, + { + "type": "object", + "properties": { + "mode": {"type": "string"}, + "target": {"type": "integer"}, + "published": {"type": "integer"}, + "protocol": {"type": "string"} + }, + "additionalProperties": false + } + ] }, "uniqueItems": true }, diff --git a/components/cli/compose/types/types.go b/components/cli/compose/types/types.go index 4bb5cb6d2d..c74014fb14 100644 --- a/components/cli/compose/types/types.go +++ b/components/cli/compose/types/types.go @@ -106,7 +106,7 @@ type ServiceConfig struct { NetworkMode string `mapstructure:"network_mode"` Networks map[string]*ServiceNetworkConfig Pid string - Ports StringOrNumberList + Ports []ServicePortConfig Privileged bool ReadOnly bool `mapstructure:"read_only"` Restart string @@ -215,6 +215,14 @@ type ServiceNetworkConfig struct { Ipv6Address string `mapstructure:"ipv6_address"` } +// ServicePortConfig is the port configuration for a service +type ServicePortConfig struct { + Mode string + Target uint32 + Published uint32 + Protocol string +} + // ServiceSecretConfig is the secret configuration for a service type ServiceSecretConfig struct { Source string From 7cfd3076e49b9bf8cf86216a3fd24469fbd24ccd Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Tue, 31 Jan 2017 12:45:45 -0800 Subject: [PATCH 584/978] Support expanded ports in Compose loader This commit adds support for expanded ports in Compose loader, and add several unit tests for loading expanded port format. Signed-off-by: Yong Tang Upstream-commit: c53471254b4184dee4c907ae51aac6628e0be873 Component: cli --- components/cli/compose/convert/service.go | 22 +- .../cli/compose/convert/service_test.go | 34 +++ components/cli/compose/loader/loader.go | 78 +++++- components/cli/compose/loader/loader_test.go | 232 +++++++++++++++++- 4 files changed, 342 insertions(+), 24 deletions(-) diff --git a/components/cli/compose/convert/service.go b/components/cli/compose/convert/service.go index a8613c0878..ef6a04ebcf 100644 --- a/components/cli/compose/convert/service.go +++ b/components/cli/compose/convert/service.go @@ -3,6 +3,7 @@ package convert import ( "fmt" "os" + "sort" "time" "github.com/docker/docker/api/types" @@ -13,8 +14,6 @@ import ( "github.com/docker/docker/client" "github.com/docker/docker/opts" runconfigopts "github.com/docker/docker/runconfig/opts" - "github.com/docker/go-connections/nat" - "sort" ) // Services from compose-file types to engine API types @@ -367,19 +366,16 @@ func (a byPublishedPort) Len() int { return len(a) } func (a byPublishedPort) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a byPublishedPort) Less(i, j int) bool { return a[i].PublishedPort < a[j].PublishedPort } -func convertEndpointSpec(source []string) (*swarm.EndpointSpec, error) { +func convertEndpointSpec(source []composetypes.ServicePortConfig) (*swarm.EndpointSpec, error) { portConfigs := []swarm.PortConfig{} - ports, portBindings, err := nat.ParsePortSpecs(source) - if err != nil { - return nil, err - } - - for port := range ports { - portConfig, err := opts.ConvertPortToPortConfig(port, portBindings) - if err != nil { - return nil, err + for _, port := range source { + portConfig := swarm.PortConfig{ + Protocol: swarm.PortConfigProtocol(port.Protocol), + TargetPort: port.Target, + PublishedPort: port.Published, + PublishMode: swarm.PortConfigPublishMode(port.Mode), } - portConfigs = append(portConfigs, portConfig...) + portConfigs = append(portConfigs, portConfig) } sort.Sort(byPublishedPort(portConfigs)) diff --git a/components/cli/compose/convert/service_test.go b/components/cli/compose/convert/service_test.go index 2e614d730c..64ccfd038e 100644 --- a/components/cli/compose/convert/service_test.go +++ b/components/cli/compose/convert/service_test.go @@ -143,6 +143,40 @@ func TestConvertHealthcheckDisableWithTest(t *testing.T) { assert.Error(t, err, "test and disable can't be set") } +func TestConvertEndpointSpec(t *testing.T) { + source := []composetypes.ServicePortConfig{ + { + Protocol: "udp", + Target: 53, + Published: 1053, + Mode: "host", + }, + { + Target: 8080, + Published: 80, + }, + } + endpoint, err := convertEndpointSpec(source) + + expected := swarm.EndpointSpec{ + Ports: []swarm.PortConfig{ + { + TargetPort: 8080, + PublishedPort: 80, + }, + { + Protocol: "udp", + TargetPort: 53, + PublishedPort: 1053, + PublishMode: "host", + }, + }, + } + + assert.NilError(t, err) + assert.DeepEqual(t, *endpoint, expected) +} + func TestConvertServiceNetworksOnlyDefault(t *testing.T) { networkConfigs := networkMap{} networks := map[string]*composetypes.ServiceNetworkConfig{} diff --git a/components/cli/compose/loader/loader.go b/components/cli/compose/loader/loader.go index 2c92666c51..2ccef7198d 100644 --- a/components/cli/compose/loader/loader.go +++ b/components/cli/compose/loader/loader.go @@ -12,7 +12,9 @@ import ( "github.com/docker/docker/cli/compose/interpolation" "github.com/docker/docker/cli/compose/schema" "github.com/docker/docker/cli/compose/types" - "github.com/docker/docker/runconfig/opts" + "github.com/docker/docker/opts" + runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/docker/go-connections/nat" units "github.com/docker/go-units" shellwords "github.com/mattn/go-shellwords" "github.com/mitchellh/mapstructure" @@ -237,6 +239,8 @@ func transformHook( return transformUlimits(data) case reflect.TypeOf(types.UnitBytes(0)): return transformSize(data) + case reflect.TypeOf([]types.ServicePortConfig{}): + return transformServicePort(data) case reflect.TypeOf(types.ServiceSecretConfig{}): return transformServiceSecret(data) case reflect.TypeOf(types.StringOrNumberList{}): @@ -340,14 +344,14 @@ func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string) e for _, file := range serviceConfig.EnvFile { filePath := absPath(workingDir, file) - fileVars, err := opts.ParseEnvFile(filePath) + fileVars, err := runconfigopts.ParseEnvFile(filePath) if err != nil { return err } envVars = append(envVars, fileVars...) } - for k, v := range opts.ConvertKVStringsToMap(envVars) { + for k, v := range runconfigopts.ConvertKVStringsToMap(envVars) { environment[k] = v } } @@ -481,6 +485,41 @@ func transformExternal(data interface{}) (interface{}, error) { } } +func transformServicePort(data interface{}) (interface{}, error) { + switch entries := data.(type) { + case []interface{}: + // We process the list instead of individual items here. + // The reason is that one entry might be mapped to multiple ServicePortConfig. + // Therefore we take an input of a list and return an output of a list. + ports := []interface{}{} + for _, entry := range entries { + switch value := entry.(type) { + case int: + v, err := toServicePortConfigs(fmt.Sprint(value)) + if err != nil { + return data, err + } + ports = append(ports, v...) + case string: + v, err := toServicePortConfigs(value) + if err != nil { + return data, err + } + ports = append(ports, v...) + case types.Dict: + ports = append(ports, value) + case map[string]interface{}: + ports = append(ports, value) + default: + return data, fmt.Errorf("invalid type %T for port", value) + } + } + return ports, nil + default: + return data, fmt.Errorf("invalid type %T for port", entries) + } +} + func transformServiceSecret(data interface{}) (interface{}, error) { switch value := data.(type) { case string: @@ -572,6 +611,39 @@ func transformSize(value interface{}) (int64, error) { panic(fmt.Errorf("invalid type for size %T", value)) } +func toServicePortConfigs(value string) ([]interface{}, error) { + var portConfigs []interface{} + + ports, portBindings, err := nat.ParsePortSpecs([]string{value}) + if err != nil { + return nil, err + } + // We need to sort the key of the ports to make sure it is consistent + keys := []string{} + for port := range ports { + keys = append(keys, string(port)) + } + sort.Strings(keys) + + for _, key := range keys { + // Reuse ConvertPortToPortConfig so that it is consistent + portConfig, err := opts.ConvertPortToPortConfig(nat.Port(key), portBindings) + if err != nil { + return nil, err + } + for _, p := range portConfig { + portConfigs = append(portConfigs, types.ServicePortConfig{ + Protocol: string(p.Protocol), + Target: p.TargetPort, + Published: p.PublishedPort, + Mode: string(p.PublishMode), + }) + } + } + + return portConfigs, nil +} + func toMapStringString(value map[string]interface{}) map[string]string { output := make(map[string]string) for key, value := range value { diff --git a/components/cli/compose/loader/loader_test.go b/components/cli/compose/loader/loader_test.go index 3a2f27204d..53f4280b64 100644 --- a/components/cli/compose/loader/loader_test.go +++ b/components/cli/compose/loader/loader_test.go @@ -675,14 +675,145 @@ func TestFullExample(t *testing.T) { "other-other-network": nil, }, Pid: "host", - Ports: []string{ - "3000", - "3000-3005", - "8000:8000", - "9090-9091:8080-8081", - "49100:22", - "127.0.0.1:8001:8001", - "127.0.0.1:5000-5010:5000-5010", + Ports: []types.ServicePortConfig{ + //"3000", + { + Mode: "ingress", + Target: 3000, + Protocol: "tcp", + }, + //"3000-3005", + { + Mode: "ingress", + Target: 3000, + Protocol: "tcp", + }, + { + Mode: "ingress", + Target: 3001, + Protocol: "tcp", + }, + { + Mode: "ingress", + Target: 3002, + Protocol: "tcp", + }, + { + Mode: "ingress", + Target: 3003, + Protocol: "tcp", + }, + { + Mode: "ingress", + Target: 3004, + Protocol: "tcp", + }, + { + Mode: "ingress", + Target: 3005, + Protocol: "tcp", + }, + //"8000:8000", + { + Mode: "ingress", + Target: 8000, + Published: 8000, + Protocol: "tcp", + }, + //"9090-9091:8080-8081", + { + Mode: "ingress", + Target: 8080, + Published: 9090, + Protocol: "tcp", + }, + { + Mode: "ingress", + Target: 8081, + Published: 9091, + Protocol: "tcp", + }, + //"49100:22", + { + Mode: "ingress", + Target: 22, + Published: 49100, + Protocol: "tcp", + }, + //"127.0.0.1:8001:8001", + { + Mode: "ingress", + Target: 8001, + Published: 8001, + Protocol: "tcp", + }, + //"127.0.0.1:5000-5010:5000-5010", + { + Mode: "ingress", + Target: 5000, + Published: 5000, + Protocol: "tcp", + }, + { + Mode: "ingress", + Target: 5001, + Published: 5001, + Protocol: "tcp", + }, + { + Mode: "ingress", + Target: 5002, + Published: 5002, + Protocol: "tcp", + }, + { + Mode: "ingress", + Target: 5003, + Published: 5003, + Protocol: "tcp", + }, + { + Mode: "ingress", + Target: 5004, + Published: 5004, + Protocol: "tcp", + }, + { + Mode: "ingress", + Target: 5005, + Published: 5005, + Protocol: "tcp", + }, + { + Mode: "ingress", + Target: 5006, + Published: 5006, + Protocol: "tcp", + }, + { + Mode: "ingress", + Target: 5007, + Published: 5007, + Protocol: "tcp", + }, + { + Mode: "ingress", + Target: 5008, + Published: 5008, + Protocol: "tcp", + }, + { + Mode: "ingress", + Target: 5009, + Published: 5009, + Protocol: "tcp", + }, + { + Mode: "ingress", + Target: 5010, + Published: 5010, + Protocol: "tcp", + }, }, Privileged: true, ReadOnly: true, @@ -825,3 +956,88 @@ networks: assert.Equal(t, expected, config.Networks) } + +func TestLoadExpandedPortFormat(t *testing.T) { + config, err := loadYAML(` +version: "3.1" +services: + web: + image: busybox + ports: + - "80-82:8080-8082" + - "90-92:8090-8092/udp" + - "85:8500" + - 8600 + - protocol: udp + target: 53 + published: 10053 + - mode: host + target: 22 + published: 10022 +`) + assert.NoError(t, err) + + expected := []types.ServicePortConfig{ + { + Mode: "ingress", + Target: 8080, + Published: 80, + Protocol: "tcp", + }, + { + Mode: "ingress", + Target: 8081, + Published: 81, + Protocol: "tcp", + }, + { + Mode: "ingress", + Target: 8082, + Published: 82, + Protocol: "tcp", + }, + { + Mode: "ingress", + Target: 8090, + Published: 90, + Protocol: "udp", + }, + { + Mode: "ingress", + Target: 8091, + Published: 91, + Protocol: "udp", + }, + { + Mode: "ingress", + Target: 8092, + Published: 92, + Protocol: "udp", + }, + { + Mode: "ingress", + Target: 8500, + Published: 85, + Protocol: "tcp", + }, + { + Mode: "ingress", + Target: 8600, + Published: 0, + Protocol: "tcp", + }, + { + Target: 53, + Published: 10053, + Protocol: "udp", + }, + { + Mode: "host", + Target: 22, + Published: 10022, + }, + } + + assert.Equal(t, 1, len(config.Services)) + assert.Equal(t, expected, config.Services[0].Ports) +} From 78cba3d0adbc5c8ce3a1d891474648fb80a6ffb0 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Wed, 25 Jan 2017 16:54:18 -0800 Subject: [PATCH 585/978] 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) Upstream-commit: 635d686a88e7ff6c5a3f9e36ec333779926b3151 Component: cli --- components/cli/command/container/create.go | 6 +----- .../cli/command/formatter/disk_usage.go | 4 ++-- components/cli/command/formatter/image.go | 4 ++-- components/cli/command/formatter/service.go | 13 ++++++------ components/cli/command/image/build.go | 4 +--- components/cli/command/image/pull.go | 8 ++++---- components/cli/command/image/trust.go | 16 +++++++-------- components/cli/command/plugin/install.go | 20 +++---------------- components/cli/command/plugin/push.go | 7 ++----- components/cli/command/plugin/upgrade.go | 12 +++++------ components/cli/command/service/trust.go | 12 ++++++----- components/cli/command/task/print.go | 14 +++++++------ components/cli/trust/trust.go | 4 ++-- 13 files changed, 53 insertions(+), 71 deletions(-) diff --git a/components/cli/command/container/create.go b/components/cli/command/container/create.go index cfd672e77a..9559ba0c05 100644 --- a/components/cli/command/container/create.go +++ b/components/cli/command/container/create.go @@ -168,11 +168,7 @@ func createContainer(ctx context.Context, dockerCli *command.DockerCli, config * return nil, err } if named, ok := ref.(reference.Named); ok { - if reference.IsNameOnly(named) { - namedRef = reference.EnsureTagged(named) - } else { - namedRef = named - } + namedRef = reference.TagNameOnly(named) if taggedRef, ok := namedRef.(reference.NamedTagged); ok && command.IsTrusted() { var err error diff --git a/components/cli/command/formatter/disk_usage.go b/components/cli/command/formatter/disk_usage.go index dc5eec41d7..fd7aabc7c2 100644 --- a/components/cli/command/formatter/disk_usage.go +++ b/components/cli/command/formatter/disk_usage.go @@ -94,12 +94,12 @@ func (ctx *DiskUsageContext) Write() { tag := "" if len(i.RepoTags) > 0 && !isDangling(*i) { // Only show the first tag - ref, err := reference.ParseNamed(i.RepoTags[0]) + ref, err := reference.ParseNormalizedNamed(i.RepoTags[0]) if err != nil { continue } if nt, ok := ref.(reference.NamedTagged); ok { - repo = ref.Name() + repo = reference.FamiliarName(ref) tag = nt.Tag() } } diff --git a/components/cli/command/formatter/image.go b/components/cli/command/formatter/image.go index 06319b9355..b6508224a3 100644 --- a/components/cli/command/formatter/image.go +++ b/components/cli/command/formatter/image.go @@ -94,7 +94,7 @@ func imageFormat(ctx ImageContext, images []types.ImageSummary, format func(subC repoTags := map[string][]string{} repoDigests := map[string][]string{} - for _, refString := range append(image.RepoTags) { + for _, refString := range image.RepoTags { ref, err := reference.ParseNormalizedNamed(refString) if err != nil { continue @@ -104,7 +104,7 @@ func imageFormat(ctx ImageContext, images []types.ImageSummary, format func(subC repoTags[familiarRef] = append(repoTags[familiarRef], nt.Tag()) } } - for _, refString := range append(image.RepoDigests) { + for _, refString := range image.RepoDigests { ref, err := reference.ParseNormalizedNamed(refString) if err != nil { continue diff --git a/components/cli/command/formatter/service.go b/components/cli/command/formatter/service.go index 9d9241b224..8e38cb3a11 100644 --- a/components/cli/command/formatter/service.go +++ b/components/cli/command/formatter/service.go @@ -5,7 +5,7 @@ import ( "strings" "time" - distreference "github.com/docker/distribution/reference" + "github.com/docker/distribution/reference" mounttypes "github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli/command/inspect" @@ -409,11 +409,12 @@ func (c *serviceContext) Replicas() string { func (c *serviceContext) Image() string { c.AddHeader(imageHeader) image := c.service.Spec.TaskTemplate.ContainerSpec.Image - if ref, err := distreference.ParseNamed(image); err == nil { - // update image string for display - namedTagged, ok := ref.(distreference.NamedTagged) - if ok { - image = namedTagged.Name() + ":" + namedTagged.Tag() + if ref, err := reference.ParseNormalizedNamed(image); err == nil { + // update image string for display, (strips any digest) + if nt, ok := ref.(reference.NamedTagged); ok { + if namedTagged, err := reference.WithTag(reference.TrimNamed(nt), nt.Tag()); err == nil { + image = reference.FamiliarString(namedTagged) + } } } diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go index 34e0a39500..96d90cf585 100644 --- a/components/cli/command/image/build.go +++ b/components/cli/command/image/build.go @@ -397,9 +397,7 @@ func rewriteDockerfileFrom(ctx context.Context, dockerfile io.Reader, translator if err != nil { return nil, nil, err } - if reference.IsNameOnly(ref) { - ref = reference.EnsureTagged(ref) - } + ref = reference.TagNameOnly(ref) if ref, ok := ref.(reference.NamedTagged); ok && command.IsTrusted() { trustedRef, err := translator(ctx, ref) if err != nil { diff --git a/components/cli/command/image/pull.go b/components/cli/command/image/pull.go index 967beca86f..515273d43c 100644 --- a/components/cli/command/image/pull.go +++ b/components/cli/command/image/pull.go @@ -42,7 +42,6 @@ func NewPullCommand(dockerCli *command.DockerCli) *cobra.Command { } func runPull(dockerCli *command.DockerCli, opts pullOptions) error { - var distributionRef reference.Named distributionRef, err := reference.ParseNormalizedNamed(opts.remote) if err != nil { return err @@ -52,9 +51,10 @@ func runPull(dockerCli *command.DockerCli, opts pullOptions) error { } if !opts.all && reference.IsNameOnly(distributionRef) { - taggedRef := reference.EnsureTagged(distributionRef) - fmt.Fprintf(dockerCli.Out(), "Using default tag: %s\n", taggedRef.Tag()) - distributionRef = taggedRef + distributionRef = reference.TagNameOnly(distributionRef) + if tagged, ok := distributionRef.(reference.Tagged); ok { + fmt.Fprintf(dockerCli.Out(), "Using default tag: %s\n", tagged.Tag()) + } } // Resolve the Repository name from fqn to RepositoryInfo diff --git a/components/cli/command/image/trust.go b/components/cli/command/image/trust.go index 2ff9b463d5..8332dd7deb 100644 --- a/components/cli/command/image/trust.go +++ b/components/cli/command/image/trust.go @@ -129,15 +129,15 @@ func PushTrustedReference(cli *command.DockerCli, repoInfo *registry.RepositoryI // Initialize the notary repository with a remotely managed snapshot key if err := repo.Initialize([]string{rootKeyID}, data.CanonicalSnapshotRole); err != nil { - return trust.NotaryError(repoInfo.FullName(), err) + return trust.NotaryError(repoInfo.Name.Name(), err) } - fmt.Fprintf(cli.Out(), "Finished initializing %q\n", repoInfo.FullName()) + fmt.Fprintf(cli.Out(), "Finished initializing %q\n", repoInfo.Name.Name()) err = repo.AddTarget(target, data.CanonicalTargetsRole) case nil: // already initialized and we have successfully downloaded the latest metadata err = addTargetToAllSignableRoles(repo, target) default: - return trust.NotaryError(repoInfo.FullName(), err) + return trust.NotaryError(repoInfo.Name.Name(), err) } if err == nil { @@ -145,11 +145,11 @@ func PushTrustedReference(cli *command.DockerCli, repoInfo *registry.RepositoryI } if err != nil { - fmt.Fprintf(cli.Out(), "Failed to sign %q:%s - %s\n", repoInfo.FullName(), tag, err.Error()) - return trust.NotaryError(repoInfo.FullName(), err) + fmt.Fprintf(cli.Out(), "Failed to sign %q:%s - %s\n", repoInfo.Name.Name(), tag, err.Error()) + return trust.NotaryError(repoInfo.Name.Name(), err) } - fmt.Fprintf(cli.Out(), "Successfully signed %q:%s\n", repoInfo.FullName(), tag) + fmt.Fprintf(cli.Out(), "Successfully signed %q:%s\n", repoInfo.Name.Name(), tag) return nil } @@ -342,12 +342,12 @@ func TrustedReference(ctx context.Context, cli *command.DockerCli, ref reference t, err := notaryRepo.GetTargetByName(ref.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole) if err != nil { - return nil, trust.NotaryError(repoInfo.FullName(), err) + return nil, trust.NotaryError(repoInfo.Name.Name(), err) } // Only list tags in the top level targets role or the releases delegation role - ignore // all other delegation roles if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole { - return nil, trust.NotaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", ref.Tag())) + return nil, trust.NotaryError(repoInfo.Name.Name(), fmt.Errorf("No trust data for %s", ref.Tag())) } r, err := convertTarget(t.Target) if err != nil { diff --git a/components/cli/command/plugin/install.go b/components/cli/command/plugin/install.go index 15877761af..9e9ea40e2a 100644 --- a/components/cli/command/plugin/install.go +++ b/components/cli/command/plugin/install.go @@ -7,7 +7,6 @@ import ( "github.com/docker/distribution/reference" "github.com/docker/docker/api/types" - registrytypes "github.com/docker/docker/api/types/registry" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/image" @@ -54,20 +53,6 @@ func newInstallCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func getRepoIndexFromUnnormalizedRef(ref reference.Named) (*registrytypes.IndexInfo, error) { - named, err := reference.ParseNormalizedNamed(ref.Name()) - if err != nil { - return nil, err - } - - repoInfo, err := registry.ParseRepositoryInfo(named) - if err != nil { - return nil, err - } - - return repoInfo.Index, nil -} - type pluginRegistryService struct { registry.Service } @@ -104,9 +89,10 @@ func buildPullConfig(ctx context.Context, dockerCli *command.DockerCli, opts plu _, isCanonical := ref.(reference.Canonical) if command.IsTrusted() && !isCanonical { + ref = reference.TagNameOnly(ref) nt, ok := ref.(reference.NamedTagged) if !ok { - nt = reference.EnsureTagged(ref) + return types.PluginInstallOptions{}, fmt.Errorf("invalid name: %s", ref.String()) } ctx := context.Background() @@ -148,7 +134,7 @@ func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error { if _, ok := aref.(reference.Canonical); ok { return fmt.Errorf("invalid name: %s", opts.localName) } - localName = reference.FamiliarString(reference.EnsureTagged(aref)) + localName = reference.FamiliarString(reference.TagNameOnly(aref)) } ctx := context.Background() diff --git a/components/cli/command/plugin/push.go b/components/cli/command/plugin/push.go index 6b826dce68..f3643b7f1b 100644 --- a/components/cli/command/plugin/push.go +++ b/components/cli/command/plugin/push.go @@ -40,10 +40,7 @@ func runPush(dockerCli *command.DockerCli, name string) error { return fmt.Errorf("invalid name: %s", name) } - taggedRef, ok := named.(reference.NamedTagged) - if !ok { - taggedRef = reference.EnsureTagged(named) - } + named = reference.TagNameOnly(named) ctx := context.Background() @@ -58,7 +55,7 @@ func runPush(dockerCli *command.DockerCli, name string) error { return err } - responseBody, err := dockerCli.Client().PluginPush(ctx, reference.FamiliarString(taggedRef), encodedAuth) + responseBody, err := dockerCli.Client().PluginPush(ctx, reference.FamiliarString(named), encodedAuth) if err != nil { return err } diff --git a/components/cli/command/plugin/upgrade.go b/components/cli/command/plugin/upgrade.go index 6861aa1b32..07f0c7bb91 100644 --- a/components/cli/command/plugin/upgrade.go +++ b/components/cli/command/plugin/upgrade.go @@ -5,10 +5,10 @@ import ( "fmt" "strings" + "github.com/docker/distribution/reference" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/pkg/jsonmessage" - "github.com/docker/docker/reference" "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -49,19 +49,19 @@ func runUpgrade(dockerCli *command.DockerCli, opts pluginOptions) error { if opts.remote == "" { opts.remote = p.PluginReference } - remote, err := reference.ParseNamed(opts.remote) + remote, err := reference.ParseNormalizedNamed(opts.remote) if err != nil { return errors.Wrap(err, "error parsing remote upgrade image reference") } - remote = reference.WithDefaultTag(remote) + remote = reference.TagNameOnly(remote) - old, err := reference.ParseNamed(p.PluginReference) + old, err := reference.ParseNormalizedNamed(p.PluginReference) if err != nil { return errors.Wrap(err, "error parsing current image reference") } - old = reference.WithDefaultTag(old) + old = reference.TagNameOnly(old) - fmt.Fprintf(dockerCli.Out(), "Upgrading plugin %s from %s to %s\n", p.Name, old, remote) + fmt.Fprintf(dockerCli.Out(), "Upgrading plugin %s from %s to %s\n", p.Name, reference.FamiliarString(old), reference.FamiliarString(remote)) if !opts.skipRemoteCheck && remote.String() != old.String() { if !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), "Plugin images do not match, are you sure?") { return errors.New("canceling upgrade request") diff --git a/components/cli/command/service/trust.go b/components/cli/command/service/trust.go index d466f3b648..3fd80ae879 100644 --- a/components/cli/command/service/trust.go +++ b/components/cli/command/service/trust.go @@ -33,10 +33,12 @@ func resolveServiceImageDigest(dockerCli *command.DockerCli, service *swarm.Serv namedRef, ok := ref.(reference.Named) if !ok { return errors.New("failed to resolve image digest using content trust: reference is not named") - } - - taggedRef := reference.EnsureTagged(namedRef) + namedRef = reference.TagNameOnly(namedRef) + taggedRef, ok := namedRef.(reference.NamedTagged) + if !ok { + return errors.New("failed to resolve image digest using content trust: reference is not tagged") + } resolvedImage, err := trustedResolveDigest(context.Background(), dockerCli, taggedRef) if err != nil { @@ -65,12 +67,12 @@ func trustedResolveDigest(ctx context.Context, cli *command.DockerCli, ref refer t, err := notaryRepo.GetTargetByName(ref.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole) if err != nil { - return nil, trust.NotaryError(repoInfo.FullName(), err) + return nil, trust.NotaryError(repoInfo.Name.Name(), err) } // Only get the tag if it's in the top level targets role or the releases delegation role // ignore it if it's in any other delegation roles if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole { - return nil, trust.NotaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", reference.FamiliarString(ref))) + return nil, trust.NotaryError(repoInfo.Name.Name(), fmt.Errorf("No trust data for %s", reference.FamiliarString(ref))) } logrus.Debugf("retrieving target for %s role\n", t.Role) diff --git a/components/cli/command/task/print.go b/components/cli/command/task/print.go index 60a2bca85b..d7e20bb59a 100644 --- a/components/cli/command/task/print.go +++ b/components/cli/command/task/print.go @@ -10,7 +10,7 @@ import ( "golang.org/x/net/context" - distreference "github.com/docker/distribution/reference" + "github.com/docker/distribution/reference" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/idresolver" @@ -129,13 +129,15 @@ func print(out io.Writer, ctx context.Context, tasks []swarm.Task, resolver *idr image := task.Spec.ContainerSpec.Image if !noTrunc { - ref, err := distreference.ParseNamed(image) + ref, err := reference.ParseNormalizedNamed(image) if err == nil { - // update image string for display - namedTagged, ok := ref.(distreference.NamedTagged) - if ok { - image = namedTagged.Name() + ":" + namedTagged.Tag() + // update image string for display, (strips any digest) + if nt, ok := ref.(reference.NamedTagged); ok { + if namedTagged, err := reference.WithTag(reference.TrimNamed(nt), nt.Tag()); err == nil { + image = reference.FamiliarString(namedTagged) + } } + } } diff --git a/components/cli/trust/trust.go b/components/cli/trust/trust.go index 44f8197ba2..777a611181 100644 --- a/components/cli/trust/trust.go +++ b/components/cli/trust/trust.go @@ -148,7 +148,7 @@ func GetNotaryRepository(streams command.Streams, repoInfo *registry.RepositoryI } scope := auth.RepositoryScope{ - Repository: repoInfo.FullName(), + Repository: repoInfo.Name.Name(), Actions: actions, Class: repoInfo.Class, } @@ -166,7 +166,7 @@ func GetNotaryRepository(streams command.Streams, repoInfo *registry.RepositoryI return client.NewNotaryRepository( trustDirectory(), - repoInfo.FullName(), + repoInfo.Name.Name(), server, tr, getPassphraseRetriever(streams), From 4e65e9211827c90b11365297d8e29d6a801fe609 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Wed, 25 Jan 2017 16:54:18 -0800 Subject: [PATCH 586/978] 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) Upstream-commit: b741d2e9b5cf4e52f8da0c94cdc3097616120411 Component: cli --- components/cli/container_commit.go | 14 ++++++++------ components/cli/image_create.go | 8 ++++---- components/cli/image_import.go | 2 +- components/cli/image_pull.go | 27 +++++++++++++++++++++------ components/cli/image_pull_test.go | 2 +- components/cli/image_push.go | 18 ++++++++++-------- components/cli/image_push_test.go | 2 +- components/cli/image_tag.go | 17 +++++++++-------- components/cli/plugin_install.go | 2 +- components/cli/plugin_upgrade.go | 2 +- 10 files changed, 57 insertions(+), 37 deletions(-) diff --git a/components/cli/container_commit.go b/components/cli/container_commit.go index c766d62e40..531d796ee7 100644 --- a/components/cli/container_commit.go +++ b/components/cli/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/components/cli/image_create.go b/components/cli/image_create.go index cf023a7186..4436abb0dd 100644 --- a/components/cli/image_create.go +++ b/components/cli/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/components/cli/image_import.go b/components/cli/image_import.go index c6f154b249..d7dedd8232 100644 --- a/components/cli/image_import.go +++ b/components/cli/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/components/cli/image_pull.go b/components/cli/image_pull.go index 3bffdb70e8..a72b9bf7fc 100644 --- a/components/cli/image_pull.go +++ b/components/cli/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/components/cli/image_pull_test.go b/components/cli/image_pull_test.go index fe6bafed97..ab49d2d349 100644 --- a/components/cli/image_pull_test.go +++ b/components/cli/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/components/cli/image_push.go b/components/cli/image_push.go index 8e73d28f56..410d2fb91d 100644 --- a/components/cli/image_push.go +++ b/components/cli/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/components/cli/image_push_test.go b/components/cli/image_push_test.go index b52da8b8dc..f93debf5bb 100644 --- a/components/cli/image_push_test.go +++ b/components/cli/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/components/cli/image_tag.go b/components/cli/image_tag.go index dbcd078e1c..35abe332bf 100644 --- a/components/cli/image_tag.go +++ b/components/cli/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/components/cli/plugin_install.go b/components/cli/plugin_install.go index 3217c4cf39..33876cc101 100644 --- a/components/cli/plugin_install.go +++ b/components/cli/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/components/cli/plugin_upgrade.go b/components/cli/plugin_upgrade.go index 95a4356b97..24293c5073 100644 --- a/components/cli/plugin_upgrade.go +++ b/components/cli/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 d81f14e15b9d7fe00f5452dfd73ffb2d2e210bcb Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Tue, 7 Feb 2017 21:58:56 +0100 Subject: [PATCH 587/978] Bump go-units To include https://github.com/docker/go-units/pull/23 Fixes a unit test that fails because of it. Signed-off-by: Vincent Demeester Upstream-commit: 6ef0b64945a6af9666ac601021d8de99317b5207 Component: cli --- .../cli/command/formatter/container_test.go | 16 ++++++------ .../cli/command/formatter/image_test.go | 26 +++++++++---------- .../cli/command/formatter/stats_test.go | 12 ++++----- components/cli/command/service/opts_test.go | 2 +- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/components/cli/command/formatter/container_test.go b/components/cli/command/formatter/container_test.go index 16137897b9..f013328158 100644 --- a/components/cli/command/formatter/container_test.go +++ b/components/cli/command/formatter/container_test.go @@ -54,8 +54,8 @@ func TestContainerPsContext(t *testing.T) { {types.Container{Created: unix}, true, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt}, {types.Container{Ports: []types.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, true, "8080/tcp", portsHeader, ctx.Ports}, {types.Container{Status: "RUNNING"}, true, "RUNNING", statusHeader, ctx.Status}, - {types.Container{SizeRw: 10}, true, "10 B", sizeHeader, ctx.Size}, - {types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10 B (virtual 20 B)", sizeHeader, ctx.Size}, + {types.Container{SizeRw: 10}, true, "10B", sizeHeader, ctx.Size}, + {types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10B (virtual 20B)", sizeHeader, ctx.Size}, {types.Container{}, true, "", labelsHeader, ctx.Labels}, {types.Container{Labels: map[string]string{"cpu": "6", "storage": "ssd"}}, true, "cpu=6,storage=ssd", labelsHeader, ctx.Labels}, {types.Container{Created: unix}, true, "About a minute", runningForHeader, ctx.RunningFor}, @@ -160,8 +160,8 @@ func TestContainerContextWrite(t *testing.T) { { Context{Format: NewContainerFormat("table", false, true)}, `CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES SIZE -containerID1 ubuntu "" 24 hours ago foobar_baz 0 B -containerID2 ubuntu "" 24 hours ago foobar_bar 0 B +containerID1 ubuntu "" 24 hours ago foobar_baz 0B +containerID2 ubuntu "" 24 hours ago foobar_bar 0B `, }, { @@ -220,7 +220,7 @@ status: names: foobar_baz labels: ports: -size: 0 B +size: 0B container_id: containerID2 image: ubuntu @@ -230,7 +230,7 @@ status: names: foobar_bar labels: ports: -size: 0 B +size: 0B `, expectedTime, expectedTime), }, @@ -333,8 +333,8 @@ func TestContainerContextWriteJSON(t *testing.T) { } expectedCreated := time.Unix(unix, 0).String() expectedJSONs := []map[string]interface{}{ - {"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID1", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_baz", "Networks": "", "Ports": "", "RunningFor": "About a minute", "Size": "0 B", "Status": ""}, - {"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID2", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_bar", "Networks": "", "Ports": "", "RunningFor": "About a minute", "Size": "0 B", "Status": ""}, + {"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID1", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_baz", "Networks": "", "Ports": "", "RunningFor": "About a minute", "Size": "0B", "Status": ""}, + {"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID2", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_bar", "Networks": "", "Ports": "", "RunningFor": "About a minute", "Size": "0B", "Status": ""}, } out := bytes.NewBufferString("") err := ContainerWrite(Context{Format: "{{json .}}", Output: out}, containers) diff --git a/components/cli/command/formatter/image_test.go b/components/cli/command/formatter/image_test.go index ffe77f6677..cf134300a1 100644 --- a/components/cli/command/formatter/image_test.go +++ b/components/cli/command/formatter/image_test.go @@ -34,7 +34,7 @@ func TestImageContext(t *testing.T) { {imageContext{ i: types.ImageSummary{Size: 10, VirtualSize: 10}, trunc: true, - }, "10 B", sizeHeader, ctx.Size}, + }, "10B", sizeHeader, ctx.Size}, {imageContext{ i: types.ImageSummary{Created: unix}, trunc: true, @@ -109,9 +109,9 @@ func TestImageContextWrite(t *testing.T) { }, }, `REPOSITORY TAG IMAGE ID CREATED SIZE -image tag1 imageID1 24 hours ago 0 B -image tag2 imageID2 24 hours ago 0 B - imageID3 24 hours ago 0 B +image tag1 imageID1 24 hours ago 0B +image tag2 imageID2 24 hours ago 0B + imageID3 24 hours ago 0B `, }, { @@ -159,9 +159,9 @@ image Digest: true, }, `REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE -image tag1 sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf imageID1 24 hours ago 0 B -image tag2 imageID2 24 hours ago 0 B - imageID3 24 hours ago 0 B +image tag1 sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf imageID1 24 hours ago 0B +image tag2 imageID2 24 hours ago 0B + imageID3 24 hours ago 0B `, }, { @@ -184,19 +184,19 @@ image tag2 tag: tag1 image_id: imageID1 created_at: %s -virtual_size: 0 B +virtual_size: 0B repository: image tag: tag2 image_id: imageID2 created_at: %s -virtual_size: 0 B +virtual_size: 0B repository: tag: image_id: imageID3 created_at: %s -virtual_size: 0 B +virtual_size: 0B `, expectedTime, expectedTime, expectedTime), }, @@ -212,21 +212,21 @@ tag: tag1 digest: sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf image_id: imageID1 created_at: %s -virtual_size: 0 B +virtual_size: 0B repository: image tag: tag2 digest: image_id: imageID2 created_at: %s -virtual_size: 0 B +virtual_size: 0B repository: tag: digest: image_id: imageID3 created_at: %s -virtual_size: 0 B +virtual_size: 0B `, expectedTime, expectedTime, expectedTime), }, diff --git a/components/cli/command/formatter/stats_test.go b/components/cli/command/formatter/stats_test.go index f9ecda33ec..546319eb88 100644 --- a/components/cli/command/formatter/stats_test.go +++ b/components/cli/command/formatter/stats_test.go @@ -22,16 +22,16 @@ func TestContainerStatsContext(t *testing.T) { {StatsEntry{Container: containerID}, "", containerID, containerHeader, ctx.Container}, {StatsEntry{CPUPercentage: 5.5}, "", "5.50%", cpuPercHeader, ctx.CPUPerc}, {StatsEntry{CPUPercentage: 5.5, IsInvalid: true}, "", "--", cpuPercHeader, ctx.CPUPerc}, - {StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3}, "", "0.31 B / 12.3 B", netIOHeader, ctx.NetIO}, + {StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3}, "", "0.31B / 12.3B", netIOHeader, ctx.NetIO}, {StatsEntry{NetworkRx: 0.31, NetworkTx: 12.3, IsInvalid: true}, "", "--", netIOHeader, ctx.NetIO}, - {StatsEntry{BlockRead: 0.1, BlockWrite: 2.3}, "", "0.1 B / 2.3 B", blockIOHeader, ctx.BlockIO}, + {StatsEntry{BlockRead: 0.1, BlockWrite: 2.3}, "", "0.1B / 2.3B", blockIOHeader, ctx.BlockIO}, {StatsEntry{BlockRead: 0.1, BlockWrite: 2.3, IsInvalid: true}, "", "--", blockIOHeader, ctx.BlockIO}, {StatsEntry{MemoryPercentage: 10.2}, "", "10.20%", memPercHeader, ctx.MemPerc}, {StatsEntry{MemoryPercentage: 10.2, IsInvalid: true}, "", "--", memPercHeader, ctx.MemPerc}, {StatsEntry{MemoryPercentage: 10.2}, "windows", "--", memPercHeader, ctx.MemPerc}, - {StatsEntry{Memory: 24, MemoryLimit: 30}, "", "24 B / 30 B", memUseHeader, ctx.MemUsage}, + {StatsEntry{Memory: 24, MemoryLimit: 30}, "", "24B / 30B", memUseHeader, ctx.MemUsage}, {StatsEntry{Memory: 24, MemoryLimit: 30, IsInvalid: true}, "", "-- / --", memUseHeader, ctx.MemUsage}, - {StatsEntry{Memory: 24, MemoryLimit: 30}, "windows", "24 B", winMemUseHeader, ctx.MemUsage}, + {StatsEntry{Memory: 24, MemoryLimit: 30}, "windows", "24B", winMemUseHeader, ctx.MemUsage}, {StatsEntry{PidsCurrent: 10}, "", "10", pidsHeader, ctx.PIDs}, {StatsEntry{PidsCurrent: 10, IsInvalid: true}, "", "--", pidsHeader, ctx.PIDs}, {StatsEntry{PidsCurrent: 10}, "windows", "--", pidsHeader, ctx.PIDs}, @@ -68,7 +68,7 @@ func TestContainerStatsContextWrite(t *testing.T) { { Context{Format: "table {{.MemUsage}}"}, `MEM USAGE / LIMIT -20 B / 20 B +20B / 20B -- / -- `, }, @@ -128,7 +128,7 @@ func TestContainerStatsContextWriteWindows(t *testing.T) { { Context{Format: "table {{.MemUsage}}"}, `PRIV WORKING SET -20 B +20B -- / -- `, }, diff --git a/components/cli/command/service/opts_test.go b/components/cli/command/service/opts_test.go index 4031d6f251..ac5106793b 100644 --- a/components/cli/command/service/opts_test.go +++ b/components/cli/command/service/opts_test.go @@ -12,7 +12,7 @@ import ( func TestMemBytesString(t *testing.T) { var mem opts.MemBytes = 1048576 - assert.Equal(t, mem.String(), "1 MiB") + assert.Equal(t, mem.String(), "1MiB") } func TestMemBytesSetAndValue(t *testing.T) { From ee6a54e8c3dc002c2c72f6cb0466f205636ee147 Mon Sep 17 00:00:00 2001 From: Zhang Wei Date: Tue, 7 Feb 2017 10:27:40 +0800 Subject: [PATCH 588/978] Fix panic of "docker stats --format {{.Name}} --all" This commit fixes panic when execute stats command: * use --format {{.Name}} with --all when there're exited containers. * use --format {{.Name}} while stating exited container. The root cause is when stating an exited container, the result from the api didn't contain the Name and ID field, which will make format process panic. Panic log is like this: ``` panic: runtime error: slice bounds out of range [recovered] panic: runtime error: slice bounds out of range goroutine 1 [running]: panic(0xb20f80, 0xc420014110) /usr/local/go/src/runtime/panic.go:500 +0x1a1 text/template.errRecover(0xc4201773e8) /usr/local/go/src/text/template/exec.go:140 +0x2ad panic(0xb20f80, 0xc420014110) /usr/local/go/src/runtime/panic.go:458 +0x243 github.com/docker/docker/cli/command/formatter.(*containerStatsContext).Name(0xc420430160, 0x0, 0x0) /go/src/github.com/docker/docker/cli/command/formatter/stats.go:148 +0x86 reflect.Value.call(0xb9a3a0, 0xc420430160, 0x2213, 0xbe3657, 0x4, 0x11bc9f8, 0x0, 0x0, 0x4d75b3, 0x1198940, ...) /usr/local/go/src/reflect/value.go:434 +0x5c8 reflect.Value.Call(0xb9a3a0, 0xc420430160, 0x2213, 0x11bc9f8, 0x0, 0x0, 0xc420424028, 0xb, 0xb) /usr/local/go/src/reflect/value.go:302 +0xa4 text/template.(*state).evalCall(0xc420177368, 0xb9a3a0, 0xc420430160, 0x16, 0xb9a3a0, 0xc420430160, 0x2213, 0x1178fa0, 0xc4203ea330, 0xc4203de283, ...) /usr/local/go/src/text/template/exec.go:658 +0x530 ``` Signed-off-by: Zhang Wei Upstream-commit: 758383200e127e00eabe11811e22ad3e7aa30930 Component: cli --- components/cli/command/formatter/stats.go | 6 ++++-- components/cli/command/formatter/stats_test.go | 8 ++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/components/cli/command/formatter/stats.go b/components/cli/command/formatter/stats.go index a37e9d7923..0e31792c4f 100644 --- a/components/cli/command/formatter/stats.go +++ b/components/cli/command/formatter/stats.go @@ -149,8 +149,10 @@ func (c *containerStatsContext) Container() string { func (c *containerStatsContext) Name() string { c.AddHeader(nameHeader) - name := c.s.Name[1:] - return name + if len(c.s.Name) > 1 { + return c.s.Name[1:] + } + return "--" } func (c *containerStatsContext) ID() string { diff --git a/components/cli/command/formatter/stats_test.go b/components/cli/command/formatter/stats_test.go index d5a17cc70e..f5c6cae0c3 100644 --- a/components/cli/command/formatter/stats_test.go +++ b/components/cli/command/formatter/stats_test.go @@ -69,6 +69,12 @@ func TestContainerStatsContextWrite(t *testing.T) { `MEM USAGE / LIMIT 20 B / 20 B -- / -- +`, + }, + { + Context{Format: "{{.Container}} {{.ID}} {{.Name}}"}, + `container1 abcdef foo +container2 -- `, }, { @@ -83,6 +89,8 @@ container2 -- stats := []StatsEntry{ { Container: "container1", + ID: "abcdef", + Name: "/foo", CPUPercentage: 20, Memory: 20, MemoryLimit: 20, From 481e937f5bdaf25be624505e0eacacd2d47560f0 Mon Sep 17 00:00:00 2001 From: chchliang Date: Thu, 9 Feb 2017 11:26:20 +0800 Subject: [PATCH 589/978] add test case check connect.EndpointConfig not nil Signed-off-by: chchliang Upstream-commit: c5a66caf93939fb23ec1ab7c0b364e233c673113 Component: cli --- components/cli/network_connect_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/components/cli/network_connect_test.go b/components/cli/network_connect_test.go index d472f4520c..91b1a76676 100644 --- a/components/cli/network_connect_test.go +++ b/components/cli/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 b9c1f91e0801b079a85bcd3a1af19c2b4e236f63 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Thu, 9 Feb 2017 14:54:05 -0800 Subject: [PATCH 590/978] print 'worker' join token after swarm init Signed-off-by: Victor Vieux Upstream-commit: 9e940b9020237f13308d2ab1441a011e85abac70 Component: cli --- components/cli/command/swarm/init.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/swarm/init.go b/components/cli/command/swarm/init.go index b796022672..59bfa5b620 100644 --- a/components/cli/command/swarm/init.go +++ b/components/cli/command/swarm/init.go @@ -78,7 +78,7 @@ func runInit(dockerCli command.Cli, flags *pflag.FlagSet, opts initOptions) erro fmt.Fprintf(dockerCli.Out(), "Swarm initialized: current node (%s) is now a manager.\n\n", nodeID) - if err := printJoinCommand(ctx, dockerCli, nodeID, false, true); err != nil { + if err := printJoinCommand(ctx, dockerCli, nodeID, true, false); err != nil { return err } From 9ac6e27b96d17fac1747b3edf3a94ad971176367 Mon Sep 17 00:00:00 2001 From: yupengzte Date: Fri, 10 Feb 2017 14:53:18 +0800 Subject: [PATCH 591/978] Add = before the value of PublishedPort Signed-off-by: yupengzte Upstream-commit: 04bea1696925f86460d591843b1be71204f162a9 Component: cli --- components/cli/command/formatter/service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/formatter/service.go b/components/cli/command/formatter/service.go index 8e38cb3a11..09f4368f4e 100644 --- a/components/cli/command/formatter/service.go +++ b/components/cli/command/formatter/service.go @@ -102,7 +102,7 @@ Endpoint Mode: {{ .EndpointMode }} {{- if .Ports }} Ports: {{- range $port := .Ports }} - PublishedPort {{ $port.PublishedPort }} + PublishedPort = {{ $port.PublishedPort }} Protocol = {{ $port.Protocol }} TargetPort = {{ $port.TargetPort }} PublishMode = {{ $port.PublishMode }} From 7cd5a4ceb51cd14336426f4e6e05affd27c38c1d Mon Sep 17 00:00:00 2001 From: "Aaron.L.Xu" Date: Fri, 10 Feb 2017 15:35:05 +0800 Subject: [PATCH 592/978] review code about cmd/* and fix some easy typos :D Signed-off-by: Aaron.L.Xu Upstream-commit: 0d8fd8584279cbcfaa090da96404ef4a6f8c8993 Component: cli --- components/cli/docker.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/docker.go b/components/cli/docker.go index f992b08469..654c7f41d6 100644 --- a/components/cli/docker.go +++ b/components/cli/docker.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "os" + "strings" "github.com/Sirupsen/logrus" "github.com/docker/docker/api/types/versions" @@ -17,7 +18,6 @@ import ( "github.com/docker/docker/pkg/term" "github.com/spf13/cobra" "github.com/spf13/pflag" - "strings" ) func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command { @@ -55,7 +55,7 @@ func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command { cli.SetupRootCommand(cmd) cmd.SetHelpFunc(func(ccmd *cobra.Command, args []string) { - if dockerCli.Client() == nil { // when using --help, PersistenPreRun is not called, so initialization is needed. + if dockerCli.Client() == nil { // when using --help, PersistentPreRun is not called, so initialization is needed. // flags must be the top-level command flags, not cmd.Flags() opts.Common.SetDefaultOptions(flags) dockerPreRun(opts) From c28cd54770f9b50cc53cc0534320df029c028868 Mon Sep 17 00:00:00 2001 From: allencloud Date: Fri, 10 Feb 2017 16:30:25 +0800 Subject: [PATCH 593/978] remove unused headers in secret_create.go Signed-off-by: allencloud Upstream-commit: a8f833a646d941b199cfafe4fc2035a1dbfec534 Component: cli --- components/cli/secret_create.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/components/cli/secret_create.go b/components/cli/secret_create.go index de8b041567..b5325a560f 100644 --- a/components/cli/secret_create.go +++ b/components/cli/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 060813f997c75cffb45aa8ce7113c65cba8fe8de Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 6 Nov 2016 21:54:40 -0800 Subject: [PATCH 594/978] Add `--format` to `docker service ps` This fix tries to address the issue raised in 27189 where it is not possible to support configured formatting stored in config.json. Since `--format` was not supported in `docker service ps`, the flag `--format` has also been added in this fix. This fix 1. Add `--format` to `docker service ps` 2. Add `tasksFormat` to config.json 3. Add `--format` to `docker stack ps` 4. Add `--format` to `docker node ps` The related docs has been updated. An integration test has been added. This fix fixes 27189. Signed-off-by: Yong Tang Upstream-commit: 53bdc98713739cb1f6d08d52d535e361de6442f2 Component: cli --- components/cli/command/cli.go | 1 + components/cli/command/formatter/task.go | 145 ++++++++++++++++++ components/cli/command/formatter/task_test.go | 107 +++++++++++++ components/cli/command/node/ps.go | 16 +- components/cli/command/service/ps.go | 15 +- components/cli/command/stack/ps.go | 16 +- components/cli/command/task/print.go | 105 ++----------- components/cli/config/configfile/file.go | 1 + 8 files changed, 311 insertions(+), 95 deletions(-) create mode 100644 components/cli/command/formatter/task.go create mode 100644 components/cli/command/formatter/task_test.go diff --git a/components/cli/command/cli.go b/components/cli/command/cli.go index bf9d554608..782c3a5074 100644 --- a/components/cli/command/cli.go +++ b/components/cli/command/cli.go @@ -38,6 +38,7 @@ type Cli interface { Out() *OutStream Err() io.Writer In() *InStream + ConfigFile() *configfile.ConfigFile } // DockerCli is an instance the docker command line client. diff --git a/components/cli/command/formatter/task.go b/components/cli/command/formatter/task.go new file mode 100644 index 0000000000..caf7651515 --- /dev/null +++ b/components/cli/command/formatter/task.go @@ -0,0 +1,145 @@ +package formatter + +import ( + "fmt" + "strings" + "time" + + "github.com/docker/distribution/reference" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/go-units" +) + +const ( + defaultTaskTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Image}}\t{{.Node}}\t{{.DesiredState}}\t{{.CurrentState}}\t{{.Error}}\t{{.Ports}}" + + nodeHeader = "NODE" + taskIDHeader = "ID" + desiredStateHeader = "DESIRED STATE" + currentStateHeader = "CURRENT STATE" + errorHeader = "ERROR" + + maxErrLength = 30 +) + +// NewTaskFormat returns a Format for rendering using a task Context +func NewTaskFormat(source string, quiet bool) Format { + switch source { + case TableFormatKey: + if quiet { + return defaultQuietFormat + } + return defaultTaskTableFormat + case RawFormatKey: + if quiet { + return `id: {{.ID}}` + } + return `id: {{.ID}}\nname: {{.Name}}\nimage: {{.Image}}\nnode: {{.Node}}\ndesired_state: {{.DesiredState}}\ncurrent_state: {{.CurrentState}}\nerror: {{.Error}}\nports: {{.Ports}}\n` + } + return Format(source) +} + +// TaskWrite writes the context +func TaskWrite(ctx Context, tasks []swarm.Task, names map[string]string, nodes map[string]string) error { + render := func(format func(subContext subContext) error) error { + for _, task := range tasks { + taskCtx := &taskContext{trunc: ctx.Trunc, task: task, name: names[task.ID], node: nodes[task.ID]} + if err := format(taskCtx); err != nil { + return err + } + } + return nil + } + return ctx.Write(&taskContext{}, render) +} + +type taskContext struct { + HeaderContext + trunc bool + task swarm.Task + name string + node string +} + +func (c *taskContext) MarshalJSON() ([]byte, error) { + return marshalJSON(c) +} + +func (c *taskContext) ID() string { + c.AddHeader(taskIDHeader) + if c.trunc { + return stringid.TruncateID(c.task.ID) + } + return c.task.ID +} + +func (c *taskContext) Name() string { + c.AddHeader(nameHeader) + return c.name +} + +func (c *taskContext) Image() string { + c.AddHeader(imageHeader) + image := c.task.Spec.ContainerSpec.Image + if c.trunc { + ref, err := reference.ParseNormalizedNamed(image) + if err == nil { + // update image string for display, (strips any digest) + if nt, ok := ref.(reference.NamedTagged); ok { + if namedTagged, err := reference.WithTag(reference.TrimNamed(nt), nt.Tag()); err == nil { + image = reference.FamiliarString(namedTagged) + } + } + } + } + return image +} + +func (c *taskContext) Node() string { + c.AddHeader(nodeHeader) + return c.node +} + +func (c *taskContext) DesiredState() string { + c.AddHeader(desiredStateHeader) + return command.PrettyPrint(c.task.DesiredState) +} + +func (c *taskContext) CurrentState() string { + c.AddHeader(currentStateHeader) + return fmt.Sprintf("%s %s ago", + command.PrettyPrint(c.task.Status.State), + strings.ToLower(units.HumanDuration(time.Since(c.task.Status.Timestamp))), + ) +} + +func (c *taskContext) Error() string { + c.AddHeader(errorHeader) + // Trim and quote the error message. + taskErr := c.task.Status.Err + if c.trunc && len(taskErr) > maxErrLength { + taskErr = fmt.Sprintf("%s…", taskErr[:maxErrLength-1]) + } + if len(taskErr) > 0 { + taskErr = fmt.Sprintf("\"%s\"", taskErr) + } + return taskErr +} + +func (c *taskContext) Ports() string { + c.AddHeader(portsHeader) + if len(c.task.Status.PortStatus.Ports) == 0 { + return "" + } + ports := []string{} + for _, pConfig := range c.task.Status.PortStatus.Ports { + ports = append(ports, fmt.Sprintf("*:%d->%d/%s", + pConfig.PublishedPort, + pConfig.TargetPort, + pConfig.Protocol, + )) + } + return strings.Join(ports, ",") +} diff --git a/components/cli/command/formatter/task_test.go b/components/cli/command/formatter/task_test.go new file mode 100644 index 0000000000..c990f68619 --- /dev/null +++ b/components/cli/command/formatter/task_test.go @@ -0,0 +1,107 @@ +package formatter + +import ( + "bytes" + "encoding/json" + "strings" + "testing" + + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestTaskContextWrite(t *testing.T) { + cases := []struct { + context Context + expected string + }{ + { + Context{Format: "{{InvalidFunction}}"}, + `Template parsing error: template: :1: function "InvalidFunction" not defined +`, + }, + { + Context{Format: "{{nil}}"}, + `Template parsing error: template: :1:2: executing "" at : nil is not a command +`, + }, + { + Context{Format: NewTaskFormat("table", true)}, + `taskID1 +taskID2 +`, + }, + { + Context{Format: NewTaskFormat("table {{.Name}} {{.Node}} {{.Ports}}", false)}, + `NAME NODE PORTS +foobar_baz foo1 +foobar_bar foo2 +`, + }, + { + Context{Format: NewTaskFormat("table {{.Name}}", true)}, + `NAME +foobar_baz +foobar_bar +`, + }, + { + Context{Format: NewTaskFormat("raw", true)}, + `id: taskID1 +id: taskID2 +`, + }, + { + Context{Format: NewTaskFormat("{{.Name}} {{.Node}}", false)}, + `foobar_baz foo1 +foobar_bar foo2 +`, + }, + } + + for _, testcase := range cases { + tasks := []swarm.Task{ + {ID: "taskID1"}, + {ID: "taskID2"}, + } + names := map[string]string{ + "taskID1": "foobar_baz", + "taskID2": "foobar_bar", + } + nodes := map[string]string{ + "taskID1": "foo1", + "taskID2": "foo2", + } + out := bytes.NewBufferString("") + testcase.context.Output = out + err := TaskWrite(testcase.context, tasks, names, nodes) + if err != nil { + assert.Error(t, err, testcase.expected) + } else { + assert.Equal(t, out.String(), testcase.expected) + } + } +} + +func TestTaskContextWriteJSONField(t *testing.T) { + tasks := []swarm.Task{ + {ID: "taskID1"}, + {ID: "taskID2"}, + } + names := map[string]string{ + "taskID1": "foobar_baz", + "taskID2": "foobar_bar", + } + out := bytes.NewBufferString("") + err := TaskWrite(Context{Format: "{{json .ID}}", Output: out}, tasks, names, map[string]string{}) + if err != nil { + t.Fatal(err) + } + for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { + var s string + if err := json.Unmarshal([]byte(line), &s); err != nil { + t.Fatal(err) + } + assert.Equal(t, s, tasks[i].ID) + } +} diff --git a/components/cli/command/node/ps.go b/components/cli/command/node/ps.go index 52ac36646e..cb0f3efdfc 100644 --- a/components/cli/command/node/ps.go +++ b/components/cli/command/node/ps.go @@ -8,6 +8,7 @@ import ( "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/formatter" "github.com/docker/docker/cli/command/idresolver" "github.com/docker/docker/cli/command/task" "github.com/docker/docker/opts" @@ -19,6 +20,8 @@ type psOptions struct { nodeIDs []string noResolve bool noTrunc bool + quiet bool + format string filter opts.FilterOpt } @@ -43,6 +46,8 @@ func newPsCommand(dockerCli command.Cli) *cobra.Command { flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output") flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names") flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") + flags.StringVar(&opts.format, "format", "", "Pretty-print tasks using a Go template") + flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display task IDs") return cmd } @@ -81,7 +86,16 @@ func runPs(dockerCli command.Cli, opts psOptions) error { tasks = append(tasks, nodeTasks...) } - if err := task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), opts.noTrunc); err != nil { + format := opts.format + if len(format) == 0 { + if dockerCli.ConfigFile() != nil && len(dockerCli.ConfigFile().TasksFormat) > 0 && !opts.quiet { + format = dockerCli.ConfigFile().TasksFormat + } else { + format = formatter.TableFormatKey + } + } + + if err := task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), !opts.noTrunc, opts.quiet, format); err != nil { errs = append(errs, err.Error()) } diff --git a/components/cli/command/service/ps.go b/components/cli/command/service/ps.go index 12b25bf4f6..c4ff1b9e3f 100644 --- a/components/cli/command/service/ps.go +++ b/components/cli/command/service/ps.go @@ -10,6 +10,7 @@ import ( "github.com/docker/docker/api/types/filters" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/formatter" "github.com/docker/docker/cli/command/idresolver" "github.com/docker/docker/cli/command/node" "github.com/docker/docker/cli/command/task" @@ -22,6 +23,7 @@ type psOptions struct { quiet bool noResolve bool noTrunc bool + format string filter opts.FilterOpt } @@ -41,6 +43,7 @@ func newPsCommand(dockerCli *command.DockerCli) *cobra.Command { flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display task IDs") flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output") flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names") + flags.StringVar(&opts.format, "format", "", "Pretty-print tasks using a Go template") flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") return cmd @@ -107,8 +110,14 @@ func runPS(dockerCli *command.DockerCli, opts psOptions) error { return err } - if opts.quiet { - return task.PrintQuiet(dockerCli, tasks) + format := opts.format + if len(format) == 0 { + if len(dockerCli.ConfigFile().TasksFormat) > 0 && !opts.quiet { + format = dockerCli.ConfigFile().TasksFormat + } else { + format = formatter.TableFormatKey + } } - return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), opts.noTrunc) + + return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), !opts.noTrunc, opts.quiet, format) } diff --git a/components/cli/command/stack/ps.go b/components/cli/command/stack/ps.go index 7bbcf54205..bac5307bd1 100644 --- a/components/cli/command/stack/ps.go +++ b/components/cli/command/stack/ps.go @@ -8,6 +8,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/formatter" "github.com/docker/docker/cli/command/idresolver" "github.com/docker/docker/cli/command/task" "github.com/docker/docker/opts" @@ -19,6 +20,8 @@ type psOptions struct { noTrunc bool namespace string noResolve bool + quiet bool + format string } func newPsCommand(dockerCli *command.DockerCli) *cobra.Command { @@ -37,6 +40,8 @@ func newPsCommand(dockerCli *command.DockerCli) *cobra.Command { flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output") flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names") flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") + flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display task IDs") + flags.StringVar(&opts.format, "format", "", "Pretty-print tasks using a Go template") return cmd } @@ -58,5 +63,14 @@ func runPS(dockerCli *command.DockerCli, opts psOptions) error { return nil } - return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), opts.noTrunc) + format := opts.format + if len(format) == 0 { + if len(dockerCli.ConfigFile().TasksFormat) > 0 && !opts.quiet { + format = dockerCli.ConfigFile().TasksFormat + } else { + format = formatter.TableFormatKey + } + } + + return task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), !opts.noTrunc, opts.quiet, format) } diff --git a/components/cli/command/task/print.go b/components/cli/command/task/print.go index d7e20bb59a..3df3b2985a 100644 --- a/components/cli/command/task/print.go +++ b/components/cli/command/task/print.go @@ -2,42 +2,16 @@ package task import ( "fmt" - "io" "sort" - "strings" - "text/tabwriter" - "time" "golang.org/x/net/context" - "github.com/docker/distribution/reference" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/formatter" "github.com/docker/docker/cli/command/idresolver" - "github.com/docker/docker/pkg/stringid" - "github.com/docker/go-units" ) -const ( - psTaskItemFmt = "%s\t%s\t%s\t%s\t%s\t%s %s ago\t%s\t%s\n" - maxErrLength = 30 -) - -type portStatus swarm.PortStatus - -func (ps portStatus) String() string { - if len(ps.Ports) == 0 { - return "" - } - - str := fmt.Sprintf("*:%d->%d/%s", ps.Ports[0].PublishedPort, ps.Ports[0].TargetPort, ps.Ports[0].Protocol) - for _, pConfig := range ps.Ports[1:] { - str += fmt.Sprintf(",*:%d->%d/%s", pConfig.PublishedPort, pConfig.TargetPort, pConfig.Protocol) - } - - return str -} - type tasksBySlot []swarm.Task func (t tasksBySlot) Len() int { @@ -58,42 +32,23 @@ func (t tasksBySlot) Less(i, j int) bool { return t[j].Meta.CreatedAt.Before(t[i].CreatedAt) } -// Print task information in a table format. +// Print task information in a format. // Besides this, command `docker node ps ` // and `docker stack ps` will call this, too. -func Print(dockerCli command.Cli, ctx context.Context, tasks []swarm.Task, resolver *idresolver.IDResolver, noTrunc bool) error { +func Print(dockerCli command.Cli, ctx context.Context, tasks []swarm.Task, resolver *idresolver.IDResolver, trunc, quiet bool, format string) error { sort.Stable(tasksBySlot(tasks)) - writer := tabwriter.NewWriter(dockerCli.Out(), 0, 4, 2, ' ', 0) + names := map[string]string{} + nodes := map[string]string{} - // Ignore flushing errors - defer writer.Flush() - fmt.Fprintln(writer, strings.Join([]string{"ID", "NAME", "IMAGE", "NODE", "DESIRED STATE", "CURRENT STATE", "ERROR", "PORTS"}, "\t")) - - return print(writer, ctx, tasks, resolver, noTrunc) -} - -// PrintQuiet shows task list in a quiet way. -func PrintQuiet(dockerCli command.Cli, tasks []swarm.Task) error { - sort.Stable(tasksBySlot(tasks)) - - out := dockerCli.Out() - - for _, task := range tasks { - fmt.Fprintln(out, task.ID) + tasksCtx := formatter.Context{ + Output: dockerCli.Out(), + Format: formatter.NewTaskFormat(format, quiet), + Trunc: trunc, } - return nil -} - -func print(out io.Writer, ctx context.Context, tasks []swarm.Task, resolver *idresolver.IDResolver, noTrunc bool) error { prevName := "" for _, task := range tasks { - id := task.ID - if !noTrunc { - id = stringid.TruncateID(id) - } - serviceName, err := resolver.Resolve(ctx, swarm.Service{}, task.ServiceID) if err != nil { return err @@ -118,42 +73,12 @@ func print(out io.Writer, ctx context.Context, tasks []swarm.Task, resolver *idr } prevName = name - // Trim and quote the error message. - taskErr := task.Status.Err - if !noTrunc && len(taskErr) > maxErrLength { - taskErr = fmt.Sprintf("%s…", taskErr[:maxErrLength-1]) + names[task.ID] = name + if tasksCtx.Format.IsTable() { + names[task.ID] = indentedName } - if len(taskErr) > 0 { - taskErr = fmt.Sprintf("\"%s\"", taskErr) - } - - image := task.Spec.ContainerSpec.Image - if !noTrunc { - ref, err := reference.ParseNormalizedNamed(image) - if err == nil { - // update image string for display, (strips any digest) - if nt, ok := ref.(reference.NamedTagged); ok { - if namedTagged, err := reference.WithTag(reference.TrimNamed(nt), nt.Tag()); err == nil { - image = reference.FamiliarString(namedTagged) - } - } - - } - } - - fmt.Fprintf( - out, - psTaskItemFmt, - id, - indentedName, - image, - nodeValue, - command.PrettyPrint(task.DesiredState), - command.PrettyPrint(task.Status.State), - strings.ToLower(units.HumanDuration(time.Since(task.Status.Timestamp))), - taskErr, - portStatus(task.Status.PortStatus), - ) + nodes[task.ID] = nodeValue } - return nil + + return formatter.TaskWrite(tasksCtx, tasks, names, nodes) } diff --git a/components/cli/config/configfile/file.go b/components/cli/config/configfile/file.go index c321b97f2e..d83434676e 100644 --- a/components/cli/config/configfile/file.go +++ b/components/cli/config/configfile/file.go @@ -36,6 +36,7 @@ type ConfigFile struct { Filename string `json:"-"` // Note: for internal use only ServiceInspectFormat string `json:"serviceInspectFormat,omitempty"` ServicesFormat string `json:"servicesFormat,omitempty"` + TasksFormat string `json:"tasksFormat,omitempty"` } // LegacyLoadFromReader reads the non-nested configuration data given and sets up the From bb163c30efce0565dbb0856e87b4b76b5dedb266 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 20 Nov 2016 09:57:06 -0800 Subject: [PATCH 595/978] Improve error handling of experimental features in non-experimental mode This fix tries to address several issues raised in 28626 where run against a non-experimental daemon may not generate correct error message: 1. Incorrect flags were not checked against the supported features: ``` $ docker stack --nonsense unknown flag: --nonsense ``` 2. Subcommands were not checked against the supported features: ``` $ docker stack ls Error response from daemon: This node is not a swarm manager... ``` This fix address the above mentioned issues by: 1. Add a pre-check for FlagErrorFunc 2. Recursively check if a feature is supported for cmd and its parents. This fix fixes 28626. Signed-off-by: Yong Tang Upstream-commit: 8e688f17a35a01023b58b0da56b917a7d4345c38 Component: cli --- components/cli/docker.go | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/components/cli/docker.go b/components/cli/docker.go index 654c7f41d6..44e6529b82 100644 --- a/components/cli/docker.go +++ b/components/cli/docker.go @@ -53,6 +53,23 @@ func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command { }, } cli.SetupRootCommand(cmd) + // When invoking `docker stack --nonsense`, we need to make sure FlagErrorFunc return appropriate + // output if the feature is not supported. + // As above cli.SetupRootCommand(cmd) have already setup the FlagErrorFunc, we will add a pre-check before the FlagErrorFunc + // is called. + flagErrorFunc := cmd.FlagErrorFunc() + cmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error { + if dockerCli.Client() == nil { // when using --help, PersistenPreRun is not called, so initialization is needed. + // flags must be the top-level command flags, not cmd.Flags() + opts.Common.SetDefaultOptions(flags) + dockerPreRun(opts) + dockerCli.Initialize(opts) + } + if err := isSupported(cmd, dockerCli.Client().ClientVersion(), dockerCli.HasExperimental()); err != nil { + return err + } + return flagErrorFunc(cmd, err) + }) cmd.SetHelpFunc(func(ccmd *cobra.Command, args []string) { if dockerCli.Client() == nil { // when using --help, PersistentPreRun is not called, so initialization is needed. @@ -167,9 +184,12 @@ func hideUnsupportedFeatures(cmd *cobra.Command, clientVersion string, hasExperi } func isSupported(cmd *cobra.Command, clientVersion string, hasExperimental bool) error { + // We check recursively so that, e.g., `docker stack ls` will return the same output as `docker stack` if !hasExperimental { - if _, ok := cmd.Tags["experimental"]; ok { - return errors.New("only supported on a Docker daemon with experimental features enabled") + for curr := cmd; curr != nil; curr = curr.Parent() { + if _, ok := curr.Tags["experimental"]; ok { + return errors.New("only supported on a Docker daemon with experimental features enabled") + } } } From 0ea3cc9974402086d6f0b1a36945a40c141dad85 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Mon, 21 Nov 2016 14:34:55 -0800 Subject: [PATCH 596/978] Additional experimental features in non-experimental mode error handling This fix is the follow up of the last commit. In this fix: 1. If any of the parents of a command has tags, then this command's `Args` (Args validation func) will be wrapped up. The warpped up func will check to see if the feature is supported or not. If it is not supported, then a not supported message is generated instead. This fix is related to 28626. Signed-off-by: Yong Tang Upstream-commit: d5010088e37f577f0b476b927fc8550150d107a0 Component: cli --- components/cli/docker.go | 99 +++++++++++++++++++++++++++++++--------- 1 file changed, 78 insertions(+), 21 deletions(-) diff --git a/components/cli/docker.go b/components/cli/docker.go index 44e6529b82..efc1cac25e 100644 --- a/components/cli/docker.go +++ b/components/cli/docker.go @@ -53,32 +53,43 @@ func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command { }, } cli.SetupRootCommand(cmd) + + flags = cmd.Flags() + flags.BoolVarP(&opts.Version, "version", "v", false, "Print version information and quit") + flags.StringVar(&opts.ConfigDir, "config", cliconfig.Dir(), "Location of client config files") + opts.Common.InstallFlags(flags) + + setFlagErrorFunc(dockerCli, cmd, flags, opts) + + setHelpFunc(dockerCli, cmd, flags, opts) + + cmd.SetOutput(dockerCli.Out()) + cmd.AddCommand(newDaemonCommand()) + commands.AddCommands(cmd, dockerCli) + + setValidateArgs(dockerCli, cmd, flags, opts) + + return cmd +} + +func setFlagErrorFunc(dockerCli *command.DockerCli, cmd *cobra.Command, flags *pflag.FlagSet, opts *cliflags.ClientOptions) { // When invoking `docker stack --nonsense`, we need to make sure FlagErrorFunc return appropriate // output if the feature is not supported. // As above cli.SetupRootCommand(cmd) have already setup the FlagErrorFunc, we will add a pre-check before the FlagErrorFunc // is called. flagErrorFunc := cmd.FlagErrorFunc() cmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error { - if dockerCli.Client() == nil { // when using --help, PersistenPreRun is not called, so initialization is needed. - // flags must be the top-level command flags, not cmd.Flags() - opts.Common.SetDefaultOptions(flags) - dockerPreRun(opts) - dockerCli.Initialize(opts) - } + initializeDockerCli(dockerCli, flags, opts) if err := isSupported(cmd, dockerCli.Client().ClientVersion(), dockerCli.HasExperimental()); err != nil { return err } return flagErrorFunc(cmd, err) }) +} +func setHelpFunc(dockerCli *command.DockerCli, cmd *cobra.Command, flags *pflag.FlagSet, opts *cliflags.ClientOptions) { cmd.SetHelpFunc(func(ccmd *cobra.Command, args []string) { - if dockerCli.Client() == nil { // when using --help, PersistentPreRun is not called, so initialization is needed. - // flags must be the top-level command flags, not cmd.Flags() - opts.Common.SetDefaultOptions(flags) - dockerPreRun(opts) - dockerCli.Initialize(opts) - } - + initializeDockerCli(dockerCli, flags, opts) if err := isSupported(ccmd, dockerCli.Client().ClientVersion(), dockerCli.HasExperimental()); err != nil { ccmd.Println(err) return @@ -90,17 +101,52 @@ func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command { ccmd.Println(err) } }) +} - flags = cmd.Flags() - flags.BoolVarP(&opts.Version, "version", "v", false, "Print version information and quit") - flags.StringVar(&opts.ConfigDir, "config", cliconfig.Dir(), "Location of client config files") - opts.Common.InstallFlags(flags) +func setValidateArgs(dockerCli *command.DockerCli, cmd *cobra.Command, flags *pflag.FlagSet, opts *cliflags.ClientOptions) { + // The Args is handled by ValidateArgs in cobra, which does not allows a pre-hook. + // As a result, here we replace the existing Args validation func to a wrapper, + // where the wrapper will check to see if the feature is supported or not. + // The Args validation error will only be returned if the feature is supported. + visitAll(cmd, func(ccmd *cobra.Command) { + // if there is no tags for a command or any of its parent, + // there is no need to wrap the Args validation. + if !hasTags(ccmd) { + return + } - cmd.SetOutput(dockerCli.Out()) - cmd.AddCommand(newDaemonCommand()) - commands.AddCommands(cmd, dockerCli) + if ccmd.Args == nil { + return + } - return cmd + cmdArgs := ccmd.Args + ccmd.Args = func(cmd *cobra.Command, args []string) error { + initializeDockerCli(dockerCli, flags, opts) + if err := isSupported(cmd, dockerCli.Client().ClientVersion(), dockerCli.HasExperimental()); err != nil { + return err + } + return cmdArgs(cmd, args) + } + }) +} + +func initializeDockerCli(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *cliflags.ClientOptions) { + if dockerCli.Client() == nil { // when using --help, PersistentPreRun is not called, so initialization is needed. + // flags must be the top-level command flags, not cmd.Flags() + opts.Common.SetDefaultOptions(flags) + dockerPreRun(opts) + dockerCli.Initialize(opts) + } +} + +// visitAll will traverse all commands from the root. +// This is different from the VisitAll of cobra.Command where only parents +// are checked. +func visitAll(root *cobra.Command, fn func(*cobra.Command)) { + for _, cmd := range root.Commands() { + visitAll(cmd, fn) + } + fn(root) } func noArgs(cmd *cobra.Command, args []string) error { @@ -230,3 +276,14 @@ func isFlagSupported(f *pflag.Flag, clientVersion string) bool { } return true } + +// hasTags return true if any of the command's parents has tags +func hasTags(cmd *cobra.Command) bool { + for curr := cmd; curr != nil; curr = curr.Parent() { + if len(curr.Tags) > 0 { + return true + } + } + + return false +} From 1737a521394e09d7c6e77151381d953837dcb390 Mon Sep 17 00:00:00 2001 From: allencloud Date: Fri, 10 Feb 2017 17:16:34 +0800 Subject: [PATCH 597/978] remove redundant code and better error msg Signed-off-by: allencloud Upstream-commit: bb22446a68fa9e113d6bdb32d9b42d3e0a7845b7 Component: cli --- components/cli/swarm_unlock.go | 4 ---- components/cli/volume_prune.go | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/components/cli/swarm_unlock.go b/components/cli/swarm_unlock.go index addfb59f0a..502c6b8407 100644 --- a/components/cli/swarm_unlock.go +++ b/components/cli/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/components/cli/volume_prune.go b/components/cli/volume_prune.go index a07e4ce637..53a31ee39b 100644 --- a/components/cli/volume_prune.go +++ b/components/cli/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 2c4a067a4b0eff1d4adec741c52712d76fe0c169 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Fri, 3 Feb 2017 16:48:46 -0800 Subject: [PATCH 598/978] Allow `--format` to use different delim in `table` format This fix is an attempt to address https://github.com/docker/docker/pull/28213#issuecomment-273840405 Currently when specify table format with table `--format "table {{.ID}}..."`, the delimiter in the header section of the table is always `"\t"`. That is actually different from the content of the table as the delimiter could be anything (or even contatenated with `.`, for example): ``` $ docker service ps web --format 'table {{.Name}}.{{.ID}}' --no-trunc NAME ID web.1.inyhxhvjcijl0hdbu8lgrwwh7 \_ web.1.p9m4kx2srjqmfms4igam0uqlb ``` This fix is an attampt to address the skewness of the table when delimiter is not `"\t"`. The basic idea is that, when header consists of `table` key, the header section will be redendered the same way as content section. A map mapping each placeholder name to the HEADER entry name is used for the context of the header. Unit tests have been updated and added to cover the changes. This fix is related to #28313. Signed-off-by: Yong Tang Upstream-commit: 9dda1155f3acc1d6e7f2532d569758f6c11228d6 Component: cli --- components/cli/command/formatter/container.go | 58 +++++++++-------- .../cli/command/formatter/container_test.go | 63 +++++++------------ components/cli/command/formatter/custom.go | 26 ++------ .../cli/command/formatter/disk_usage.go | 31 ++++----- .../cli/command/formatter/disk_usage_test.go | 56 +++++++++++++++++ components/cli/command/formatter/formatter.go | 14 ++--- components/cli/command/formatter/image.go | 51 ++++++++++----- .../cli/command/formatter/image_test.go | 26 +++----- components/cli/command/formatter/network.go | 37 ++++++----- .../cli/command/formatter/network_test.go | 26 +++----- components/cli/command/formatter/plugin.go | 15 +++-- .../cli/command/formatter/plugin_test.go | 14 ++--- components/cli/command/formatter/service.go | 15 +++-- components/cli/command/formatter/stats.go | 33 +++++----- .../cli/command/formatter/stats_test.go | 5 -- components/cli/command/formatter/volume.go | 40 +++++++----- .../cli/command/formatter/volume_test.go | 18 ++---- 17 files changed, 281 insertions(+), 247 deletions(-) create mode 100644 components/cli/command/formatter/disk_usage_test.go diff --git a/components/cli/command/formatter/container.go b/components/cli/command/formatter/container.go index 6273453355..c8cb7b69e0 100644 --- a/components/cli/command/formatter/container.go +++ b/components/cli/command/formatter/container.go @@ -14,7 +14,7 @@ import ( ) const ( - defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}} ago\t{{.Status}}\t{{.Ports}}\t{{.Names}}" + defaultContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.RunningFor}}\t{{.Status}}\t{{.Ports}}\t{{.Names}}" containerIDHeader = "CONTAINER ID" namesHeader = "NAMES" @@ -71,7 +71,17 @@ func ContainerWrite(ctx Context, containers []types.Container) error { } return nil } - return ctx.Write(&containerContext{}, render) + return ctx.Write(newContainerContext(), render) +} + +type containerHeaderContext map[string]string + +func (c containerHeaderContext) Label(name string) string { + n := strings.Split(name, ".") + r := strings.NewReplacer("-", " ", "_", " ") + h := r.Replace(n[len(n)-1]) + + return h } type containerContext struct { @@ -80,12 +90,31 @@ type containerContext struct { c types.Container } +func newContainerContext() *containerContext { + containerCtx := containerContext{} + containerCtx.header = containerHeaderContext{ + "ID": containerIDHeader, + "Names": namesHeader, + "Image": imageHeader, + "Command": commandHeader, + "CreatedAt": createdAtHeader, + "RunningFor": runningForHeader, + "Ports": portsHeader, + "Status": statusHeader, + "Size": sizeHeader, + "Labels": labelsHeader, + "Mounts": mountsHeader, + "LocalVolumes": localVolumes, + "Networks": networksHeader, + } + return &containerCtx +} + func (c *containerContext) MarshalJSON() ([]byte, error) { return marshalJSON(c) } func (c *containerContext) ID() string { - c.AddHeader(containerIDHeader) if c.trunc { return stringid.TruncateID(c.c.ID) } @@ -93,7 +122,6 @@ func (c *containerContext) ID() string { } func (c *containerContext) Names() string { - c.AddHeader(namesHeader) names := stripNamePrefix(c.c.Names) if c.trunc { for _, name := range names { @@ -107,7 +135,6 @@ func (c *containerContext) Names() string { } func (c *containerContext) Image() string { - c.AddHeader(imageHeader) if c.c.Image == "" { return "" } @@ -120,7 +147,6 @@ func (c *containerContext) Image() string { } func (c *containerContext) Command() string { - c.AddHeader(commandHeader) command := c.c.Command if c.trunc { command = stringutils.Ellipsis(command, 20) @@ -129,28 +155,23 @@ func (c *containerContext) Command() string { } func (c *containerContext) CreatedAt() string { - c.AddHeader(createdAtHeader) return time.Unix(int64(c.c.Created), 0).String() } func (c *containerContext) RunningFor() string { - c.AddHeader(runningForHeader) createdAt := time.Unix(int64(c.c.Created), 0) - return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago" } func (c *containerContext) Ports() string { - c.AddHeader(portsHeader) return api.DisplayablePorts(c.c.Ports) } func (c *containerContext) Status() string { - c.AddHeader(statusHeader) return c.c.Status } func (c *containerContext) Size() string { - c.AddHeader(sizeHeader) srw := units.HumanSizeWithPrecision(float64(c.c.SizeRw), 3) sv := units.HumanSizeWithPrecision(float64(c.c.SizeRootFs), 3) @@ -162,7 +183,6 @@ func (c *containerContext) Size() string { } func (c *containerContext) Labels() string { - c.AddHeader(labelsHeader) if c.c.Labels == nil { return "" } @@ -175,12 +195,6 @@ func (c *containerContext) Labels() string { } func (c *containerContext) Label(name string) string { - n := strings.Split(name, ".") - r := strings.NewReplacer("-", " ", "_", " ") - h := r.Replace(n[len(n)-1]) - - c.AddHeader(h) - if c.c.Labels == nil { return "" } @@ -188,8 +202,6 @@ func (c *containerContext) Label(name string) string { } func (c *containerContext) Mounts() string { - c.AddHeader(mountsHeader) - var name string var mounts []string for _, m := range c.c.Mounts { @@ -207,8 +219,6 @@ func (c *containerContext) Mounts() string { } func (c *containerContext) LocalVolumes() string { - c.AddHeader(localVolumes) - count := 0 for _, m := range c.c.Mounts { if m.Driver == "local" { @@ -220,8 +230,6 @@ func (c *containerContext) LocalVolumes() string { } func (c *containerContext) Networks() string { - c.AddHeader(networksHeader) - if c.c.NetworkSettings == nil { return "" } diff --git a/components/cli/command/formatter/container_test.go b/components/cli/command/formatter/container_test.go index f013328158..ef6e86c597 100644 --- a/components/cli/command/formatter/container_test.go +++ b/components/cli/command/formatter/container_test.go @@ -22,22 +22,20 @@ func TestContainerPsContext(t *testing.T) { container types.Container trunc bool expValue string - expHeader string call func() string }{ - {types.Container{ID: containerID}, true, stringid.TruncateID(containerID), containerIDHeader, ctx.ID}, - {types.Container{ID: containerID}, false, containerID, containerIDHeader, ctx.ID}, - {types.Container{Names: []string{"/foobar_baz"}}, true, "foobar_baz", namesHeader, ctx.Names}, - {types.Container{Image: "ubuntu"}, true, "ubuntu", imageHeader, ctx.Image}, - {types.Container{Image: "verylongimagename"}, true, "verylongimagename", imageHeader, ctx.Image}, - {types.Container{Image: "verylongimagename"}, false, "verylongimagename", imageHeader, ctx.Image}, + {types.Container{ID: containerID}, true, stringid.TruncateID(containerID), ctx.ID}, + {types.Container{ID: containerID}, false, containerID, ctx.ID}, + {types.Container{Names: []string{"/foobar_baz"}}, true, "foobar_baz", ctx.Names}, + {types.Container{Image: "ubuntu"}, true, "ubuntu", ctx.Image}, + {types.Container{Image: "verylongimagename"}, true, "verylongimagename", ctx.Image}, + {types.Container{Image: "verylongimagename"}, false, "verylongimagename", ctx.Image}, {types.Container{ Image: "a5a665ff33eced1e0803148700880edab4", ImageID: "a5a665ff33eced1e0803148700880edab4269067ed77e27737a708d0d293fbf5", }, true, "a5a665ff33ec", - imageHeader, ctx.Image, }, {types.Container{ @@ -46,19 +44,18 @@ func TestContainerPsContext(t *testing.T) { }, false, "a5a665ff33eced1e0803148700880edab4", - imageHeader, ctx.Image, }, - {types.Container{Image: ""}, true, "", imageHeader, ctx.Image}, - {types.Container{Command: "sh -c 'ls -la'"}, true, `"sh -c 'ls -la'"`, commandHeader, ctx.Command}, - {types.Container{Created: unix}, true, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt}, - {types.Container{Ports: []types.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, true, "8080/tcp", portsHeader, ctx.Ports}, - {types.Container{Status: "RUNNING"}, true, "RUNNING", statusHeader, ctx.Status}, - {types.Container{SizeRw: 10}, true, "10B", sizeHeader, ctx.Size}, - {types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10B (virtual 20B)", sizeHeader, ctx.Size}, - {types.Container{}, true, "", labelsHeader, ctx.Labels}, - {types.Container{Labels: map[string]string{"cpu": "6", "storage": "ssd"}}, true, "cpu=6,storage=ssd", labelsHeader, ctx.Labels}, - {types.Container{Created: unix}, true, "About a minute", runningForHeader, ctx.RunningFor}, + {types.Container{Image: ""}, true, "", ctx.Image}, + {types.Container{Command: "sh -c 'ls -la'"}, true, `"sh -c 'ls -la'"`, ctx.Command}, + {types.Container{Created: unix}, true, time.Unix(unix, 0).String(), ctx.CreatedAt}, + {types.Container{Ports: []types.Port{{PrivatePort: 8080, PublicPort: 8080, Type: "tcp"}}}, true, "8080/tcp", ctx.Ports}, + {types.Container{Status: "RUNNING"}, true, "RUNNING", ctx.Status}, + {types.Container{SizeRw: 10}, true, "10B", ctx.Size}, + {types.Container{SizeRw: 10, SizeRootFs: 20}, true, "10B (virtual 20B)", ctx.Size}, + {types.Container{}, true, "", ctx.Labels}, + {types.Container{Labels: map[string]string{"cpu": "6", "storage": "ssd"}}, true, "cpu=6,storage=ssd", ctx.Labels}, + {types.Container{Created: unix}, true, "About a minute ago", ctx.RunningFor}, {types.Container{ Mounts: []types.MountPoint{ { @@ -67,7 +64,7 @@ func TestContainerPsContext(t *testing.T) { Source: "/a/path", }, }, - }, true, "this-is-a-lo...", mountsHeader, ctx.Mounts}, + }, true, "this-is-a-lo...", ctx.Mounts}, {types.Container{ Mounts: []types.MountPoint{ { @@ -75,7 +72,7 @@ func TestContainerPsContext(t *testing.T) { Source: "/a/path", }, }, - }, false, "/a/path", mountsHeader, ctx.Mounts}, + }, false, "/a/path", ctx.Mounts}, {types.Container{ Mounts: []types.MountPoint{ { @@ -84,7 +81,7 @@ func TestContainerPsContext(t *testing.T) { Source: "/a/path", }, }, - }, false, "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203", mountsHeader, ctx.Mounts}, + }, false, "733908409c91817de8e92b0096373245f329f19a88e2c849f02460e9b3d1c203", ctx.Mounts}, } for _, c := range cases { @@ -95,11 +92,6 @@ func TestContainerPsContext(t *testing.T) { } else if v != c.expValue { t.Fatalf("Expected %s, was %s\n", c.expValue, v) } - - h := ctx.FullHeader() - if h != c.expHeader { - t.Fatalf("Expected %s, was %s\n", c.expHeader, h) - } } c1 := types.Container{Labels: map[string]string{"com.docker.swarm.swarm-id": "33", "com.docker.swarm.node_name": "ubuntu"}} @@ -115,12 +107,6 @@ func TestContainerPsContext(t *testing.T) { t.Fatalf("Expected ubuntu, was %s\n", node) } - h := ctx.FullHeader() - if h != "SWARM ID\tNODE NAME" { - t.Fatalf("Expected %s, was %s\n", "SWARM ID\tNODE NAME", h) - - } - c2 := types.Container{} ctx = containerContext{c: c2, trunc: true} @@ -128,13 +114,6 @@ func TestContainerPsContext(t *testing.T) { if label != "" { t.Fatalf("Expected an empty string, was %s", label) } - - ctx = containerContext{c: c2, trunc: true} - FullHeader := ctx.FullHeader() - if FullHeader != "" { - t.Fatalf("Expected FullHeader to be empty, was %s", FullHeader) - } - } func TestContainerContextWrite(t *testing.T) { @@ -333,8 +312,8 @@ func TestContainerContextWriteJSON(t *testing.T) { } expectedCreated := time.Unix(unix, 0).String() expectedJSONs := []map[string]interface{}{ - {"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID1", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_baz", "Networks": "", "Ports": "", "RunningFor": "About a minute", "Size": "0B", "Status": ""}, - {"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID2", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_bar", "Networks": "", "Ports": "", "RunningFor": "About a minute", "Size": "0B", "Status": ""}, + {"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID1", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_baz", "Networks": "", "Ports": "", "RunningFor": "About a minute ago", "Size": "0B", "Status": ""}, + {"Command": "\"\"", "CreatedAt": expectedCreated, "ID": "containerID2", "Image": "ubuntu", "Labels": "", "LocalVolumes": "0", "Mounts": "", "Names": "foobar_bar", "Networks": "", "Ports": "", "RunningFor": "About a minute ago", "Size": "0B", "Status": ""}, } out := bytes.NewBufferString("") err := ContainerWrite(Context{Format: "{{json .}}", Output: out}, containers) diff --git a/components/cli/command/formatter/custom.go b/components/cli/command/formatter/custom.go index df32684429..73487f63ef 100644 --- a/components/cli/command/formatter/custom.go +++ b/components/cli/command/formatter/custom.go @@ -1,9 +1,5 @@ package formatter -import ( - "strings" -) - const ( imageHeader = "IMAGE" createdSinceHeader = "CREATED" @@ -16,29 +12,17 @@ const ( ) type subContext interface { - FullHeader() string - AddHeader(header string) + FullHeader() interface{} } // HeaderContext provides the subContext interface for managing headers type HeaderContext struct { - header []string + header interface{} } -// FullHeader returns the header as a string -func (c *HeaderContext) FullHeader() string { - if c.header == nil { - return "" - } - return strings.Join(c.header, "\t") -} - -// AddHeader adds another column to the header -func (c *HeaderContext) AddHeader(header string) { - if c.header == nil { - c.header = []string{} - } - c.header = append(c.header, strings.ToUpper(header)) +// FullHeader returns the header as an interface +func (c *HeaderContext) FullHeader() interface{} { + return c.header } func stripNamePrefix(ss []string) []string { diff --git a/components/cli/command/formatter/disk_usage.go b/components/cli/command/formatter/disk_usage.go index fd7aabc7c2..7170411e1b 100644 --- a/components/cli/command/formatter/disk_usage.go +++ b/components/cli/command/formatter/disk_usage.go @@ -77,7 +77,15 @@ func (ctx *DiskUsageContext) Write() { return } - ctx.postFormat(tmpl, &diskUsageContainersContext{containers: []*types.Container{}}) + diskUsageContainersCtx := diskUsageContainersContext{containers: []*types.Container{}} + diskUsageContainersCtx.header = map[string]string{ + "Type": typeHeader, + "TotalCount": totalHeader, + "Active": activeHeader, + "Size": sizeHeader, + "Reclaimable": reclaimableHeader, + } + ctx.postFormat(tmpl, &diskUsageContainersCtx) return } @@ -114,7 +122,7 @@ func (ctx *DiskUsageContext) Write() { return } } - ctx.postFormat(tmpl, &imageContext{}) + ctx.postFormat(tmpl, newImageContext()) // Now containers ctx.Output.Write([]byte("\nContainers space usage:\n\n")) @@ -133,7 +141,7 @@ func (ctx *DiskUsageContext) Write() { return } } - ctx.postFormat(tmpl, &containerContext{}) + ctx.postFormat(tmpl, newContainerContext()) // And volumes ctx.Output.Write([]byte("\nLocal Volumes space usage:\n\n")) @@ -149,7 +157,7 @@ func (ctx *DiskUsageContext) Write() { return } } - ctx.postFormat(tmpl, &volumeContext{v: types.Volume{}}) + ctx.postFormat(tmpl, newVolumeContext()) } type diskUsageImagesContext struct { @@ -163,17 +171,14 @@ func (c *diskUsageImagesContext) MarshalJSON() ([]byte, error) { } func (c *diskUsageImagesContext) Type() string { - c.AddHeader(typeHeader) return "Images" } func (c *diskUsageImagesContext) TotalCount() string { - c.AddHeader(totalHeader) return fmt.Sprintf("%d", len(c.images)) } func (c *diskUsageImagesContext) Active() string { - c.AddHeader(activeHeader) used := 0 for _, i := range c.images { if i.Containers > 0 { @@ -185,7 +190,6 @@ func (c *diskUsageImagesContext) Active() string { } func (c *diskUsageImagesContext) Size() string { - c.AddHeader(sizeHeader) return units.HumanSize(float64(c.totalSize)) } @@ -193,7 +197,6 @@ func (c *diskUsageImagesContext) Size() string { func (c *diskUsageImagesContext) Reclaimable() string { var used int64 - c.AddHeader(reclaimableHeader) for _, i := range c.images { if i.Containers != 0 { if i.VirtualSize == -1 || i.SharedSize == -1 { @@ -221,12 +224,10 @@ func (c *diskUsageContainersContext) MarshalJSON() ([]byte, error) { } func (c *diskUsageContainersContext) Type() string { - c.AddHeader(typeHeader) return "Containers" } func (c *diskUsageContainersContext) TotalCount() string { - c.AddHeader(totalHeader) return fmt.Sprintf("%d", len(c.containers)) } @@ -237,7 +238,6 @@ func (c *diskUsageContainersContext) isActive(container types.Container) bool { } func (c *diskUsageContainersContext) Active() string { - c.AddHeader(activeHeader) used := 0 for _, container := range c.containers { if c.isActive(*container) { @@ -251,7 +251,6 @@ func (c *diskUsageContainersContext) Active() string { func (c *diskUsageContainersContext) Size() string { var size int64 - c.AddHeader(sizeHeader) for _, container := range c.containers { size += container.SizeRw } @@ -263,7 +262,6 @@ func (c *diskUsageContainersContext) Reclaimable() string { var reclaimable int64 var totalSize int64 - c.AddHeader(reclaimableHeader) for _, container := range c.containers { if !c.isActive(*container) { reclaimable += container.SizeRw @@ -289,17 +287,14 @@ func (c *diskUsageVolumesContext) MarshalJSON() ([]byte, error) { } func (c *diskUsageVolumesContext) Type() string { - c.AddHeader(typeHeader) return "Local Volumes" } func (c *diskUsageVolumesContext) TotalCount() string { - c.AddHeader(totalHeader) return fmt.Sprintf("%d", len(c.volumes)) } func (c *diskUsageVolumesContext) Active() string { - c.AddHeader(activeHeader) used := 0 for _, v := range c.volumes { @@ -314,7 +309,6 @@ func (c *diskUsageVolumesContext) Active() string { func (c *diskUsageVolumesContext) Size() string { var size int64 - c.AddHeader(sizeHeader) for _, v := range c.volumes { if v.UsageData.Size != -1 { size += v.UsageData.Size @@ -328,7 +322,6 @@ func (c *diskUsageVolumesContext) Reclaimable() string { var reclaimable int64 var totalSize int64 - c.AddHeader(reclaimableHeader) for _, v := range c.volumes { if v.UsageData.Size != -1 { if v.UsageData.RefCount == 0 { diff --git a/components/cli/command/formatter/disk_usage_test.go b/components/cli/command/formatter/disk_usage_test.go new file mode 100644 index 0000000000..06d1c2c1fe --- /dev/null +++ b/components/cli/command/formatter/disk_usage_test.go @@ -0,0 +1,56 @@ +package formatter + +import ( + "bytes" + //"encoding/json" + //"strings" + "testing" + //"time" + + //"github.com/docker/docker/api/types" + //"github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestDiskUsageContextFormatWrite(t *testing.T) { + // Check default output format (verbose and non-verbose mode) for table headers + cases := []struct { + context DiskUsageContext + expected string + }{ + { + DiskUsageContext{Verbose: false}, + `TYPE TOTAL ACTIVE SIZE RECLAIMABLE +Images 0 0 0B 0B +Containers 0 0 0B 0B +Local Volumes 0 0 0B 0B +`, + }, + { + DiskUsageContext{Verbose: true}, + `Images space usage: + +REPOSITORY TAG IMAGE ID CREATED ago SIZE SHARED SIZE UNIQUE SiZE CONTAINERS + +Containers space usage: + +CONTAINER ID IMAGE COMMAND LOCAL VOLUMES SIZE CREATED ago STATUS NAMES + +Local Volumes space usage: + +VOLUME NAME LINKS SIZE +`, + }, + } + + for _, testcase := range cases { + //networks := []types.NetworkResource{ + // {ID: "networkID1", Name: "foobar_baz", Driver: "foo", Scope: "local", Created: timestamp1}, + // {ID: "networkID2", Name: "foobar_bar", Driver: "bar", Scope: "local", Created: timestamp2}, + //} + out := bytes.NewBufferString("") + testcase.context.Output = out + testcase.context.Write() + assert.Equal(t, out.String(), testcase.expected) + } +} diff --git a/components/cli/command/formatter/formatter.go b/components/cli/command/formatter/formatter.go index 4345f7c3bc..16e8e6af2c 100644 --- a/components/cli/command/formatter/formatter.go +++ b/components/cli/command/formatter/formatter.go @@ -44,7 +44,7 @@ type Context struct { // internal element finalFormat string - header string + header interface{} buffer *bytes.Buffer } @@ -71,14 +71,10 @@ func (c *Context) parseFormat() (*template.Template, error) { func (c *Context) postFormat(tmpl *template.Template, subContext subContext) { if c.Format.IsTable() { - if len(c.header) == 0 { - // if we still don't have a header, we didn't have any containers so we need to fake it to get the right headers from the template - tmpl.Execute(bytes.NewBufferString(""), subContext) - c.header = subContext.FullHeader() - } - t := tabwriter.NewWriter(c.Output, 20, 1, 3, ' ', 0) - t.Write([]byte(c.header)) + buffer := bytes.NewBufferString("") + tmpl.Execute(buffer, subContext.FullHeader()) + buffer.WriteTo(t) t.Write([]byte("\n")) c.buffer.WriteTo(t) t.Flush() @@ -91,7 +87,7 @@ func (c *Context) contextFormat(tmpl *template.Template, subContext subContext) if err := tmpl.Execute(c.buffer, subContext); err != nil { return fmt.Errorf("Template parsing error: %v\n", err) } - if c.Format.IsTable() && len(c.header) == 0 { + if c.Format.IsTable() && c.header != nil { c.header = subContext.FullHeader() } c.buffer.WriteString("\n") diff --git a/components/cli/command/formatter/image.go b/components/cli/command/formatter/image.go index b6508224a3..8f18045c11 100644 --- a/components/cli/command/formatter/image.go +++ b/components/cli/command/formatter/image.go @@ -11,8 +11,8 @@ import ( ) const ( - defaultImageTableFormat = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.Size}}" - defaultImageTableFormatWithDigest = "table {{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.ID}}\t{{.CreatedSince}} ago\t{{.Size}}" + defaultImageTableFormat = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}}\t{{.Size}}" + defaultImageTableFormatWithDigest = "table {{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.ID}}\t{{.CreatedSince}}\t{{.Size}}" imageIDHeader = "IMAGE ID" repositoryHeader = "REPOSITORY" @@ -76,7 +76,21 @@ func ImageWrite(ctx ImageContext, images []types.ImageSummary) error { render := func(format func(subContext subContext) error) error { return imageFormat(ctx, images, format) } - return ctx.Write(&imageContext{}, render) + imageCtx := imageContext{} + imageCtx.header = map[string]string{ + "ID": imageIDHeader, + "Repository": repositoryHeader, + "Tag": tagHeader, + "Digest": digestHeader, + "CreatedSince": createdSinceHeader, + "CreatedAt": createdAtHeader, + "Size": sizeHeader, + "Containers": containersHeader, + "VirtualSize": sizeHeader, + "SharedSize": sharedSizeHeader, + "UniqueSize": uniqueSizeHeader, + } + return ctx.Write(newImageContext(), render) } func imageFormat(ctx ImageContext, images []types.ImageSummary, format func(subContext subContext) error) error { @@ -192,12 +206,29 @@ type imageContext struct { digest string } +func newImageContext() *imageContext { + imageCtx := imageContext{} + imageCtx.header = map[string]string{ + "ID": imageIDHeader, + "Repository": repositoryHeader, + "Tag": tagHeader, + "Digest": digestHeader, + "CreatedSince": createdSinceHeader, + "CreatedAt": createdAtHeader, + "Size": sizeHeader, + "Containers": containersHeader, + "VirtualSize": sizeHeader, + "SharedSize": sharedSizeHeader, + "UniqueSize": uniqueSizeHeader, + } + return &imageCtx +} + func (c *imageContext) MarshalJSON() ([]byte, error) { return marshalJSON(c) } func (c *imageContext) ID() string { - c.AddHeader(imageIDHeader) if c.trunc { return stringid.TruncateID(c.i.ID) } @@ -205,38 +236,31 @@ func (c *imageContext) ID() string { } func (c *imageContext) Repository() string { - c.AddHeader(repositoryHeader) return c.repo } func (c *imageContext) Tag() string { - c.AddHeader(tagHeader) return c.tag } func (c *imageContext) Digest() string { - c.AddHeader(digestHeader) return c.digest } func (c *imageContext) CreatedSince() string { - c.AddHeader(createdSinceHeader) createdAt := time.Unix(int64(c.i.Created), 0) - return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + return units.HumanDuration(time.Now().UTC().Sub(createdAt)) + " ago" } func (c *imageContext) CreatedAt() string { - c.AddHeader(createdAtHeader) return time.Unix(int64(c.i.Created), 0).String() } func (c *imageContext) Size() string { - c.AddHeader(sizeHeader) return units.HumanSizeWithPrecision(float64(c.i.Size), 3) } func (c *imageContext) Containers() string { - c.AddHeader(containersHeader) if c.i.Containers == -1 { return "N/A" } @@ -244,12 +268,10 @@ func (c *imageContext) Containers() string { } func (c *imageContext) VirtualSize() string { - c.AddHeader(sizeHeader) return units.HumanSize(float64(c.i.VirtualSize)) } func (c *imageContext) SharedSize() string { - c.AddHeader(sharedSizeHeader) if c.i.SharedSize == -1 { return "N/A" } @@ -257,7 +279,6 @@ func (c *imageContext) SharedSize() string { } func (c *imageContext) UniqueSize() string { - c.AddHeader(uniqueSizeHeader) if c.i.VirtualSize == -1 || c.i.SharedSize == -1 { return "N/A" } diff --git a/components/cli/command/formatter/image_test.go b/components/cli/command/formatter/image_test.go index cf134300a1..e7c15dbf5a 100644 --- a/components/cli/command/formatter/image_test.go +++ b/components/cli/command/formatter/image_test.go @@ -18,27 +18,26 @@ func TestImageContext(t *testing.T) { var ctx imageContext cases := []struct { - imageCtx imageContext - expValue string - expHeader string - call func() string + imageCtx imageContext + expValue string + call func() string }{ {imageContext{ i: types.ImageSummary{ID: imageID}, trunc: true, - }, stringid.TruncateID(imageID), imageIDHeader, ctx.ID}, + }, stringid.TruncateID(imageID), ctx.ID}, {imageContext{ i: types.ImageSummary{ID: imageID}, trunc: false, - }, imageID, imageIDHeader, ctx.ID}, + }, imageID, ctx.ID}, {imageContext{ i: types.ImageSummary{Size: 10, VirtualSize: 10}, trunc: true, - }, "10B", sizeHeader, ctx.Size}, + }, "10B", ctx.Size}, {imageContext{ i: types.ImageSummary{Created: unix}, trunc: true, - }, time.Unix(unix, 0).String(), createdAtHeader, ctx.CreatedAt}, + }, time.Unix(unix, 0).String(), ctx.CreatedAt}, // FIXME // {imageContext{ // i: types.ImageSummary{Created: unix}, @@ -47,15 +46,15 @@ func TestImageContext(t *testing.T) { {imageContext{ i: types.ImageSummary{}, repo: "busybox", - }, "busybox", repositoryHeader, ctx.Repository}, + }, "busybox", ctx.Repository}, {imageContext{ i: types.ImageSummary{}, tag: "latest", - }, "latest", tagHeader, ctx.Tag}, + }, "latest", ctx.Tag}, {imageContext{ i: types.ImageSummary{}, digest: "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", - }, "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", digestHeader, ctx.Digest}, + }, "sha256:d149ab53f8718e987c3a3024bb8aa0e2caadf6c0328f1d9d850b2a2a67f2819a", ctx.Digest}, } for _, c := range cases { @@ -66,11 +65,6 @@ func TestImageContext(t *testing.T) { } else if v != c.expValue { t.Fatalf("Expected %s, was %s\n", c.expValue, v) } - - h := ctx.FullHeader() - if h != c.expHeader { - t.Fatalf("Expected %s, was %s\n", c.expHeader, h) - } } } diff --git a/components/cli/command/formatter/network.go b/components/cli/command/formatter/network.go index c29be412aa..4aeebd1750 100644 --- a/components/cli/command/formatter/network.go +++ b/components/cli/command/formatter/network.go @@ -44,7 +44,28 @@ func NetworkWrite(ctx Context, networks []types.NetworkResource) error { } return nil } - return ctx.Write(&networkContext{}, render) + networkCtx := networkContext{} + networkCtx.header = networkHeaderContext{ + "ID": networkIDHeader, + "Name": nameHeader, + "Driver": driverHeader, + "Scope": scopeHeader, + "IPv6": ipv6Header, + "Internal": internalHeader, + "Labels": labelsHeader, + "CreatedAt": createdAtHeader, + } + return ctx.Write(&networkCtx, render) +} + +type networkHeaderContext map[string]string + +func (c networkHeaderContext) Label(name string) string { + n := strings.Split(name, ".") + r := strings.NewReplacer("-", " ", "_", " ") + h := r.Replace(n[len(n)-1]) + + return h } type networkContext struct { @@ -58,7 +79,6 @@ func (c *networkContext) MarshalJSON() ([]byte, error) { } func (c *networkContext) ID() string { - c.AddHeader(networkIDHeader) if c.trunc { return stringid.TruncateID(c.n.ID) } @@ -66,32 +86,26 @@ func (c *networkContext) ID() string { } func (c *networkContext) Name() string { - c.AddHeader(nameHeader) return c.n.Name } func (c *networkContext) Driver() string { - c.AddHeader(driverHeader) return c.n.Driver } func (c *networkContext) Scope() string { - c.AddHeader(scopeHeader) return c.n.Scope } func (c *networkContext) IPv6() string { - c.AddHeader(ipv6Header) return fmt.Sprintf("%v", c.n.EnableIPv6) } func (c *networkContext) Internal() string { - c.AddHeader(internalHeader) return fmt.Sprintf("%v", c.n.Internal) } func (c *networkContext) Labels() string { - c.AddHeader(labelsHeader) if c.n.Labels == nil { return "" } @@ -104,12 +118,6 @@ func (c *networkContext) Labels() string { } func (c *networkContext) Label(name string) string { - n := strings.Split(name, ".") - r := strings.NewReplacer("-", " ", "_", " ") - h := r.Replace(n[len(n)-1]) - - c.AddHeader(h) - if c.n.Labels == nil { return "" } @@ -117,6 +125,5 @@ func (c *networkContext) Label(name string) string { } func (c *networkContext) CreatedAt() string { - c.AddHeader(createdAtHeader) return c.n.Created.String() } diff --git a/components/cli/command/formatter/network_test.go b/components/cli/command/formatter/network_test.go index e105afbdf8..24bf46d256 100644 --- a/components/cli/command/formatter/network_test.go +++ b/components/cli/command/formatter/network_test.go @@ -19,41 +19,40 @@ func TestNetworkContext(t *testing.T) { cases := []struct { networkCtx networkContext expValue string - expHeader string call func() string }{ {networkContext{ n: types.NetworkResource{ID: networkID}, trunc: false, - }, networkID, networkIDHeader, ctx.ID}, + }, networkID, ctx.ID}, {networkContext{ n: types.NetworkResource{ID: networkID}, trunc: true, - }, stringid.TruncateID(networkID), networkIDHeader, ctx.ID}, + }, stringid.TruncateID(networkID), ctx.ID}, {networkContext{ n: types.NetworkResource{Name: "network_name"}, - }, "network_name", nameHeader, ctx.Name}, + }, "network_name", ctx.Name}, {networkContext{ n: types.NetworkResource{Driver: "driver_name"}, - }, "driver_name", driverHeader, ctx.Driver}, + }, "driver_name", ctx.Driver}, {networkContext{ n: types.NetworkResource{EnableIPv6: true}, - }, "true", ipv6Header, ctx.IPv6}, + }, "true", ctx.IPv6}, {networkContext{ n: types.NetworkResource{EnableIPv6: false}, - }, "false", ipv6Header, ctx.IPv6}, + }, "false", ctx.IPv6}, {networkContext{ n: types.NetworkResource{Internal: true}, - }, "true", internalHeader, ctx.Internal}, + }, "true", ctx.Internal}, {networkContext{ n: types.NetworkResource{Internal: false}, - }, "false", internalHeader, ctx.Internal}, + }, "false", ctx.Internal}, {networkContext{ n: types.NetworkResource{}, - }, "", labelsHeader, ctx.Labels}, + }, "", ctx.Labels}, {networkContext{ n: types.NetworkResource{Labels: map[string]string{"label1": "value1", "label2": "value2"}}, - }, "label1=value1,label2=value2", labelsHeader, ctx.Labels}, + }, "label1=value1,label2=value2", ctx.Labels}, } for _, c := range cases { @@ -64,11 +63,6 @@ func TestNetworkContext(t *testing.T) { } else if v != c.expValue { t.Fatalf("Expected %s, was %s\n", c.expValue, v) } - - h := ctx.FullHeader() - if h != c.expHeader { - t.Fatalf("Expected %s, was %s\n", c.expHeader, h) - } } } diff --git a/components/cli/command/formatter/plugin.go b/components/cli/command/formatter/plugin.go index 00bdf3d0f4..2b71281a58 100644 --- a/components/cli/command/formatter/plugin.go +++ b/components/cli/command/formatter/plugin.go @@ -44,7 +44,15 @@ func PluginWrite(ctx Context, plugins []*types.Plugin) error { } return nil } - return ctx.Write(&pluginContext{}, render) + pluginCtx := pluginContext{} + pluginCtx.header = map[string]string{ + "ID": pluginIDHeader, + "Name": nameHeader, + "Description": descriptionHeader, + "Enabled": enabledHeader, + "PluginReference": imageHeader, + } + return ctx.Write(&pluginCtx, render) } type pluginContext struct { @@ -58,7 +66,6 @@ func (c *pluginContext) MarshalJSON() ([]byte, error) { } func (c *pluginContext) ID() string { - c.AddHeader(pluginIDHeader) if c.trunc { return stringid.TruncateID(c.p.ID) } @@ -66,12 +73,10 @@ func (c *pluginContext) ID() string { } func (c *pluginContext) Name() string { - c.AddHeader(nameHeader) return c.p.Name } func (c *pluginContext) Description() string { - c.AddHeader(descriptionHeader) desc := strings.Replace(c.p.Config.Description, "\n", "", -1) desc = strings.Replace(desc, "\r", "", -1) if c.trunc { @@ -82,11 +87,9 @@ func (c *pluginContext) Description() string { } func (c *pluginContext) Enabled() bool { - c.AddHeader(enabledHeader) return c.p.Enabled } func (c *pluginContext) PluginReference() string { - c.AddHeader(imageHeader) return c.p.PluginReference } diff --git a/components/cli/command/formatter/plugin_test.go b/components/cli/command/formatter/plugin_test.go index a6c8f7e6c1..3cc0af8a3e 100644 --- a/components/cli/command/formatter/plugin_test.go +++ b/components/cli/command/formatter/plugin_test.go @@ -18,23 +18,22 @@ func TestPluginContext(t *testing.T) { cases := []struct { pluginCtx pluginContext expValue string - expHeader string call func() string }{ {pluginContext{ p: types.Plugin{ID: pluginID}, trunc: false, - }, pluginID, pluginIDHeader, ctx.ID}, + }, pluginID, ctx.ID}, {pluginContext{ p: types.Plugin{ID: pluginID}, trunc: true, - }, stringid.TruncateID(pluginID), pluginIDHeader, ctx.ID}, + }, stringid.TruncateID(pluginID), ctx.ID}, {pluginContext{ p: types.Plugin{Name: "plugin_name"}, - }, "plugin_name", nameHeader, ctx.Name}, + }, "plugin_name", ctx.Name}, {pluginContext{ p: types.Plugin{Config: types.PluginConfig{Description: "plugin_description"}}, - }, "plugin_description", descriptionHeader, ctx.Description}, + }, "plugin_description", ctx.Description}, } for _, c := range cases { @@ -45,11 +44,6 @@ func TestPluginContext(t *testing.T) { } else if v != c.expValue { t.Fatalf("Expected %s, was %s\n", c.expValue, v) } - - h := ctx.FullHeader() - if h != c.expHeader { - t.Fatalf("Expected %s, was %s\n", c.expHeader, h) - } } } diff --git a/components/cli/command/formatter/service.go b/components/cli/command/formatter/service.go index 8e38cb3a11..f7d78154ef 100644 --- a/components/cli/command/formatter/service.go +++ b/components/cli/command/formatter/service.go @@ -372,7 +372,15 @@ func ServiceListWrite(ctx Context, services []swarm.Service, info map[string]Ser } return nil } - return ctx.Write(&serviceContext{}, render) + serviceCtx := serviceContext{} + serviceCtx.header = map[string]string{ + "ID": serviceIDHeader, + "Name": nameHeader, + "Mode": modeHeader, + "Replicas": replicasHeader, + "Image": imageHeader, + } + return ctx.Write(&serviceCtx, render) } type serviceContext struct { @@ -387,27 +395,22 @@ func (c *serviceContext) MarshalJSON() ([]byte, error) { } func (c *serviceContext) ID() string { - c.AddHeader(serviceIDHeader) return stringid.TruncateID(c.service.ID) } func (c *serviceContext) Name() string { - c.AddHeader(nameHeader) return c.service.Spec.Name } func (c *serviceContext) Mode() string { - c.AddHeader(modeHeader) return c.mode } func (c *serviceContext) Replicas() string { - c.AddHeader(replicasHeader) return c.replicas } func (c *serviceContext) Image() string { - c.AddHeader(imageHeader) image := c.service.Spec.TaskTemplate.ContainerSpec.Image if ref, err := reference.ParseNormalizedNamed(image); err == nil { // update image string for display, (strips any digest) diff --git a/components/cli/command/formatter/stats.go b/components/cli/command/formatter/stats.go index 750f57eb43..c0151101a0 100644 --- a/components/cli/command/formatter/stats.go +++ b/components/cli/command/formatter/stats.go @@ -129,7 +129,24 @@ func ContainerStatsWrite(ctx Context, containerStats []StatsEntry, osType string } return nil } - return ctx.Write(&containerStatsContext{os: osType}, render) + memUsage := memUseHeader + if osType == winOSType { + memUsage = winMemUseHeader + } + containerStatsCtx := containerStatsContext{} + containerStatsCtx.header = map[string]string{ + "Container": containerHeader, + "Name": nameHeader, + "ID": containerIDHeader, + "CPUPerc": cpuPercHeader, + "MemUsage": memUsage, + "MemPerc": memPercHeader, + "NetIO": netIOHeader, + "BlockIO": blockIOHeader, + "PIDs": pidsHeader, + } + containerStatsCtx.os = osType + return ctx.Write(&containerStatsCtx, render) } type containerStatsContext struct { @@ -143,12 +160,10 @@ func (c *containerStatsContext) MarshalJSON() ([]byte, error) { } func (c *containerStatsContext) Container() string { - c.AddHeader(containerHeader) return c.s.Container } func (c *containerStatsContext) Name() string { - c.AddHeader(nameHeader) if len(c.s.Name) > 1 { return c.s.Name[1:] } @@ -156,12 +171,10 @@ func (c *containerStatsContext) Name() string { } func (c *containerStatsContext) ID() string { - c.AddHeader(containerIDHeader) return c.s.ID } func (c *containerStatsContext) CPUPerc() string { - c.AddHeader(cpuPercHeader) if c.s.IsInvalid { return fmt.Sprintf("--") } @@ -169,11 +182,6 @@ func (c *containerStatsContext) CPUPerc() string { } func (c *containerStatsContext) MemUsage() string { - header := memUseHeader - if c.os == winOSType { - header = winMemUseHeader - } - c.AddHeader(header) if c.s.IsInvalid { return fmt.Sprintf("-- / --") } @@ -184,8 +192,6 @@ func (c *containerStatsContext) MemUsage() string { } func (c *containerStatsContext) MemPerc() string { - header := memPercHeader - c.AddHeader(header) if c.s.IsInvalid || c.os == winOSType { return fmt.Sprintf("--") } @@ -193,7 +199,6 @@ func (c *containerStatsContext) MemPerc() string { } func (c *containerStatsContext) NetIO() string { - c.AddHeader(netIOHeader) if c.s.IsInvalid { return fmt.Sprintf("--") } @@ -201,7 +206,6 @@ func (c *containerStatsContext) NetIO() string { } func (c *containerStatsContext) BlockIO() string { - c.AddHeader(blockIOHeader) if c.s.IsInvalid { return fmt.Sprintf("--") } @@ -209,7 +213,6 @@ func (c *containerStatsContext) BlockIO() string { } func (c *containerStatsContext) PIDs() string { - c.AddHeader(pidsHeader) if c.s.IsInvalid || c.os == winOSType { return fmt.Sprintf("--") } diff --git a/components/cli/command/formatter/stats_test.go b/components/cli/command/formatter/stats_test.go index 9f48862b2a..5d6a91e7c9 100644 --- a/components/cli/command/formatter/stats_test.go +++ b/components/cli/command/formatter/stats_test.go @@ -42,11 +42,6 @@ func TestContainerStatsContext(t *testing.T) { if v := te.call(); v != te.expValue { t.Fatalf("Expected %q, got %q", te.expValue, v) } - - h := ctx.FullHeader() - if h != te.expHeader { - t.Fatalf("Expected %q, got %q", te.expHeader, h) - } } } diff --git a/components/cli/command/formatter/volume.go b/components/cli/command/formatter/volume.go index 90c9b13536..342f2fb934 100644 --- a/components/cli/command/formatter/volume.go +++ b/components/cli/command/formatter/volume.go @@ -45,7 +45,17 @@ func VolumeWrite(ctx Context, volumes []*types.Volume) error { } return nil } - return ctx.Write(&volumeContext{}, render) + return ctx.Write(newVolumeContext(), render) +} + +type volumeHeaderContext map[string]string + +func (c volumeHeaderContext) Label(name string) string { + n := strings.Split(name, ".") + r := strings.NewReplacer("-", " ", "_", " ") + h := r.Replace(n[len(n)-1]) + + return h } type volumeContext struct { @@ -53,32 +63,41 @@ type volumeContext struct { v types.Volume } +func newVolumeContext() *volumeContext { + volumeCtx := volumeContext{} + volumeCtx.header = volumeHeaderContext{ + "Name": volumeNameHeader, + "Driver": driverHeader, + "Scope": scopeHeader, + "Mountpoint": mountpointHeader, + "Labels": labelsHeader, + "Links": linksHeader, + "Size": sizeHeader, + } + return &volumeCtx +} + func (c *volumeContext) MarshalJSON() ([]byte, error) { return marshalJSON(c) } func (c *volumeContext) Name() string { - c.AddHeader(volumeNameHeader) return c.v.Name } func (c *volumeContext) Driver() string { - c.AddHeader(driverHeader) return c.v.Driver } func (c *volumeContext) Scope() string { - c.AddHeader(scopeHeader) return c.v.Scope } func (c *volumeContext) Mountpoint() string { - c.AddHeader(mountpointHeader) return c.v.Mountpoint } func (c *volumeContext) Labels() string { - c.AddHeader(labelsHeader) if c.v.Labels == nil { return "" } @@ -91,13 +110,6 @@ func (c *volumeContext) Labels() string { } func (c *volumeContext) Label(name string) string { - - n := strings.Split(name, ".") - r := strings.NewReplacer("-", " ", "_", " ") - h := r.Replace(n[len(n)-1]) - - c.AddHeader(h) - if c.v.Labels == nil { return "" } @@ -105,7 +117,6 @@ func (c *volumeContext) Label(name string) string { } func (c *volumeContext) Links() string { - c.AddHeader(linksHeader) if c.v.UsageData == nil { return "N/A" } @@ -113,7 +124,6 @@ func (c *volumeContext) Links() string { } func (c *volumeContext) Size() string { - c.AddHeader(sizeHeader) if c.v.UsageData == nil { return "N/A" } diff --git a/components/cli/command/formatter/volume_test.go b/components/cli/command/formatter/volume_test.go index 9ec18b6916..9c23ae447d 100644 --- a/components/cli/command/formatter/volume_test.go +++ b/components/cli/command/formatter/volume_test.go @@ -18,27 +18,26 @@ func TestVolumeContext(t *testing.T) { cases := []struct { volumeCtx volumeContext expValue string - expHeader string call func() string }{ {volumeContext{ v: types.Volume{Name: volumeName}, - }, volumeName, volumeNameHeader, ctx.Name}, + }, volumeName, ctx.Name}, {volumeContext{ v: types.Volume{Driver: "driver_name"}, - }, "driver_name", driverHeader, ctx.Driver}, + }, "driver_name", ctx.Driver}, {volumeContext{ v: types.Volume{Scope: "local"}, - }, "local", scopeHeader, ctx.Scope}, + }, "local", ctx.Scope}, {volumeContext{ v: types.Volume{Mountpoint: "mountpoint"}, - }, "mountpoint", mountpointHeader, ctx.Mountpoint}, + }, "mountpoint", ctx.Mountpoint}, {volumeContext{ v: types.Volume{}, - }, "", labelsHeader, ctx.Labels}, + }, "", ctx.Labels}, {volumeContext{ v: types.Volume{Labels: map[string]string{"label1": "value1", "label2": "value2"}}, - }, "label1=value1,label2=value2", labelsHeader, ctx.Labels}, + }, "label1=value1,label2=value2", ctx.Labels}, } for _, c := range cases { @@ -49,11 +48,6 @@ func TestVolumeContext(t *testing.T) { } else if v != c.expValue { t.Fatalf("Expected %s, was %s\n", c.expValue, v) } - - h := ctx.FullHeader() - if h != c.expHeader { - t.Fatalf("Expected %s, was %s\n", c.expHeader, h) - } } } From e6e37cf26c6a0a66dae81060070199378af2f015 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Fri, 3 Feb 2017 20:23:00 -0800 Subject: [PATCH 599/978] Ignore some functions in the Go template when header is rendered This fix ignore some functions in the Go template when header is redendered, so that `--format "{{truncate .ID 1}}"` will still be able to redener the header correctly. Additional test cases have been added to the unit test. Signed-off-by: Yong Tang Upstream-commit: 82bf90ffbcd9003326dd43fd4d862171e3138a06 Component: cli --- components/cli/command/formatter/container_test.go | 8 ++++++++ .../cli/command/formatter/disk_usage_test.go | 9 --------- components/cli/command/formatter/formatter.go | 2 +- components/cli/command/formatter/image.go | 14 -------------- 4 files changed, 9 insertions(+), 24 deletions(-) diff --git a/components/cli/command/formatter/container_test.go b/components/cli/command/formatter/container_test.go index ef6e86c597..a5615d1768 100644 --- a/components/cli/command/formatter/container_test.go +++ b/components/cli/command/formatter/container_test.go @@ -226,6 +226,14 @@ size: 0B Context{Format: NewContainerFormat("{{.Image}}", false, true)}, "ubuntu\nubuntu\n", }, + // Special headers for customerized table format + { + Context{Format: NewContainerFormat(`table {{truncate .ID 5}}\t{{json .Image}} {{.RunningFor}}/{{title .Status}}/{{pad .Ports 2 2}}.{{upper .Names}} {{lower .Status}}`, false, true)}, + `CONTAINER ID IMAGE CREATED/STATUS/ PORTS .NAMES STATUS +conta "ubuntu" 24 hours ago//.FOOBAR_BAZ +conta "ubuntu" 24 hours ago//.FOOBAR_BAR +`, + }, } for _, testcase := range cases { diff --git a/components/cli/command/formatter/disk_usage_test.go b/components/cli/command/formatter/disk_usage_test.go index 06d1c2c1fe..318e1692be 100644 --- a/components/cli/command/formatter/disk_usage_test.go +++ b/components/cli/command/formatter/disk_usage_test.go @@ -2,13 +2,8 @@ package formatter import ( "bytes" - //"encoding/json" - //"strings" "testing" - //"time" - //"github.com/docker/docker/api/types" - //"github.com/docker/docker/pkg/stringid" "github.com/docker/docker/pkg/testutil/assert" ) @@ -44,10 +39,6 @@ VOLUME NAME LINKS SIZE } for _, testcase := range cases { - //networks := []types.NetworkResource{ - // {ID: "networkID1", Name: "foobar_baz", Driver: "foo", Scope: "local", Created: timestamp1}, - // {ID: "networkID2", Name: "foobar_bar", Driver: "bar", Scope: "local", Created: timestamp2}, - //} out := bytes.NewBufferString("") testcase.context.Output = out testcase.context.Write() diff --git a/components/cli/command/formatter/formatter.go b/components/cli/command/formatter/formatter.go index 16e8e6af2c..a151e9c283 100644 --- a/components/cli/command/formatter/formatter.go +++ b/components/cli/command/formatter/formatter.go @@ -73,7 +73,7 @@ func (c *Context) postFormat(tmpl *template.Template, subContext subContext) { if c.Format.IsTable() { t := tabwriter.NewWriter(c.Output, 20, 1, 3, ' ', 0) buffer := bytes.NewBufferString("") - tmpl.Execute(buffer, subContext.FullHeader()) + tmpl.Funcs(templates.HeaderFunctions).Execute(buffer, subContext.FullHeader()) buffer.WriteTo(t) t.Write([]byte("\n")) c.buffer.WriteTo(t) diff --git a/components/cli/command/formatter/image.go b/components/cli/command/formatter/image.go index 8f18045c11..3aae34ea11 100644 --- a/components/cli/command/formatter/image.go +++ b/components/cli/command/formatter/image.go @@ -76,20 +76,6 @@ func ImageWrite(ctx ImageContext, images []types.ImageSummary) error { render := func(format func(subContext subContext) error) error { return imageFormat(ctx, images, format) } - imageCtx := imageContext{} - imageCtx.header = map[string]string{ - "ID": imageIDHeader, - "Repository": repositoryHeader, - "Tag": tagHeader, - "Digest": digestHeader, - "CreatedSince": createdSinceHeader, - "CreatedAt": createdAtHeader, - "Size": sizeHeader, - "Containers": containersHeader, - "VirtualSize": sizeHeader, - "SharedSize": sharedSizeHeader, - "UniqueSize": uniqueSizeHeader, - } return ctx.Write(newImageContext(), render) } From 7448bd180725dfe0b5cd87e11d917dd03989b5ac Mon Sep 17 00:00:00 2001 From: allencloud Date: Mon, 13 Feb 2017 10:16:57 +0800 Subject: [PATCH 600/978] remove redundant colon introduced by mistake Signed-off-by: allencloud Upstream-commit: 2c8cac3bd6e0d6d6f17b0aa770999eace336b152 Component: cli --- components/cli/volume_prune.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/volume_prune.go b/components/cli/volume_prune.go index 53a31ee39b..2e7fea7747 100644 --- a/components/cli/volume_prune.go +++ b/components/cli/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 193f92812e8200154b88655794960ab146b30a13 Mon Sep 17 00:00:00 2001 From: yupengzte Date: Wed, 8 Feb 2017 16:31:16 +0800 Subject: [PATCH 601/978] fix the type Signed-off-by: yupengzte Upstream-commit: 03aed78d68d80c490d360975313fcde2f7160b62 Component: cli --- components/cli/command/node/update.go | 4 ++-- components/cli/command/service/opts.go | 4 ++-- components/cli/command/swarm/init.go | 2 +- components/cli/command/swarm/join.go | 2 +- components/cli/flags/common.go | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/components/cli/command/node/update.go b/components/cli/command/node/update.go index 6ca2a7c1e3..aecb88c4ab 100644 --- a/components/cli/command/node/update.go +++ b/components/cli/command/node/update.go @@ -31,8 +31,8 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command { } flags := cmd.Flags() - flags.StringVar(&nodeOpts.role, flagRole, "", "Role of the node (worker/manager)") - flags.StringVar(&nodeOpts.availability, flagAvailability, "", "Availability of the node (active/pause/drain)") + flags.StringVar(&nodeOpts.role, flagRole, "", `Role of the node ("worker"|"manager")`) + flags.StringVar(&nodeOpts.availability, flagAvailability, "", `Availability of the node ("active"|"pause"|"drain")`) flags.Var(&nodeOpts.annotations.labels, flagLabelAdd, "Add or update a node label (key=value)") labelKeys := opts.NewListOpts(nil) flags.Var(&labelKeys, flagLabelRemove, "Remove a node label if exists") diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index f2470673a6..3b43c7042b 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -446,7 +446,7 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { flags.Var(&opts.replicas, flagReplicas, "Number of tasks") - flags.StringVar(&opts.restartPolicy.condition, flagRestartCondition, "", "Restart when condition is met (none, on-failure, or any)") + flags.StringVar(&opts.restartPolicy.condition, flagRestartCondition, "", `Restart when condition is met ("none"|"on-failure"|"any")`) flags.Var(&opts.restartPolicy.delay, flagRestartDelay, "Delay between restart attempts (ns|us|ms|s|m|h)") flags.Var(&opts.restartPolicy.maxAttempts, flagRestartMaxAttempts, "Maximum number of restarts before giving up") flags.Var(&opts.restartPolicy.window, flagRestartWindow, "Window used to evaluate the restart policy (ns|us|ms|s|m|h)") @@ -455,7 +455,7 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { flags.DurationVar(&opts.update.delay, flagUpdateDelay, time.Duration(0), "Delay between updates (ns|us|ms|s|m|h) (default 0s)") flags.DurationVar(&opts.update.monitor, flagUpdateMonitor, time.Duration(0), "Duration after each task update to monitor for failure (ns|us|ms|s|m|h) (default 0s)") flags.SetAnnotation(flagUpdateMonitor, "version", []string{"1.25"}) - flags.StringVar(&opts.update.onFailure, flagUpdateFailureAction, "pause", "Action on update failure (pause|continue)") + flags.StringVar(&opts.update.onFailure, flagUpdateFailureAction, "pause", `Action on update failure ("pause"|"continue")`) flags.Var(&opts.update.maxFailureRatio, flagUpdateMaxFailureRatio, "Failure rate to tolerate during an update") flags.SetAnnotation(flagUpdateMaxFailureRatio, "version", []string{"1.25"}) diff --git a/components/cli/command/swarm/init.go b/components/cli/command/swarm/init.go index b796022672..28fff3c8a5 100644 --- a/components/cli/command/swarm/init.go +++ b/components/cli/command/swarm/init.go @@ -42,7 +42,7 @@ func newInitCommand(dockerCli command.Cli) *cobra.Command { flags.StringVar(&opts.advertiseAddr, flagAdvertiseAddr, "", "Advertised address (format: [:port])") flags.BoolVar(&opts.forceNewCluster, "force-new-cluster", false, "Force create a new cluster from current state") flags.BoolVar(&opts.autolock, flagAutolock, false, "Enable manager autolocking (requiring an unlock key to start a stopped manager)") - flags.StringVar(&opts.availability, flagAvailability, "active", "Availability of the node (active/pause/drain)") + flags.StringVar(&opts.availability, flagAvailability, "active", `Availability of the node ("active"|"pause"|"drain")`) addSwarmFlags(flags, &opts.swarmOptions) return cmd } diff --git a/components/cli/command/swarm/join.go b/components/cli/command/swarm/join.go index 40fc5c192f..3022f6e89a 100644 --- a/components/cli/command/swarm/join.go +++ b/components/cli/command/swarm/join.go @@ -41,7 +41,7 @@ func newJoinCommand(dockerCli command.Cli) *cobra.Command { flags.Var(&opts.listenAddr, flagListenAddr, "Listen address (format: [:port])") flags.StringVar(&opts.advertiseAddr, flagAdvertiseAddr, "", "Advertised address (format: [:port])") flags.StringVar(&opts.token, flagToken, "", "Token for entry into the swarm") - flags.StringVar(&opts.availability, flagAvailability, "active", "Availability of the node (active/pause/drain)") + flags.StringVar(&opts.availability, flagAvailability, "active", `Availability of the node ("active"|"pause"|"drain")`) return cmd } diff --git a/components/cli/flags/common.go b/components/cli/flags/common.go index af2fe0603a..3c9d8fa6e9 100644 --- a/components/cli/flags/common.go +++ b/components/cli/flags/common.go @@ -53,7 +53,7 @@ func (commonOpts *CommonOptions) InstallFlags(flags *pflag.FlagSet) { } flags.BoolVarP(&commonOpts.Debug, "debug", "D", false, "Enable debug mode") - flags.StringVarP(&commonOpts.LogLevel, "log-level", "l", "info", "Set the logging level (\"debug\", \"info\", \"warn\", \"error\", \"fatal\")") + flags.StringVarP(&commonOpts.LogLevel, "log-level", "l", "info", `Set the logging level ("debug"|"info"|"warn"|"error"|"fatal")`) flags.BoolVar(&commonOpts.TLS, "tls", false, "Use TLS; implied by --tlsverify") flags.BoolVar(&commonOpts.TLSVerify, FlagTLSVerify, dockerTLSVerify, "Use TLS and verify the remote") From 200980812d3171f3c0b42608a74a9f7f45fbc0e7 Mon Sep 17 00:00:00 2001 From: "bingshen.wbs" Date: Wed, 15 Feb 2017 17:32:37 +0800 Subject: [PATCH 602/978] fix docker stack volume's nocopy parameter Signed-off-by: bingshen.wbs Upstream-commit: 6887337d86e8247e6251b8fabbb2cef118acaf3c Component: cli --- components/cli/compose/convert/volume.go | 3 +++ components/cli/compose/convert/volume_test.go | 26 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/components/cli/compose/convert/volume.go b/components/cli/compose/convert/volume.go index 24442d4dc7..53c50958fa 100644 --- a/components/cli/compose/convert/volume.go +++ b/components/cli/compose/convert/volume.go @@ -75,6 +75,9 @@ func convertVolumeToMount(volumeSpec string, stackVolumes volumes, namespace Nam var volumeOptions *mount.VolumeOptions if stackVolume.External.Name != "" { + volumeOptions = &mount.VolumeOptions{ + NoCopy: isNoCopy(mode), + } source = stackVolume.External.Name } else { volumeOptions = &mount.VolumeOptions{ diff --git a/components/cli/compose/convert/volume_test.go b/components/cli/compose/convert/volume_test.go index 1132136b22..d218e7c2f5 100644 --- a/components/cli/compose/convert/volume_test.go +++ b/components/cli/compose/convert/volume_test.go @@ -105,12 +105,38 @@ func TestConvertVolumeToMountNamedVolumeExternal(t *testing.T) { Type: mount.TypeVolume, Source: "special", Target: "/foo", + VolumeOptions: &mount.VolumeOptions{ + NoCopy: false, + }, } mount, err := convertVolumeToMount("outside:/foo", stackVolumes, namespace) assert.NilError(t, err) assert.DeepEqual(t, mount, expected) } +func TestConvertVolumeToMountNamedVolumeExternalNoCopy(t *testing.T) { + stackVolumes := volumes{ + "outside": composetypes.VolumeConfig{ + External: composetypes.External{ + External: true, + Name: "special", + }, + }, + } + namespace := NewNamespace("foo") + expected := mount.Mount{ + Type: mount.TypeVolume, + Source: "special", + Target: "/foo", + VolumeOptions: &mount.VolumeOptions{ + NoCopy: true, + }, + } + mount, err := convertVolumeToMount("outside:/foo:nocopy", stackVolumes, namespace) + assert.NilError(t, err) + assert.DeepEqual(t, mount, expected) +} + func TestConvertVolumeToMountBind(t *testing.T) { stackVolumes := volumes{} namespace := NewNamespace("foo") From d2654dd0cc8e6e370e94a6fbfcf9ff3ae11ec0e5 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Tue, 14 Feb 2017 15:12:03 +0100 Subject: [PATCH 603/978] Set 0444 as default secret mode in stack deploy Change the default secret mode to match the default one used in `service` subcommands. Signed-off-by: Vincent Demeester Upstream-commit: 645f6ba7f5700e0e9ad2ba7436bad5e87f0cafad Component: cli --- components/cli/compose/convert/service.go | 10 +++++++++- components/cli/compose/types/types.go | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/components/cli/compose/convert/service.go b/components/cli/compose/convert/service.go index ef6a04ebcf..93b910967e 100644 --- a/components/cli/compose/convert/service.go +++ b/components/cli/compose/convert/service.go @@ -217,19 +217,27 @@ func convertServiceSecrets( if gid == "" { gid = "0" } + mode := secret.Mode + if mode == nil { + mode = uint32Ptr(0444) + } opts = append(opts, &types.SecretRequestOption{ Source: source, Target: target, UID: uid, GID: gid, - Mode: os.FileMode(secret.Mode), + Mode: os.FileMode(*mode), }) } return servicecli.ParseSecrets(client, opts) } +func uint32Ptr(value uint32) *uint32 { + return &value +} + func convertExtraHosts(extraHosts map[string]string) []string { hosts := []string{} for host, ip := range extraHosts { diff --git a/components/cli/compose/types/types.go b/components/cli/compose/types/types.go index c74014fb14..058c4d09b8 100644 --- a/components/cli/compose/types/types.go +++ b/components/cli/compose/types/types.go @@ -229,7 +229,7 @@ type ServiceSecretConfig struct { Target string UID string GID string - Mode uint32 + Mode *uint32 } // UlimitsConfig the ulimit configuration From c0bbf0e6a7584959c199c9ca75fe4b2126bf4cba Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Thu, 16 Feb 2017 14:30:39 +0100 Subject: [PATCH 604/978] Sort `docker stack ls` by name Signed-off-by: Vincent Demeester Upstream-commit: ddae8d967b7fd5dc80357351e540df5700aa1656 Component: cli --- components/cli/command/stack/list.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/components/cli/command/stack/list.go b/components/cli/command/stack/list.go index 9b6c645e29..3d81242b7a 100644 --- a/components/cli/command/stack/list.go +++ b/components/cli/command/stack/list.go @@ -3,17 +3,17 @@ package stack import ( "fmt" "io" + "sort" "strconv" "text/tabwriter" - "golang.org/x/net/context" - "github.com/docker/docker/api/types" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/compose/convert" "github.com/docker/docker/client" "github.com/spf13/cobra" + "golang.org/x/net/context" ) const ( @@ -53,12 +53,20 @@ func runList(dockerCli *command.DockerCli, opts listOptions) error { return nil } +type byName []*stack + +func (n byName) Len() int { return len(n) } +func (n byName) Swap(i, j int) { n[i], n[j] = n[j], n[i] } +func (n byName) Less(i, j int) bool { return n[i].Name < n[j].Name } + func printTable(out io.Writer, stacks []*stack) { writer := tabwriter.NewWriter(out, 0, 4, 2, ' ', 0) // Ignore flushing errors defer writer.Flush() + sort.Sort(byName(stacks)) + fmt.Fprintf(writer, listItemFmt, "NAME", "SERVICES") for _, stack := range stacks { fmt.Fprintf( From 6fa6de6703485cd5d6f86b3795531bebbc90282f Mon Sep 17 00:00:00 2001 From: "Aaron.L.Xu" Date: Thu, 16 Feb 2017 23:56:53 +0800 Subject: [PATCH 605/978] why there are so many mistakes in our repo (up to /cmd) Signed-off-by: Aaron.L.Xu Upstream-commit: ca2aeb5a3e6521d146d971c5ebe4a7d46fa94086 Component: cli --- components/cli/command/container/exec.go | 2 +- components/cli/command/formatter/reflect.go | 2 +- components/cli/command/image/build.go | 2 +- components/cli/command/image/build/context.go | 2 +- components/cli/command/network/create.go | 2 +- components/cli/command/swarm/init_test.go | 2 +- components/cli/compose/types/types.go | 2 +- components/cli/config/credentials/native_store.go | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/components/cli/command/container/exec.go b/components/cli/command/container/exec.go index 73329869a6..676708c77b 100644 --- a/components/cli/command/container/exec.go +++ b/components/cli/command/container/exec.go @@ -32,7 +32,7 @@ func newExecOptions() *execOptions { } } -// NewExecCommand creats a new cobra.Command for `docker exec` +// NewExecCommand creates a new cobra.Command for `docker exec` func NewExecCommand(dockerCli *command.DockerCli) *cobra.Command { opts := newExecOptions() diff --git a/components/cli/command/formatter/reflect.go b/components/cli/command/formatter/reflect.go index d1d8737d21..9692bbce7d 100644 --- a/components/cli/command/formatter/reflect.go +++ b/components/cli/command/formatter/reflect.go @@ -22,7 +22,7 @@ func marshalMap(x interface{}) (map[string]interface{}, error) { return nil, fmt.Errorf("expected a pointer to a struct, got %v", val.Kind()) } if val.IsNil() { - return nil, fmt.Errorf("expxected a pointer to a struct, got nil pointer") + return nil, fmt.Errorf("expected a pointer to a struct, got nil pointer") } valElem := val.Elem() if valElem.Kind() != reflect.Struct { diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go index 96d90cf585..34c231d63e 100644 --- a/components/cli/command/image/build.go +++ b/components/cli/command/image/build.go @@ -120,7 +120,7 @@ func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command { // lastProgressOutput is the same as progress.Output except // that it only output with the last update. It is used in -// non terminal scenarios to depresss verbose messages +// non terminal scenarios to suppress verbose messages type lastProgressOutput struct { output progress.Output } diff --git a/components/cli/command/image/build/context.go b/components/cli/command/image/build/context.go index 86157c359d..9ea065adf8 100644 --- a/components/cli/command/image/build/context.go +++ b/components/cli/command/image/build/context.go @@ -91,7 +91,7 @@ func GetContextFromReader(r io.ReadCloser, dockerfileName string) (out io.ReadCl // Input should be read as a Dockerfile. tmpDir, err := ioutil.TempDir("", "docker-build-context-") if err != nil { - return nil, "", fmt.Errorf("unbale to create temporary context directory: %v", err) + return nil, "", fmt.Errorf("unable to create temporary context directory: %v", err) } f, err := os.Create(filepath.Join(tmpDir, DefaultDockerfileName)) diff --git a/components/cli/command/network/create.go b/components/cli/command/network/create.go index 57c59ed053..21300d7839 100644 --- a/components/cli/command/network/create.go +++ b/components/cli/command/network/create.go @@ -106,7 +106,7 @@ func runCreate(dockerCli *command.DockerCli, opts createOptions) error { // Consolidates the ipam configuration as a group from different related configurations // user can configure network with multiple non-overlapping subnets and hence it is // possible to correlate the various related parameters and consolidate them. -// consoidateIpam consolidates subnets, ip-ranges, gateways and auxiliary addresses into +// consolidateIpam consolidates subnets, ip-ranges, gateways and auxiliary addresses into // structured ipam data. func consolidateIpam(subnets, ranges, gateways []string, auxaddrs map[string]string) ([]network.IPAMConfig, error) { if len(subnets) < len(ranges) || len(subnets) < len(gateways) { diff --git a/components/cli/command/swarm/init_test.go b/components/cli/command/swarm/init_test.go index 13de1cd550..4f56de357f 100644 --- a/components/cli/command/swarm/init_test.go +++ b/components/cli/command/swarm/init_test.go @@ -31,7 +31,7 @@ func TestSwarmInitErrorOnAPIFailure(t *testing.T) { expectedError: "error initializing the swarm", }, { - name: "init-faild-with-ip-choice", + name: "init-failed-with-ip-choice", swarmInitFunc: func() (string, error) { return "", fmt.Errorf("could not choose an IP address to advertise") }, diff --git a/components/cli/compose/types/types.go b/components/cli/compose/types/types.go index c74014fb14..66c1641861 100644 --- a/components/cli/compose/types/types.go +++ b/components/cli/compose/types/types.go @@ -137,7 +137,7 @@ type StringOrNumberList []string // key=value strings type MappingWithEquals map[string]string -// MappingWithColon is a mapping type that can be converted from alist of +// MappingWithColon is a mapping type that can be converted from a list of // 'key: value' strings type MappingWithColon map[string]string diff --git a/components/cli/config/credentials/native_store.go b/components/cli/config/credentials/native_store.go index 9e0ab7f0f8..68a87e8c66 100644 --- a/components/cli/config/credentials/native_store.go +++ b/components/cli/config/credentials/native_store.go @@ -120,7 +120,7 @@ func (c *nativeStore) getCredentialsFromStore(serverAddress string) (types.AuthC if err != nil { if credentials.IsErrCredentialsNotFound(err) { // do not return an error if the credentials are not - // in the keyckain. Let docker ask for new credentials. + // in the keychain. Let docker ask for new credentials. return ret, nil } return ret, err From 6416340b1cb1a5944d414c066d2402688a62a3d1 Mon Sep 17 00:00:00 2001 From: "Aaron.L.Xu" Date: Thu, 16 Feb 2017 23:56:53 +0800 Subject: [PATCH 606/978] why there are so many mistakes in our repo (up to /cmd) Signed-off-by: Aaron.L.Xu Upstream-commit: d5e4c0d0be983f48bca4b8fd7080d85f2d8f1c4d Component: cli --- components/cli/container_create_test.go | 2 +- components/cli/image_tag_test.go | 2 +- components/cli/swarm_unlock.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/cli/container_create_test.go b/components/cli/container_create_test.go index 73474cf56f..3ab608c21e 100644 --- a/components/cli/container_create_test.go +++ b/components/cli/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/components/cli/image_tag_test.go b/components/cli/image_tag_test.go index d37bd0e85e..52c5e873a5 100644 --- a/components/cli/image_tag_test.go +++ b/components/cli/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/components/cli/swarm_unlock.go b/components/cli/swarm_unlock.go index 502c6b8407..9ee441fed2 100644 --- a/components/cli/swarm_unlock.go +++ b/components/cli/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 9ca4d7a35c56df4828cbc3f98fddd6884e6c8ce5 Mon Sep 17 00:00:00 2001 From: Nishant Totla Date: Wed, 8 Feb 2017 14:15:32 -0800 Subject: [PATCH 607/978] Suppressing image digest in docker ps Signed-off-by: Nishant Totla Upstream-commit: 9e78c9b063f27cda9bdbeee51a643ed09866ee11 Component: cli --- components/cli/command/formatter/container.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/components/cli/command/formatter/container.go b/components/cli/command/formatter/container.go index 6273453355..e31611c1e7 100644 --- a/components/cli/command/formatter/container.go +++ b/components/cli/command/formatter/container.go @@ -6,6 +6,7 @@ import ( "strings" "time" + "github.com/docker/distribution/reference" "github.com/docker/docker/api" "github.com/docker/docker/api/types" "github.com/docker/docker/pkg/stringid" @@ -115,7 +116,22 @@ func (c *containerContext) Image() string { if trunc := stringid.TruncateID(c.c.ImageID); trunc == stringid.TruncateID(c.c.Image) { return trunc } + // truncate digest if no-trunc option was not selected + ref, err := reference.ParseNormalizedNamed(c.c.Image) + if err == nil { + if nt, ok := ref.(reference.NamedTagged); ok { + // case for when a tag is provided + if namedTagged, err := reference.WithTag(reference.TrimNamed(nt), nt.Tag()); err == nil { + return reference.FamiliarString(namedTagged) + } + } else { + // case for when a tag is not provided + named := reference.TrimNamed(ref) + return reference.FamiliarString(named) + } + } } + return c.c.Image } From 00c0b3590a7dc4d7fa6c3a2e77334624cbc18066 Mon Sep 17 00:00:00 2001 From: allencloud Date: Fri, 17 Feb 2017 16:28:08 +0800 Subject: [PATCH 608/978] split compose deploy from deploy.go Signed-off-by: allencloud Upstream-commit: 16b16315944f8ae2e483eef11333a24292400fb9 Component: cli --- components/cli/command/stack/deploy.go | 282 ----------------- .../cli/command/stack/deploy_composefile.go | 290 ++++++++++++++++++ 2 files changed, 290 insertions(+), 282 deletions(-) create mode 100644 components/cli/command/stack/deploy_composefile.go diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index 753b1503b1..22557fc45b 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -2,20 +2,9 @@ package stack import ( "fmt" - "io/ioutil" - "os" - "sort" - "strings" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" - "github.com/docker/docker/cli/compose/convert" - "github.com/docker/docker/cli/compose/loader" - composetypes "github.com/docker/docker/cli/compose/types" - apiclient "github.com/docker/docker/client" - dockerclient "github.com/docker/docker/client" "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" @@ -82,274 +71,3 @@ func checkDaemonIsSwarmManager(ctx context.Context, dockerCli *command.DockerCli } return nil } - -func deployCompose(ctx context.Context, dockerCli *command.DockerCli, opts deployOptions) error { - configDetails, err := getConfigDetails(opts) - if err != nil { - return err - } - - config, err := loader.Load(configDetails) - if err != nil { - if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok { - return fmt.Errorf("Compose file contains unsupported options:\n\n%s\n", - propertyWarnings(fpe.Properties)) - } - - return err - } - - unsupportedProperties := loader.GetUnsupportedProperties(configDetails) - if len(unsupportedProperties) > 0 { - fmt.Fprintf(dockerCli.Err(), "Ignoring unsupported options: %s\n\n", - strings.Join(unsupportedProperties, ", ")) - } - - deprecatedProperties := loader.GetDeprecatedProperties(configDetails) - if len(deprecatedProperties) > 0 { - fmt.Fprintf(dockerCli.Err(), "Ignoring deprecated options:\n\n%s\n\n", - propertyWarnings(deprecatedProperties)) - } - - if err := checkDaemonIsSwarmManager(ctx, dockerCli); err != nil { - return err - } - - namespace := convert.NewNamespace(opts.namespace) - - serviceNetworks := getServicesDeclaredNetworks(config.Services) - - networks, externalNetworks := convert.Networks(namespace, config.Networks, serviceNetworks) - if err := validateExternalNetworks(ctx, dockerCli, externalNetworks); err != nil { - return err - } - if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil { - return err - } - - secrets, err := convert.Secrets(namespace, config.Secrets) - if err != nil { - return err - } - if err := createSecrets(ctx, dockerCli, namespace, secrets); err != nil { - return err - } - - services, err := convert.Services(namespace, config, dockerCli.Client()) - if err != nil { - return err - } - return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth) -} - -func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} { - serviceNetworks := map[string]struct{}{} - for _, serviceConfig := range serviceConfigs { - if len(serviceConfig.Networks) == 0 { - serviceNetworks["default"] = struct{}{} - continue - } - for network := range serviceConfig.Networks { - serviceNetworks[network] = struct{}{} - } - } - return serviceNetworks -} - -func propertyWarnings(properties map[string]string) string { - var msgs []string - for name, description := range properties { - msgs = append(msgs, fmt.Sprintf("%s: %s", name, description)) - } - sort.Strings(msgs) - return strings.Join(msgs, "\n\n") -} - -func getConfigDetails(opts deployOptions) (composetypes.ConfigDetails, error) { - var details composetypes.ConfigDetails - var err error - - details.WorkingDir, err = os.Getwd() - if err != nil { - return details, err - } - - configFile, err := getConfigFile(opts.composefile) - if err != nil { - return details, err - } - // TODO: support multiple files - details.ConfigFiles = []composetypes.ConfigFile{*configFile} - return details, nil -} - -func getConfigFile(filename string) (*composetypes.ConfigFile, error) { - bytes, err := ioutil.ReadFile(filename) - if err != nil { - return nil, err - } - config, err := loader.ParseYAML(bytes) - if err != nil { - return nil, err - } - return &composetypes.ConfigFile{ - Filename: filename, - Config: config, - }, nil -} - -func validateExternalNetworks( - ctx context.Context, - dockerCli *command.DockerCli, - externalNetworks []string) error { - client := dockerCli.Client() - - for _, networkName := range externalNetworks { - network, err := client.NetworkInspect(ctx, networkName) - if err != nil { - if dockerclient.IsErrNetworkNotFound(err) { - return fmt.Errorf("network %q is declared as external, but could not be found. You need to create the network before the stack is deployed (with overlay driver)", networkName) - } - return err - } - if network.Scope != "swarm" { - return fmt.Errorf("network %q is declared as external, but it is not in the right scope: %q instead of %q", networkName, network.Scope, "swarm") - } - } - - return nil -} - -func createSecrets( - ctx context.Context, - dockerCli *command.DockerCli, - namespace convert.Namespace, - secrets []swarm.SecretSpec, -) error { - client := dockerCli.Client() - - for _, secretSpec := range secrets { - secret, _, err := client.SecretInspectWithRaw(ctx, secretSpec.Name) - if err == nil { - // secret already exists, then we update that - if err := client.SecretUpdate(ctx, secret.ID, secret.Meta.Version, secretSpec); err != nil { - return err - } - } else if apiclient.IsErrSecretNotFound(err) { - // secret does not exist, then we create a new one. - if _, err := client.SecretCreate(ctx, secretSpec); err != nil { - return err - } - } else { - return err - } - } - return nil -} - -func createNetworks( - ctx context.Context, - dockerCli *command.DockerCli, - namespace convert.Namespace, - networks map[string]types.NetworkCreate, -) error { - client := dockerCli.Client() - - existingNetworks, err := getStackNetworks(ctx, client, namespace.Name()) - if err != nil { - return err - } - - existingNetworkMap := make(map[string]types.NetworkResource) - for _, network := range existingNetworks { - existingNetworkMap[network.Name] = network - } - - for internalName, createOpts := range networks { - name := namespace.Scope(internalName) - if _, exists := existingNetworkMap[name]; exists { - continue - } - - if createOpts.Driver == "" { - createOpts.Driver = defaultNetworkDriver - } - - fmt.Fprintf(dockerCli.Out(), "Creating network %s\n", name) - if _, err := client.NetworkCreate(ctx, name, createOpts); err != nil { - return err - } - } - - return nil -} - -func deployServices( - ctx context.Context, - dockerCli *command.DockerCli, - services map[string]swarm.ServiceSpec, - namespace convert.Namespace, - sendAuth bool, -) error { - apiClient := dockerCli.Client() - out := dockerCli.Out() - - existingServices, err := getServices(ctx, apiClient, namespace.Name()) - if err != nil { - return err - } - - existingServiceMap := make(map[string]swarm.Service) - for _, service := range existingServices { - existingServiceMap[service.Spec.Name] = service - } - - for internalName, serviceSpec := range services { - name := namespace.Scope(internalName) - - encodedAuth := "" - if sendAuth { - // Retrieve encoded auth token from the image reference - image := serviceSpec.TaskTemplate.ContainerSpec.Image - encodedAuth, err = command.RetrieveAuthTokenFromImage(ctx, dockerCli, image) - if err != nil { - return err - } - } - - if service, exists := existingServiceMap[name]; exists { - fmt.Fprintf(out, "Updating service %s (id: %s)\n", name, service.ID) - - updateOpts := types.ServiceUpdateOptions{} - if sendAuth { - updateOpts.EncodedRegistryAuth = encodedAuth - } - response, err := apiClient.ServiceUpdate( - ctx, - service.ID, - service.Version, - serviceSpec, - updateOpts, - ) - if err != nil { - return err - } - - for _, warning := range response.Warnings { - fmt.Fprintln(dockerCli.Err(), warning) - } - } else { - fmt.Fprintf(out, "Creating service %s\n", name) - - createOpts := types.ServiceCreateOptions{} - if sendAuth { - createOpts.EncodedRegistryAuth = encodedAuth - } - if _, err := apiClient.ServiceCreate(ctx, serviceSpec, createOpts); err != nil { - return err - } - } - } - - return nil -} diff --git a/components/cli/command/stack/deploy_composefile.go b/components/cli/command/stack/deploy_composefile.go new file mode 100644 index 0000000000..72f9b8aac9 --- /dev/null +++ b/components/cli/command/stack/deploy_composefile.go @@ -0,0 +1,290 @@ +package stack + +import ( + "fmt" + "io/ioutil" + "os" + "sort" + "strings" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/compose/convert" + "github.com/docker/docker/cli/compose/loader" + composetypes "github.com/docker/docker/cli/compose/types" + apiclient "github.com/docker/docker/client" + dockerclient "github.com/docker/docker/client" + "golang.org/x/net/context" +) + +func deployCompose(ctx context.Context, dockerCli *command.DockerCli, opts deployOptions) error { + configDetails, err := getConfigDetails(opts) + if err != nil { + return err + } + + config, err := loader.Load(configDetails) + if err != nil { + if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok { + return fmt.Errorf("Compose file contains unsupported options:\n\n%s\n", + propertyWarnings(fpe.Properties)) + } + + return err + } + + unsupportedProperties := loader.GetUnsupportedProperties(configDetails) + if len(unsupportedProperties) > 0 { + fmt.Fprintf(dockerCli.Err(), "Ignoring unsupported options: %s\n\n", + strings.Join(unsupportedProperties, ", ")) + } + + deprecatedProperties := loader.GetDeprecatedProperties(configDetails) + if len(deprecatedProperties) > 0 { + fmt.Fprintf(dockerCli.Err(), "Ignoring deprecated options:\n\n%s\n\n", + propertyWarnings(deprecatedProperties)) + } + + if err := checkDaemonIsSwarmManager(ctx, dockerCli); err != nil { + return err + } + + namespace := convert.NewNamespace(opts.namespace) + + serviceNetworks := getServicesDeclaredNetworks(config.Services) + + networks, externalNetworks := convert.Networks(namespace, config.Networks, serviceNetworks) + if err := validateExternalNetworks(ctx, dockerCli, externalNetworks); err != nil { + return err + } + if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil { + return err + } + + secrets, err := convert.Secrets(namespace, config.Secrets) + if err != nil { + return err + } + if err := createSecrets(ctx, dockerCli, namespace, secrets); err != nil { + return err + } + + services, err := convert.Services(namespace, config, dockerCli.Client()) + if err != nil { + return err + } + return deployServices(ctx, dockerCli, services, namespace, opts.sendRegistryAuth) +} + +func getServicesDeclaredNetworks(serviceConfigs []composetypes.ServiceConfig) map[string]struct{} { + serviceNetworks := map[string]struct{}{} + for _, serviceConfig := range serviceConfigs { + if len(serviceConfig.Networks) == 0 { + serviceNetworks["default"] = struct{}{} + continue + } + for network := range serviceConfig.Networks { + serviceNetworks[network] = struct{}{} + } + } + return serviceNetworks +} + +func propertyWarnings(properties map[string]string) string { + var msgs []string + for name, description := range properties { + msgs = append(msgs, fmt.Sprintf("%s: %s", name, description)) + } + sort.Strings(msgs) + return strings.Join(msgs, "\n\n") +} + +func getConfigDetails(opts deployOptions) (composetypes.ConfigDetails, error) { + var details composetypes.ConfigDetails + var err error + + details.WorkingDir, err = os.Getwd() + if err != nil { + return details, err + } + + configFile, err := getConfigFile(opts.composefile) + if err != nil { + return details, err + } + // TODO: support multiple files + details.ConfigFiles = []composetypes.ConfigFile{*configFile} + return details, nil +} + +func getConfigFile(filename string) (*composetypes.ConfigFile, error) { + bytes, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + config, err := loader.ParseYAML(bytes) + if err != nil { + return nil, err + } + return &composetypes.ConfigFile{ + Filename: filename, + Config: config, + }, nil +} + +func validateExternalNetworks( + ctx context.Context, + dockerCli *command.DockerCli, + externalNetworks []string) error { + client := dockerCli.Client() + + for _, networkName := range externalNetworks { + network, err := client.NetworkInspect(ctx, networkName) + if err != nil { + if dockerclient.IsErrNetworkNotFound(err) { + return fmt.Errorf("network %q is declared as external, but could not be found. You need to create the network before the stack is deployed (with overlay driver)", networkName) + } + return err + } + if network.Scope != "swarm" { + return fmt.Errorf("network %q is declared as external, but it is not in the right scope: %q instead of %q", networkName, network.Scope, "swarm") + } + } + + return nil +} + +func createSecrets( + ctx context.Context, + dockerCli *command.DockerCli, + namespace convert.Namespace, + secrets []swarm.SecretSpec, +) error { + client := dockerCli.Client() + + for _, secretSpec := range secrets { + secret, _, err := client.SecretInspectWithRaw(ctx, secretSpec.Name) + if err == nil { + // secret already exists, then we update that + if err := client.SecretUpdate(ctx, secret.ID, secret.Meta.Version, secretSpec); err != nil { + return err + } + } else if apiclient.IsErrSecretNotFound(err) { + // secret does not exist, then we create a new one. + if _, err := client.SecretCreate(ctx, secretSpec); err != nil { + return err + } + } else { + return err + } + } + return nil +} + +func createNetworks( + ctx context.Context, + dockerCli *command.DockerCli, + namespace convert.Namespace, + networks map[string]types.NetworkCreate, +) error { + client := dockerCli.Client() + + existingNetworks, err := getStackNetworks(ctx, client, namespace.Name()) + if err != nil { + return err + } + + existingNetworkMap := make(map[string]types.NetworkResource) + for _, network := range existingNetworks { + existingNetworkMap[network.Name] = network + } + + for internalName, createOpts := range networks { + name := namespace.Scope(internalName) + if _, exists := existingNetworkMap[name]; exists { + continue + } + + if createOpts.Driver == "" { + createOpts.Driver = defaultNetworkDriver + } + + fmt.Fprintf(dockerCli.Out(), "Creating network %s\n", name) + if _, err := client.NetworkCreate(ctx, name, createOpts); err != nil { + return err + } + } + + return nil +} + +func deployServices( + ctx context.Context, + dockerCli *command.DockerCli, + services map[string]swarm.ServiceSpec, + namespace convert.Namespace, + sendAuth bool, +) error { + apiClient := dockerCli.Client() + out := dockerCli.Out() + + existingServices, err := getServices(ctx, apiClient, namespace.Name()) + if err != nil { + return err + } + + existingServiceMap := make(map[string]swarm.Service) + for _, service := range existingServices { + existingServiceMap[service.Spec.Name] = service + } + + for internalName, serviceSpec := range services { + name := namespace.Scope(internalName) + + encodedAuth := "" + if sendAuth { + // Retrieve encoded auth token from the image reference + image := serviceSpec.TaskTemplate.ContainerSpec.Image + encodedAuth, err = command.RetrieveAuthTokenFromImage(ctx, dockerCli, image) + if err != nil { + return err + } + } + + if service, exists := existingServiceMap[name]; exists { + fmt.Fprintf(out, "Updating service %s (id: %s)\n", name, service.ID) + + updateOpts := types.ServiceUpdateOptions{} + if sendAuth { + updateOpts.EncodedRegistryAuth = encodedAuth + } + response, err := apiClient.ServiceUpdate( + ctx, + service.ID, + service.Version, + serviceSpec, + updateOpts, + ) + if err != nil { + return err + } + + for _, warning := range response.Warnings { + fmt.Fprintln(dockerCli.Err(), warning) + } + } else { + fmt.Fprintf(out, "Creating service %s\n", name) + + createOpts := types.ServiceCreateOptions{} + if sendAuth { + createOpts.EncodedRegistryAuth = encodedAuth + } + if _, err := apiClient.ServiceCreate(ctx, serviceSpec, createOpts); err != nil { + return err + } + } + } + + return nil +} From d7f5ca1d450d3ff95dd927a35095e853d49bc40a Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Sun, 19 Feb 2017 00:43:08 -0800 Subject: [PATCH 609/978] add missing API changes Signed-off-by: Victor Vieux Upstream-commit: e858f5f7c4dca2d73d2648155e07ac871c73788c Component: cli --- components/cli/command/service/opts.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index 9a0ae64ca9..35c5f2f657 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -469,7 +469,7 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { flags.SetAnnotation(flagTTY, "version", []string{"1.25"}) flags.BoolVar(&opts.readOnly, flagReadOnly, false, "Mount the container's root filesystem as read only") - flags.SetAnnotation(flagReadOnly, "version", []string{"1.26"}) + flags.SetAnnotation(flagReadOnly, "version", []string{"1.27"}) } const ( From 8176608bd250819e96f6f4f58289fb417c0b3c2c Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Sun, 19 Feb 2017 00:43:08 -0800 Subject: [PATCH 610/978] add missing API changes Signed-off-by: Victor Vieux Upstream-commit: 0d367623d0c11247be15a77bcf62c0b8b53a7cc0 Component: cli --- components/cli/client.go | 6 ++---- components/cli/client_test.go | 15 ++++++++------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/components/cli/client.go b/components/cli/client.go index 75cfc8698b..df3698adc6 100644 --- a/components/cli/client.go +++ b/components/cli/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/components/cli/client_test.go b/components/cli/client_test.go index 7c26403ebe..64188d5fb1 100644 --- a/components/cli/client_test.go +++ b/components/cli/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 68022c25ce65c721b18432ec46c838608b25f5f4 Mon Sep 17 00:00:00 2001 From: Tony Abboud Date: Fri, 13 Jan 2017 10:01:58 -0500 Subject: [PATCH 611/978] Add --add-host for docker build Signed-off-by: Tony Abboud Upstream-commit: 1c579ffcc5c449bb6ace0d917f751e4a12c782ff Component: cli --- components/cli/image_build.go | 1 + 1 file changed, 1 insertion(+) diff --git a/components/cli/image_build.go b/components/cli/image_build.go index 411d5493ea..cc5a71c2a1 100644 --- a/components/cli/image_build.go +++ b/components/cli/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 4b398302d795f11285aa775b0d70d7a54c07e5b4 Mon Sep 17 00:00:00 2001 From: Tony Abboud Date: Fri, 13 Jan 2017 10:01:58 -0500 Subject: [PATCH 612/978] Add --add-host for docker build Signed-off-by: Tony Abboud Upstream-commit: e66e519e8dbc1028806f4dc965f705017af3dd8f Component: cli --- components/cli/command/image/build.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go index 34c231d63e..4639833a9e 100644 --- a/components/cli/command/image/build.go +++ b/components/cli/command/image/build.go @@ -38,6 +38,7 @@ type buildOptions struct { tags opts.ListOpts labels opts.ListOpts buildArgs opts.ListOpts + extraHosts opts.ListOpts ulimits *opts.UlimitOpt memory string memorySwap string @@ -65,10 +66,11 @@ type buildOptions struct { func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command { ulimits := make(map[string]*units.Ulimit) options := buildOptions{ - tags: opts.NewListOpts(validateTag), - buildArgs: opts.NewListOpts(opts.ValidateEnv), - ulimits: opts.NewUlimitOpt(&ulimits), - labels: opts.NewListOpts(opts.ValidateEnv), + tags: opts.NewListOpts(validateTag), + buildArgs: opts.NewListOpts(opts.ValidateEnv), + ulimits: opts.NewUlimitOpt(&ulimits), + labels: opts.NewListOpts(opts.ValidateEnv), + extraHosts: opts.NewListOpts(opts.ValidateExtraHost), } cmd := &cobra.Command{ @@ -108,6 +110,7 @@ func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command { flags.StringSliceVar(&options.securityOpt, "security-opt", []string{}, "Security options") flags.StringVar(&options.networkMode, "network", "default", "Set the networking mode for the RUN instructions during build") flags.SetAnnotation("network", "version", []string{"1.25"}) + flags.Var(&options.extraHosts, "add-host", "Add a custom host-to-IP mapping (host:ip)") command.AddTrustVerificationFlags(flags) @@ -301,6 +304,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { SecurityOpt: options.securityOpt, NetworkMode: options.networkMode, Squash: options.squash, + ExtraHosts: options.extraHosts.GetAll(), } response, err := dockerCli.Client().ImageBuild(ctx, body, buildOptions) From b7afa18df246387c4481766e1df24f36d2d92a1f Mon Sep 17 00:00:00 2001 From: Reficul Date: Tue, 21 Feb 2017 10:26:06 +0800 Subject: [PATCH 613/978] fix wrong print format Signed-off-by: Reficul Upstream-commit: a77cf51173e2844e0e289de74b78f037bce1b57f Component: cli --- components/cli/command/container/opts_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/container/opts_test.go b/components/cli/command/container/opts_test.go index d0655069e9..3aef42704f 100644 --- a/components/cli/command/container/opts_test.go +++ b/components/cli/command/container/opts_test.go @@ -492,7 +492,7 @@ func TestParseHealth(t *testing.T) { t.Fatalf("--health-cmd: got %#v", health.Test) } if health.Timeout != 0 { - t.Fatalf("--health-cmd: timeout = %f", health.Timeout) + t.Fatalf("--health-cmd: timeout = %s", health.Timeout) } checkError("--no-healthcheck conflicts with --health-* options", From f79fc0f81ebb65581641265fb51608c863e3e2d0 Mon Sep 17 00:00:00 2001 From: Krasi Georgiev Date: Wed, 15 Feb 2017 21:43:01 +0200 Subject: [PATCH 614/978] ignore registry url from user when it is the default namespace Signed-off-by: Krasi Georgiev Upstream-commit: 1d0e556669544bf38f50cc98a00cfac64cdc418f Component: cli --- components/cli/command/registry/login.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/cli/command/registry/login.go b/components/cli/command/registry/login.go index bdcc9a103b..ee4fd97f11 100644 --- a/components/cli/command/registry/login.go +++ b/components/cli/command/registry/login.go @@ -7,6 +7,7 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/registry" "github.com/spf13/cobra" ) @@ -49,7 +50,7 @@ func runLogin(dockerCli *command.DockerCli, opts loginOptions) error { serverAddress string authServer = command.ElectAuthServer(ctx, dockerCli) ) - if opts.serverAddress != "" { + if opts.serverAddress != "" && opts.serverAddress != registry.DefaultNamespace { serverAddress = opts.serverAddress } else { serverAddress = authServer From e1a9ffba6b2dad9220c09696ceae6f77020e8dc6 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 5 Feb 2017 21:22:57 -0800 Subject: [PATCH 615/978] Add `--stop-signal` for `service create` and `service update` This fix tries to address the issue raised in 25696 where it was not possible to specify `--stop-signal` for `docker service create` and `docker service update`, in order to use special signal to stop the container. This fix adds `--stop-signal` and update the `StopSignal` in `Config` through `service create` and `service update`. Related docs has been updated. Integration test has been added. This fix fixes 25696. Signed-off-by: Yong Tang Upstream-commit: 9037f77d315acf2c794bdef337564aa713620aed Component: cli --- components/cli/command/service/opts.go | 28 +++++++++++-------- components/cli/command/service/update.go | 2 ++ components/cli/command/service/update_test.go | 22 +++++++++++++++ 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index d8618e73ca..adab9b3658 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -269,6 +269,7 @@ type serviceOptions struct { workdir string user string groups opts.ListOpts + stopSignal string tty bool readOnly bool mounts opts.MountOpt @@ -372,17 +373,18 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { }, TaskTemplate: swarm.TaskSpec{ ContainerSpec: swarm.ContainerSpec{ - Image: opts.image, - Args: opts.args, - Env: currentEnv, - Hostname: opts.hostname, - Labels: runconfigopts.ConvertKVStringsToMap(opts.containerLabels.GetAll()), - Dir: opts.workdir, - User: opts.user, - Groups: opts.groups.GetAll(), - TTY: opts.tty, - ReadOnly: opts.readOnly, - Mounts: opts.mounts.Value(), + Image: opts.image, + Args: opts.args, + Env: currentEnv, + Hostname: opts.hostname, + Labels: runconfigopts.ConvertKVStringsToMap(opts.containerLabels.GetAll()), + Dir: opts.workdir, + User: opts.user, + Groups: opts.groups.GetAll(), + StopSignal: opts.stopSignal, + TTY: opts.tty, + ReadOnly: opts.readOnly, + Mounts: opts.mounts.Value(), DNSConfig: &swarm.DNSConfig{ Nameservers: opts.dns.GetAll(), Search: opts.dnsSearch.GetAll(), @@ -470,6 +472,9 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { flags.BoolVar(&opts.readOnly, flagReadOnly, false, "Mount the container's root filesystem as read only") flags.SetAnnotation(flagReadOnly, "version", []string{"1.27"}) + + flags.StringVar(&opts.stopSignal, flagStopSignal, "", "Signal to stop the container") + flags.SetAnnotation(flagStopSignal, "version", []string{"1.27"}) } const ( @@ -523,6 +528,7 @@ const ( flagRestartMaxAttempts = "restart-max-attempts" flagRestartWindow = "restart-window" flagStopGracePeriod = "stop-grace-period" + flagStopSignal = "stop-signal" flagTTY = "tty" flagUpdateDelay = "update-delay" flagUpdateFailureAction = "update-failure-action" diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index 7f461c90a9..770a5bd26f 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -349,6 +349,8 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error { cspec.ReadOnly = readOnly } + updateString(flagStopSignal, &cspec.StopSignal) + return nil } diff --git a/components/cli/command/service/update_test.go b/components/cli/command/service/update_test.go index f2887e229d..c43e596136 100644 --- a/components/cli/command/service/update_test.go +++ b/components/cli/command/service/update_test.go @@ -441,3 +441,25 @@ func TestUpdateReadOnly(t *testing.T) { updateService(flags, spec) assert.Equal(t, cspec.ReadOnly, false) } + +func TestUpdateStopSignal(t *testing.T) { + spec := &swarm.ServiceSpec{} + cspec := &spec.TaskTemplate.ContainerSpec + + // Update with --stop-signal=SIGUSR1 + flags := newUpdateCommand(nil).Flags() + flags.Set("stop-signal", "SIGUSR1") + updateService(flags, spec) + assert.Equal(t, cspec.StopSignal, "SIGUSR1") + + // Update without --stop-signal, no change + flags = newUpdateCommand(nil).Flags() + updateService(flags, spec) + assert.Equal(t, cspec.StopSignal, "SIGUSR1") + + // Update with --stop-signal=SIGWINCH + flags = newUpdateCommand(nil).Flags() + flags.Set("stop-signal", "SIGWINCH") + updateService(flags, spec) + assert.Equal(t, cspec.StopSignal, "SIGWINCH") +} From 5145dfe136948eb21fd9a6ef6928525d49baf2e1 Mon Sep 17 00:00:00 2001 From: fate-grand-order Date: Tue, 21 Feb 2017 16:53:29 +0800 Subject: [PATCH 616/978] use t.Fatal() to output the err message where the values used for formatting text does not appear to contain a placeholder Signed-off-by: Helen Xie Upstream-commit: 98c222239e4f807ea668fc16713926b476463a1d Component: cli --- components/cli/command/container/opts_test.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/components/cli/command/container/opts_test.go b/components/cli/command/container/opts_test.go index d0655069e9..725c9beb42 100644 --- a/components/cli/command/container/opts_test.go +++ b/components/cli/command/container/opts_test.go @@ -28,7 +28,7 @@ func TestValidateAttach(t *testing.T) { "STDERR", } if _, err := validateAttach("invalid"); err == nil { - t.Fatalf("Expected error with [valid streams are STDIN, STDOUT and STDERR], got nothing") + t.Fatal("Expected error with [valid streams are STDIN, STDOUT and STDERR], got nothing") } for _, attach := range valid { @@ -96,28 +96,28 @@ func TestParseRunAttach(t *testing.T) { } if _, _, err := parsetest(t, "-a"); err == nil { - t.Fatalf("Error parsing attach flags, `-a` should be an error but is not") + t.Fatal("Error parsing attach flags, `-a` should be an error but is not") } if _, _, err := parsetest(t, "-a invalid"); err == nil { - t.Fatalf("Error parsing attach flags, `-a invalid` should be an error but is not") + t.Fatal("Error parsing attach flags, `-a invalid` should be an error but is not") } if _, _, err := parsetest(t, "-a invalid -a stdout"); err == nil { - t.Fatalf("Error parsing attach flags, `-a stdout -a invalid` should be an error but is not") + t.Fatal("Error parsing attach flags, `-a stdout -a invalid` should be an error but is not") } if _, _, err := parsetest(t, "-a stdout -a stderr -d"); err == nil { - t.Fatalf("Error parsing attach flags, `-a stdout -a stderr -d` should be an error but is not") + t.Fatal("Error parsing attach flags, `-a stdout -a stderr -d` should be an error but is not") } if _, _, err := parsetest(t, "-a stdin -d"); err == nil { - t.Fatalf("Error parsing attach flags, `-a stdin -d` should be an error but is not") + t.Fatal("Error parsing attach flags, `-a stdin -d` should be an error but is not") } if _, _, err := parsetest(t, "-a stdout -d"); err == nil { - t.Fatalf("Error parsing attach flags, `-a stdout -d` should be an error but is not") + t.Fatal("Error parsing attach flags, `-a stdout -d` should be an error but is not") } if _, _, err := parsetest(t, "-a stderr -d"); err == nil { - t.Fatalf("Error parsing attach flags, `-a stderr -d` should be an error but is not") + t.Fatal("Error parsing attach flags, `-a stderr -d` should be an error but is not") } if _, _, err := parsetest(t, "-d --rm"); err == nil { - t.Fatalf("Error parsing attach flags, `-d --rm` should be an error but is not") + t.Fatal("Error parsing attach flags, `-d --rm` should be an error but is not") } } From 1195f9c5ee818163da1700c951ce2b94fc89f9cd Mon Sep 17 00:00:00 2001 From: yupengzte Date: Thu, 23 Feb 2017 16:46:08 +0800 Subject: [PATCH 617/978] Delete dots to align with other commands description Signed-off-by: yupengzte Upstream-commit: 5b67f20a917e4fbe42b7c19d1c689a2c5f380eb8 Component: cli --- components/cli/command/node/inspect.go | 2 +- components/cli/command/service/inspect.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/command/node/inspect.go b/components/cli/command/node/inspect.go index 97a2717781..a08497003d 100644 --- a/components/cli/command/node/inspect.go +++ b/components/cli/command/node/inspect.go @@ -37,7 +37,7 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command { flags := cmd.Flags() flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") - flags.BoolVar(&opts.pretty, "pretty", false, "Print the information in a human friendly format.") + flags.BoolVar(&opts.pretty, "pretty", false, "Print the information in a human friendly format") return cmd } diff --git a/components/cli/command/service/inspect.go b/components/cli/command/service/inspect.go index deb701bf6d..7af9b98c3c 100644 --- a/components/cli/command/service/inspect.go +++ b/components/cli/command/service/inspect.go @@ -38,7 +38,7 @@ func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") - flags.BoolVar(&opts.pretty, "pretty", false, "Print the information in a human friendly format.") + flags.BoolVar(&opts.pretty, "pretty", false, "Print the information in a human friendly format") return cmd } From b6d88b7b920a9a5147b6d87f247cac085b2f6c9e Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Thu, 23 Feb 2017 13:36:57 +0100 Subject: [PATCH 618/978] add d_type warning to docker info, and optimize output The overlay(2) drivers were moved up in the list of storage drivers, and are known to have problems if the backing filesystem does not support d_type. Commit 2e20e63da2a8a0ffbbb3f2146f87559e17f43046 added a warning, which is logged in the daemon logs, however, many users do not check those logs, and may overlook this warning. This patch adds the same warning to the output of `docker info` so that the warning is more easily found. In addition, the output of warnings printed by `docker info` is optimized, by; - moving all warnings to the _end_ of the output, instead of mixing them with the regular output - wrapping the storage-driver warnings, so that they are more easily readable Example output with this patch applied ============================================ devicemapper using loopback devices: ... Insecure Registries: 127.0.0.0/8 Live Restore Enabled: false WARNING: devicemapper: usage of loopback devices is strongly discouraged for production use. Use `--storage-opt dm.thinpooldev` to specify a custom block storage device. WARNING: bridge-nf-call-iptables is disabled WARNING: bridge-nf-call-ip6tables is disabled overlay2 on xfs without d_type support; ... Insecure Registries: 127.0.0.0/8 Live Restore Enabled: false WARNING: overlay2: the backing xfs filesystem is formatted without d_type support, which leads to incorrect behavior. Reformat the filesystem with ftype=1 to enable d_type support. Running without d_type support will not be supported in future releases. WARNING: bridge-nf-call-iptables is disabled Signed-off-by: Sebastiaan van Stijn Upstream-commit: b09aa604c8b6abb81356b1a29457e11d72121791 Component: cli --- components/cli/command/system/info.go | 121 ++++++++++++++++---------- 1 file changed, 76 insertions(+), 45 deletions(-) diff --git a/components/cli/command/system/info.go b/components/cli/command/system/info.go index d9fafd1aa1..aa9246670c 100644 --- a/components/cli/command/system/info.go +++ b/components/cli/command/system/info.go @@ -6,8 +6,6 @@ import ( "strings" "time" - "golang.org/x/net/context" - "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" @@ -17,6 +15,7 @@ import ( "github.com/docker/docker/pkg/templates" "github.com/docker/go-units" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type infoOptions struct { @@ -66,11 +65,6 @@ func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error { if info.DriverStatus != nil { for _, pair := range info.DriverStatus { fmt.Fprintf(dockerCli.Out(), " %s: %s\n", pair[0], pair[1]) - - // print a warning if devicemapper is using a loopback file - if pair[0] == "Data loop file" { - fmt.Fprintln(dockerCli.Err(), " WARNING: Usage of loopback devices is strongly discouraged for production use. Use `--storage-opt dm.thinpooldev` to specify a custom block storage device.") - } } } @@ -228,43 +222,6 @@ func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error { fmt.Fprintf(dockerCli.Out(), "Registry: %v\n", info.IndexServerAddress) } - // Only output these warnings if the server does not support these features - if info.OSType != "windows" { - if !info.MemoryLimit { - fmt.Fprintln(dockerCli.Err(), "WARNING: No memory limit support") - } - if !info.SwapLimit { - fmt.Fprintln(dockerCli.Err(), "WARNING: No swap limit support") - } - if !info.KernelMemory { - fmt.Fprintln(dockerCli.Err(), "WARNING: No kernel memory limit support") - } - if !info.OomKillDisable { - fmt.Fprintln(dockerCli.Err(), "WARNING: No oom kill disable support") - } - if !info.CPUCfsQuota { - fmt.Fprintln(dockerCli.Err(), "WARNING: No cpu cfs quota support") - } - if !info.CPUCfsPeriod { - fmt.Fprintln(dockerCli.Err(), "WARNING: No cpu cfs period support") - } - if !info.CPUShares { - fmt.Fprintln(dockerCli.Err(), "WARNING: No cpu shares support") - } - if !info.CPUSet { - fmt.Fprintln(dockerCli.Err(), "WARNING: No cpuset support") - } - if !info.IPv4Forwarding { - fmt.Fprintln(dockerCli.Err(), "WARNING: IPv4 forwarding is disabled") - } - if !info.BridgeNfIptables { - fmt.Fprintln(dockerCli.Err(), "WARNING: bridge-nf-call-iptables is disabled") - } - if !info.BridgeNfIP6tables { - fmt.Fprintln(dockerCli.Err(), "WARNING: bridge-nf-call-ip6tables is disabled") - } - } - if info.Labels != nil { fmt.Fprintln(dockerCli.Out(), "Labels:") for _, attribute := range info.Labels { @@ -317,11 +274,85 @@ func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error { } } - fmt.Fprintf(dockerCli.Out(), "Live Restore Enabled: %v\n", info.LiveRestoreEnabled) + fmt.Fprintf(dockerCli.Out(), "Live Restore Enabled: %v\n\n", info.LiveRestoreEnabled) + + // Only output these warnings if the server does not support these features + if info.OSType != "windows" { + printStorageDriverWarnings(dockerCli, info) + + if !info.MemoryLimit { + fmt.Fprintln(dockerCli.Err(), "WARNING: No memory limit support") + } + if !info.SwapLimit { + fmt.Fprintln(dockerCli.Err(), "WARNING: No swap limit support") + } + if !info.KernelMemory { + fmt.Fprintln(dockerCli.Err(), "WARNING: No kernel memory limit support") + } + if !info.OomKillDisable { + fmt.Fprintln(dockerCli.Err(), "WARNING: No oom kill disable support") + } + if !info.CPUCfsQuota { + fmt.Fprintln(dockerCli.Err(), "WARNING: No cpu cfs quota support") + } + if !info.CPUCfsPeriod { + fmt.Fprintln(dockerCli.Err(), "WARNING: No cpu cfs period support") + } + if !info.CPUShares { + fmt.Fprintln(dockerCli.Err(), "WARNING: No cpu shares support") + } + if !info.CPUSet { + fmt.Fprintln(dockerCli.Err(), "WARNING: No cpuset support") + } + if !info.IPv4Forwarding { + fmt.Fprintln(dockerCli.Err(), "WARNING: IPv4 forwarding is disabled") + } + if !info.BridgeNfIptables { + fmt.Fprintln(dockerCli.Err(), "WARNING: bridge-nf-call-iptables is disabled") + } + if !info.BridgeNfIP6tables { + fmt.Fprintln(dockerCli.Err(), "WARNING: bridge-nf-call-ip6tables is disabled") + } + } return nil } +func printStorageDriverWarnings(dockerCli *command.DockerCli, info types.Info) { + if info.DriverStatus == nil { + return + } + + for _, pair := range info.DriverStatus { + if pair[0] == "Data loop file" { + fmt.Fprintf(dockerCli.Err(), "WARNING: %s: usage of loopback devices is strongly discouraged for production use.\n Use `--storage-opt dm.thinpooldev` to specify a custom block storage device.\n", info.Driver) + } + if pair[0] == "Supports d_type" && pair[1] == "false" { + backingFs := getBackingFs(info) + + msg := fmt.Sprintf("WARNING: %s: the backing %s filesystem is formatted without d_type support, which leads to incorrect behavior.\n", info.Driver, backingFs) + if backingFs == "xfs" { + msg += " Reformat the filesystem with ftype=1 to enable d_type support.\n" + } + msg += " Running without d_type support will not be supported in future releases." + fmt.Fprintln(dockerCli.Err(), msg) + } + } +} + +func getBackingFs(info types.Info) string { + if info.DriverStatus == nil { + return "" + } + + for _, pair := range info.DriverStatus { + if pair[0] == "Backing Filesystem" { + return pair[1] + } + } + return "" +} + func formatInfo(dockerCli *command.DockerCli, info types.Info, format string) error { tmpl, err := templates.Parse(format) if err != nil { From ac3e9a7a54674b1099d9fb6590dde22a52565173 Mon Sep 17 00:00:00 2001 From: Arash Deshmeh Date: Sat, 18 Feb 2017 00:29:51 -0500 Subject: [PATCH 619/978] docker compose interpolation format error now includes a hint on escaping $ characters. Signed-off-by: Arash Deshmeh Upstream-commit: 3581bec44220e38193f0b69e0d2d159494592c04 Component: cli --- components/cli/compose/interpolation/interpolation.go | 2 +- components/cli/compose/interpolation/interpolation_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/compose/interpolation/interpolation.go b/components/cli/compose/interpolation/interpolation.go index 734f28ec9d..29c2e0e279 100644 --- a/components/cli/compose/interpolation/interpolation.go +++ b/components/cli/compose/interpolation/interpolation.go @@ -39,7 +39,7 @@ func interpolateSectionItem( interpolatedValue, err := recursiveInterpolate(value, mapping) if err != nil { return nil, fmt.Errorf( - "Invalid interpolation format for %#v option in %s %#v: %#v", + "Invalid interpolation format for %#v option in %s %#v: %#v. You may need to escape any $ with another $.", key, section, name, err.Template, ) } diff --git a/components/cli/compose/interpolation/interpolation_test.go b/components/cli/compose/interpolation/interpolation_test.go index c3921701b3..1852b9eb44 100644 --- a/components/cli/compose/interpolation/interpolation_test.go +++ b/components/cli/compose/interpolation/interpolation_test.go @@ -55,5 +55,5 @@ func TestInvalidInterpolation(t *testing.T) { }, } _, err := Interpolate(services, "service", defaultMapping) - assert.EqualError(t, err, `Invalid interpolation format for "image" option in service "servicea": "${"`) + assert.EqualError(t, err, `Invalid interpolation format for "image" option in service "servicea": "${". You may need to escape any $ with another $.`) } From 123d3719463be9fd85134427e127852cfa497622 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 22 Feb 2017 13:52:09 -0500 Subject: [PATCH 620/978] Support customizing the default network for a stack. Signed-off-by: Daniel Nephin Upstream-commit: 5de7378cbe4ddd1e41e89b54fa9a06939e0e6614 Component: cli --- components/cli/compose/convert/service.go | 13 ++++----- .../cli/compose/convert/service_test.go | 28 +++++++++++++++++-- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/components/cli/compose/convert/service.go b/components/cli/compose/convert/service.go index 93b910967e..9af4a74309 100644 --- a/components/cli/compose/convert/service.go +++ b/components/cli/compose/convert/service.go @@ -16,6 +16,8 @@ import ( runconfigopts "github.com/docker/docker/runconfig/opts" ) +const defaultNetwork = "default" + // Services from compose-file types to engine API types // TODO: fix secrets API so that SecretAPIClient is not required here func Services( @@ -156,18 +158,15 @@ func convertServiceNetworks( name string, ) ([]swarm.NetworkAttachmentConfig, error) { if len(networks) == 0 { - return []swarm.NetworkAttachmentConfig{ - { - Target: namespace.Scope("default"), - Aliases: []string{name}, - }, - }, nil + networks = map[string]*composetypes.ServiceNetworkConfig{ + defaultNetwork: {}, + } } nets := []swarm.NetworkAttachmentConfig{} for networkName, network := range networks { networkConfig, ok := networkConfigs[networkName] - if !ok { + if !ok && networkName != defaultNetwork { return []swarm.NetworkAttachmentConfig{}, fmt.Errorf( "service %q references network %q, which is not declared", name, networkName) } diff --git a/components/cli/compose/convert/service_test.go b/components/cli/compose/convert/service_test.go index 64ccfd038e..10bde35080 100644 --- a/components/cli/compose/convert/service_test.go +++ b/components/cli/compose/convert/service_test.go @@ -179,10 +179,9 @@ func TestConvertEndpointSpec(t *testing.T) { func TestConvertServiceNetworksOnlyDefault(t *testing.T) { networkConfigs := networkMap{} - networks := map[string]*composetypes.ServiceNetworkConfig{} configs, err := convertServiceNetworks( - networks, networkConfigs, NewNamespace("foo"), "service") + nil, networkConfigs, NewNamespace("foo"), "service") expected := []swarm.NetworkAttachmentConfig{ { @@ -235,6 +234,31 @@ func TestConvertServiceNetworks(t *testing.T) { assert.DeepEqual(t, []swarm.NetworkAttachmentConfig(sortedConfigs), expected) } +func TestConvertServiceNetworksCustomDefault(t *testing.T) { + networkConfigs := networkMap{ + "default": composetypes.NetworkConfig{ + External: composetypes.External{ + External: true, + Name: "custom", + }, + }, + } + networks := map[string]*composetypes.ServiceNetworkConfig{} + + configs, err := convertServiceNetworks( + networks, networkConfigs, NewNamespace("foo"), "service") + + expected := []swarm.NetworkAttachmentConfig{ + { + Target: "custom", + Aliases: []string{"service"}, + }, + } + + assert.NilError(t, err) + assert.DeepEqual(t, []swarm.NetworkAttachmentConfig(configs), expected) +} + type byTargetSort []swarm.NetworkAttachmentConfig func (s byTargetSort) Len() int { From f51c729cfc683240b91614e7b174dec39e015948 Mon Sep 17 00:00:00 2001 From: Genki Takiuchi Date: Sun, 26 Feb 2017 12:51:03 +0900 Subject: [PATCH 621/978] Fixed typo. Signed-off-by: Genki Takiuchi Upstream-commit: 5a53ae51707e955add875d92f830f7607770aac1 Component: cli --- components/cli/command/formatter/service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/formatter/service.go b/components/cli/command/formatter/service.go index 09f4368f4e..b13c5ee608 100644 --- a/components/cli/command/formatter/service.go +++ b/components/cli/command/formatter/service.go @@ -40,7 +40,7 @@ UpdateStatus: {{- end }} Placement: {{- if .TaskPlacementConstraints -}} - Contraints: {{ .TaskPlacementConstraints }} + Constraints: {{ .TaskPlacementConstraints }} {{- end }} {{- if .HasUpdateConfig }} UpdateConfig: From 7e228fcaaf3b30a42ce9a7d75deef8327d587540 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 27 Feb 2017 18:39:35 +0100 Subject: [PATCH 622/978] Add unit tests to cli/command/volume package Signed-off-by: Vincent Demeester Upstream-commit: 407d65df9d0929b649095ba6018e091197aa9b9b Component: cli --- components/cli/command/cli.go | 1 + components/cli/command/volume/client_test.go | 53 +++++++ components/cli/command/volume/cmd.go | 3 +- components/cli/command/volume/create.go | 7 +- components/cli/command/volume/create_test.go | 142 +++++++++++++++++ components/cli/command/volume/inspect.go | 7 +- components/cli/command/volume/inspect_test.go | 150 ++++++++++++++++++ components/cli/command/volume/list.go | 7 +- components/cli/command/volume/list_test.go | 124 +++++++++++++++ components/cli/command/volume/prune.go | 7 +- components/cli/command/volume/prune_test.go | 132 +++++++++++++++ components/cli/command/volume/remove.go | 18 +-- components/cli/command/volume/remove_test.go | 47 ++++++ ...e-inspect-with-format.json-template.golden | 1 + ...inspect-with-format.simple-template.golden | 1 + ...-format.multiple-volume-with-labels.golden | 22 +++ ...nspect-without-format.single-volume.golden | 10 ++ .../volume-list-with-config-format.golden | 3 + .../testdata/volume-list-with-format.golden | 3 + .../volume-list-without-format.golden | 4 + .../volume/testdata/volume-prune-no.golden | 2 + .../volume/testdata/volume-prune-yes.golden | 7 + .../volume-prune.deletedVolumes.golden | 6 + .../volume/testdata/volume-prune.empty.golden | 1 + components/cli/internal/test/builders/doc.go | 3 + components/cli/internal/test/builders/node.go | 3 + .../cli/internal/test/builders/volume.go | 43 +++++ components/cli/internal/test/cli.go | 37 ++++- components/cli/internal/test/doc.go | 5 + 29 files changed, 815 insertions(+), 34 deletions(-) create mode 100644 components/cli/command/volume/client_test.go create mode 100644 components/cli/command/volume/create_test.go create mode 100644 components/cli/command/volume/inspect_test.go create mode 100644 components/cli/command/volume/list_test.go create mode 100644 components/cli/command/volume/prune_test.go create mode 100644 components/cli/command/volume/remove_test.go create mode 100644 components/cli/command/volume/testdata/volume-inspect-with-format.json-template.golden create mode 100644 components/cli/command/volume/testdata/volume-inspect-with-format.simple-template.golden create mode 100644 components/cli/command/volume/testdata/volume-inspect-without-format.multiple-volume-with-labels.golden create mode 100644 components/cli/command/volume/testdata/volume-inspect-without-format.single-volume.golden create mode 100644 components/cli/command/volume/testdata/volume-list-with-config-format.golden create mode 100644 components/cli/command/volume/testdata/volume-list-with-format.golden create mode 100644 components/cli/command/volume/testdata/volume-list-without-format.golden create mode 100644 components/cli/command/volume/testdata/volume-prune-no.golden create mode 100644 components/cli/command/volume/testdata/volume-prune-yes.golden create mode 100644 components/cli/command/volume/testdata/volume-prune.deletedVolumes.golden create mode 100644 components/cli/command/volume/testdata/volume-prune.empty.golden create mode 100644 components/cli/internal/test/builders/doc.go create mode 100644 components/cli/internal/test/builders/volume.go create mode 100644 components/cli/internal/test/doc.go diff --git a/components/cli/command/cli.go b/components/cli/command/cli.go index bf9d554608..782c3a5074 100644 --- a/components/cli/command/cli.go +++ b/components/cli/command/cli.go @@ -38,6 +38,7 @@ type Cli interface { Out() *OutStream Err() io.Writer In() *InStream + ConfigFile() *configfile.ConfigFile } // DockerCli is an instance the docker command line client. diff --git a/components/cli/command/volume/client_test.go b/components/cli/command/volume/client_test.go new file mode 100644 index 0000000000..c29655cdb0 --- /dev/null +++ b/components/cli/command/volume/client_test.go @@ -0,0 +1,53 @@ +package volume + +import ( + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + volumetypes "github.com/docker/docker/api/types/volume" + "github.com/docker/docker/client" + "golang.org/x/net/context" +) + +type fakeClient struct { + client.Client + volumeCreateFunc func(volumetypes.VolumesCreateBody) (types.Volume, error) + volumeInspectFunc func(volumeID string) (types.Volume, error) + volumeListFunc func(filter filters.Args) (volumetypes.VolumesListOKBody, error) + volumeRemoveFunc func(volumeID string, force bool) error + volumePruneFunc func(filter filters.Args) (types.VolumesPruneReport, error) +} + +func (c *fakeClient) VolumeCreate(ctx context.Context, options volumetypes.VolumesCreateBody) (types.Volume, error) { + if c.volumeCreateFunc != nil { + return c.volumeCreateFunc(options) + } + return types.Volume{}, nil +} + +func (c *fakeClient) VolumeInspect(ctx context.Context, volumeID string) (types.Volume, error) { + if c.volumeInspectFunc != nil { + return c.volumeInspectFunc(volumeID) + } + return types.Volume{}, nil +} + +func (c *fakeClient) VolumeList(ctx context.Context, filter filters.Args) (volumetypes.VolumesListOKBody, error) { + if c.volumeListFunc != nil { + return c.volumeListFunc(filter) + } + return volumetypes.VolumesListOKBody{}, nil +} + +func (c *fakeClient) VolumesPrune(ctx context.Context, filter filters.Args) (types.VolumesPruneReport, error) { + if c.volumePruneFunc != nil { + return c.volumePruneFunc(filter) + } + return types.VolumesPruneReport{}, nil +} + +func (c *fakeClient) VolumeRemove(ctx context.Context, volumeID string, force bool) error { + if c.volumeRemoveFunc != nil { + return c.volumeRemoveFunc(volumeID, force) + } + return nil +} diff --git a/components/cli/command/volume/cmd.go b/components/cli/command/volume/cmd.go index 2bc7687750..4ef8381333 100644 --- a/components/cli/command/volume/cmd.go +++ b/components/cli/command/volume/cmd.go @@ -1,10 +1,9 @@ package volume import ( - "github.com/spf13/cobra" - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/spf13/cobra" ) // NewVolumeCommand returns a cobra command for `volume` subcommands diff --git a/components/cli/command/volume/create.go b/components/cli/command/volume/create.go index 21cfa84b7b..f7ca362150 100644 --- a/components/cli/command/volume/create.go +++ b/components/cli/command/volume/create.go @@ -19,7 +19,7 @@ type createOptions struct { labels opts.ListOpts } -func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { +func newCreateCommand(dockerCli command.Cli) *cobra.Command { opts := createOptions{ driverOpts: *opts.NewMapOpts(nil, nil), labels: opts.NewListOpts(opts.ValidateEnv), @@ -32,8 +32,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 1 { if opts.name != "" { - fmt.Fprint(dockerCli.Err(), "Conflicting options: either specify --name or provide positional arg, not both\n") - return cli.StatusError{StatusCode: 1} + return fmt.Errorf("Conflicting options: either specify --name or provide positional arg, not both\n") } opts.name = args[0] } @@ -50,7 +49,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runCreate(dockerCli *command.DockerCli, opts createOptions) error { +func runCreate(dockerCli command.Cli, opts createOptions) error { client := dockerCli.Client() volReq := volumetypes.VolumesCreateBody{ diff --git a/components/cli/command/volume/create_test.go b/components/cli/command/volume/create_test.go new file mode 100644 index 0000000000..b7d5a443a5 --- /dev/null +++ b/components/cli/command/volume/create_test.go @@ -0,0 +1,142 @@ +package volume + +import ( + "bytes" + "fmt" + "io/ioutil" + "strings" + "testing" + + "github.com/docker/docker/api/types" + volumetypes "github.com/docker/docker/api/types/volume" + "github.com/docker/docker/cli/internal/test" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestVolumeCreateErrors(t *testing.T) { + testCases := []struct { + args []string + flags map[string]string + volumeCreateFunc func(volumetypes.VolumesCreateBody) (types.Volume, error) + expectedError string + }{ + { + args: []string{"volumeName"}, + flags: map[string]string{ + "name": "volumeName", + }, + expectedError: "Conflicting options: either specify --name or provide positional arg, not both", + }, + { + args: []string{"too", "many"}, + expectedError: "requires at most 1 argument(s)", + }, + { + volumeCreateFunc: func(createBody volumetypes.VolumesCreateBody) (types.Volume, error) { + return types.Volume{}, fmt.Errorf("error creating volume") + }, + expectedError: "error creating volume", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newCreateCommand( + test.NewFakeCli(&fakeClient{ + volumeCreateFunc: tc.volumeCreateFunc, + }, buf), + ) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestVolumeCreateWithName(t *testing.T) { + name := "foo" + buf := new(bytes.Buffer) + cli := test.NewFakeCli(&fakeClient{ + volumeCreateFunc: func(body volumetypes.VolumesCreateBody) (types.Volume, error) { + if body.Name != name { + return types.Volume{}, fmt.Errorf("expected name %q, got %q", name, body.Name) + } + return types.Volume{ + Name: body.Name, + }, nil + }, + }, buf) + + // Test by flags + cmd := newCreateCommand(cli) + cmd.Flags().Set("name", name) + assert.NilError(t, cmd.Execute()) + assert.Equal(t, strings.TrimSpace(buf.String()), name) + + // Then by args + buf.Reset() + cmd = newCreateCommand(cli) + cmd.SetArgs([]string{name}) + assert.NilError(t, cmd.Execute()) + assert.Equal(t, strings.TrimSpace(buf.String()), name) +} + +func TestVolumeCreateWithFlags(t *testing.T) { + expectedDriver := "foo" + expectedOpts := map[string]string{ + "bar": "1", + "baz": "baz", + } + expectedLabels := map[string]string{ + "lbl1": "v1", + "lbl2": "v2", + } + name := "banana" + + buf := new(bytes.Buffer) + cli := test.NewFakeCli(&fakeClient{ + volumeCreateFunc: func(body volumetypes.VolumesCreateBody) (types.Volume, error) { + if body.Name != "" { + return types.Volume{}, fmt.Errorf("expected empty name, got %q", body.Name) + } + if body.Driver != expectedDriver { + return types.Volume{}, fmt.Errorf("expected driver %q, got %q", expectedDriver, body.Driver) + } + if !compareMap(body.DriverOpts, expectedOpts) { + return types.Volume{}, fmt.Errorf("expected drivers opts %v, got %v", expectedOpts, body.DriverOpts) + } + if !compareMap(body.Labels, expectedLabels) { + return types.Volume{}, fmt.Errorf("expected labels %v, got %v", expectedLabels, body.Labels) + } + return types.Volume{ + Name: name, + }, nil + }, + }, buf) + + cmd := newCreateCommand(cli) + cmd.Flags().Set("driver", "foo") + cmd.Flags().Set("opt", "bar=1") + cmd.Flags().Set("opt", "baz=baz") + cmd.Flags().Set("label", "lbl1=v1") + cmd.Flags().Set("label", "lbl2=v2") + assert.NilError(t, cmd.Execute()) + assert.Equal(t, strings.TrimSpace(buf.String()), name) +} + +func compareMap(actual map[string]string, expected map[string]string) bool { + if len(actual) != len(expected) { + return false + } + for key, value := range actual { + if expectedValue, ok := expected[key]; ok { + if expectedValue != value { + return false + } + } else { + return false + } + } + return true +} diff --git a/components/cli/command/volume/inspect.go b/components/cli/command/volume/inspect.go index f58b927ace..70db264951 100644 --- a/components/cli/command/volume/inspect.go +++ b/components/cli/command/volume/inspect.go @@ -1,12 +1,11 @@ package volume import ( - "golang.org/x/net/context" - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/inspect" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type inspectOptions struct { @@ -14,7 +13,7 @@ type inspectOptions struct { names []string } -func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { +func newInspectCommand(dockerCli command.Cli) *cobra.Command { var opts inspectOptions cmd := &cobra.Command{ @@ -32,7 +31,7 @@ func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { +func runInspect(dockerCli command.Cli, opts inspectOptions) error { client := dockerCli.Client() ctx := context.Background() diff --git a/components/cli/command/volume/inspect_test.go b/components/cli/command/volume/inspect_test.go new file mode 100644 index 0000000000..e2ea7b35de --- /dev/null +++ b/components/cli/command/volume/inspect_test.go @@ -0,0 +1,150 @@ +package volume + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/cli/internal/test" + // Import builders to get the builder function as package function + . "github.com/docker/docker/cli/internal/test/builders" + "github.com/docker/docker/pkg/testutil/assert" + "github.com/docker/docker/pkg/testutil/golden" +) + +func TestVolumeInspectErrors(t *testing.T) { + testCases := []struct { + args []string + flags map[string]string + volumeInspectFunc func(volumeID string) (types.Volume, error) + expectedError string + }{ + { + expectedError: "requires at least 1 argument", + }, + { + args: []string{"foo"}, + volumeInspectFunc: func(volumeID string) (types.Volume, error) { + return types.Volume{}, fmt.Errorf("error while inspecting the volume") + }, + expectedError: "error while inspecting the volume", + }, + { + args: []string{"foo"}, + flags: map[string]string{ + "format": "{{invalid format}}", + }, + expectedError: "Template parsing error", + }, + { + args: []string{"foo", "bar"}, + volumeInspectFunc: func(volumeID string) (types.Volume, error) { + if volumeID == "foo" { + return types.Volume{ + Name: "foo", + }, nil + } + return types.Volume{}, fmt.Errorf("error while inspecting the volume") + }, + expectedError: "error while inspecting the volume", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newInspectCommand( + test.NewFakeCli(&fakeClient{ + volumeInspectFunc: tc.volumeInspectFunc, + }, buf), + ) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestVolumeInspectWithoutFormat(t *testing.T) { + testCases := []struct { + name string + args []string + volumeInspectFunc func(volumeID string) (types.Volume, error) + }{ + { + name: "single-volume", + args: []string{"foo"}, + volumeInspectFunc: func(volumeID string) (types.Volume, error) { + if volumeID != "foo" { + return types.Volume{}, fmt.Errorf("Invalid volumeID, expected %s, got %s", "foo", volumeID) + } + return *Volume(), nil + }, + }, + { + name: "multiple-volume-with-labels", + args: []string{"foo", "bar"}, + volumeInspectFunc: func(volumeID string) (types.Volume, error) { + return *Volume(VolumeName(volumeID), VolumeLabels(map[string]string{ + "foo": "bar", + })), nil + }, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newInspectCommand( + test.NewFakeCli(&fakeClient{ + volumeInspectFunc: tc.volumeInspectFunc, + }, buf), + ) + cmd.SetArgs(tc.args) + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), fmt.Sprintf("volume-inspect-without-format.%s.golden", tc.name)) + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) + } +} + +func TestVolumeInspectWithFormat(t *testing.T) { + volumeInspectFunc := func(volumeID string) (types.Volume, error) { + return *Volume(VolumeLabels(map[string]string{ + "foo": "bar", + })), nil + } + testCases := []struct { + name string + format string + args []string + volumeInspectFunc func(volumeID string) (types.Volume, error) + }{ + { + name: "simple-template", + format: "{{.Name}}", + args: []string{"foo"}, + volumeInspectFunc: volumeInspectFunc, + }, + { + name: "json-template", + format: "{{json .Labels}}", + args: []string{"foo"}, + volumeInspectFunc: volumeInspectFunc, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newInspectCommand( + test.NewFakeCli(&fakeClient{ + volumeInspectFunc: tc.volumeInspectFunc, + }, buf), + ) + cmd.SetArgs(tc.args) + cmd.Flags().Set("format", tc.format) + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), fmt.Sprintf("volume-inspect-with-format.%s.golden", tc.name)) + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) + } +} diff --git a/components/cli/command/volume/list.go b/components/cli/command/volume/list.go index 0de83aea4e..3577db9554 100644 --- a/components/cli/command/volume/list.go +++ b/components/cli/command/volume/list.go @@ -3,14 +3,13 @@ package volume import ( "sort" - "golang.org/x/net/context" - "github.com/docker/docker/api/types" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/formatter" "github.com/docker/docker/opts" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type byVolumeName []*types.Volume @@ -27,7 +26,7 @@ type listOptions struct { filter opts.FilterOpt } -func newListCommand(dockerCli *command.DockerCli) *cobra.Command { +func newListCommand(dockerCli command.Cli) *cobra.Command { opts := listOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -48,7 +47,7 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runList(dockerCli *command.DockerCli, opts listOptions) error { +func runList(dockerCli command.Cli, opts listOptions) error { client := dockerCli.Client() volumes, err := client.VolumeList(context.Background(), opts.filter.Value()) if err != nil { diff --git a/components/cli/command/volume/list_test.go b/components/cli/command/volume/list_test.go new file mode 100644 index 0000000000..2f4a366333 --- /dev/null +++ b/components/cli/command/volume/list_test.go @@ -0,0 +1,124 @@ +package volume + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + volumetypes "github.com/docker/docker/api/types/volume" + "github.com/docker/docker/cli/config/configfile" + "github.com/docker/docker/cli/internal/test" + // Import builders to get the builder function as package function + . "github.com/docker/docker/cli/internal/test/builders" + "github.com/docker/docker/pkg/testutil/assert" + "github.com/docker/docker/pkg/testutil/golden" +) + +func TestVolumeListErrors(t *testing.T) { + testCases := []struct { + args []string + flags map[string]string + volumeListFunc func(filter filters.Args) (volumetypes.VolumesListOKBody, error) + expectedError string + }{ + { + args: []string{"foo"}, + expectedError: "accepts no argument", + }, + { + volumeListFunc: func(filter filters.Args) (volumetypes.VolumesListOKBody, error) { + return volumetypes.VolumesListOKBody{}, fmt.Errorf("error listing volumes") + }, + expectedError: "error listing volumes", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newListCommand( + test.NewFakeCli(&fakeClient{ + volumeListFunc: tc.volumeListFunc, + }, buf), + ) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestVolumeListWithoutFormat(t *testing.T) { + buf := new(bytes.Buffer) + cli := test.NewFakeCli(&fakeClient{ + volumeListFunc: func(filter filters.Args) (volumetypes.VolumesListOKBody, error) { + return volumetypes.VolumesListOKBody{ + Volumes: []*types.Volume{ + Volume(), + Volume(VolumeName("foo"), VolumeDriver("bar")), + Volume(VolumeName("baz"), VolumeLabels(map[string]string{ + "foo": "bar", + })), + }, + }, nil + }, + }, buf) + cli.SetConfigfile(&configfile.ConfigFile{}) + cmd := newListCommand(cli) + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "volume-list-without-format.golden") + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) +} + +func TestVolumeListWithConfigFormat(t *testing.T) { + buf := new(bytes.Buffer) + cli := test.NewFakeCli(&fakeClient{ + volumeListFunc: func(filter filters.Args) (volumetypes.VolumesListOKBody, error) { + return volumetypes.VolumesListOKBody{ + Volumes: []*types.Volume{ + Volume(), + Volume(VolumeName("foo"), VolumeDriver("bar")), + Volume(VolumeName("baz"), VolumeLabels(map[string]string{ + "foo": "bar", + })), + }, + }, nil + }, + }, buf) + cli.SetConfigfile(&configfile.ConfigFile{ + VolumesFormat: "{{ .Name }} {{ .Driver }} {{ .Labels }}", + }) + cmd := newListCommand(cli) + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "volume-list-with-config-format.golden") + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) +} + +func TestVolumeListWithFormat(t *testing.T) { + buf := new(bytes.Buffer) + cli := test.NewFakeCli(&fakeClient{ + volumeListFunc: func(filter filters.Args) (volumetypes.VolumesListOKBody, error) { + return volumetypes.VolumesListOKBody{ + Volumes: []*types.Volume{ + Volume(), + Volume(VolumeName("foo"), VolumeDriver("bar")), + Volume(VolumeName("baz"), VolumeLabels(map[string]string{ + "foo": "bar", + })), + }, + }, nil + }, + }, buf) + cli.SetConfigfile(&configfile.ConfigFile{}) + cmd := newListCommand(cli) + cmd.Flags().Set("format", "{{ .Name }} {{ .Driver }} {{ .Labels }}") + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "volume-list-with-format.golden") + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) +} diff --git a/components/cli/command/volume/prune.go b/components/cli/command/volume/prune.go index 405fbeb295..7e78c66e07 100644 --- a/components/cli/command/volume/prune.go +++ b/components/cli/command/volume/prune.go @@ -3,13 +3,12 @@ package volume import ( "fmt" - "golang.org/x/net/context" - "github.com/docker/docker/api/types/filters" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" units "github.com/docker/go-units" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type pruneOptions struct { @@ -17,7 +16,7 @@ type pruneOptions struct { } // NewPruneCommand returns a new cobra prune command for volumes -func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { +func NewPruneCommand(dockerCli command.Cli) *cobra.Command { var opts pruneOptions cmd := &cobra.Command{ @@ -47,7 +46,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command { const warning = `WARNING! This will remove all volumes not used by at least one container. Are you sure you want to continue?` -func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed uint64, output string, err error) { +func runPrune(dockerCli command.Cli, opts pruneOptions) (spaceReclaimed uint64, output string, err error) { if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) { return } diff --git a/components/cli/command/volume/prune_test.go b/components/cli/command/volume/prune_test.go new file mode 100644 index 0000000000..c07834675e --- /dev/null +++ b/components/cli/command/volume/prune_test.go @@ -0,0 +1,132 @@ +package volume + +import ( + "bytes" + "fmt" + "io/ioutil" + "runtime" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/cli/internal/test" + "github.com/docker/docker/pkg/testutil/assert" + "github.com/docker/docker/pkg/testutil/golden" +) + +func TestVolumePruneErrors(t *testing.T) { + testCases := []struct { + args []string + flags map[string]string + volumePruneFunc func(args filters.Args) (types.VolumesPruneReport, error) + expectedError string + }{ + { + args: []string{"foo"}, + expectedError: "accepts no argument", + }, + { + flags: map[string]string{ + "force": "true", + }, + volumePruneFunc: func(args filters.Args) (types.VolumesPruneReport, error) { + return types.VolumesPruneReport{}, fmt.Errorf("error pruning volumes") + }, + expectedError: "error pruning volumes", + }, + } + for _, tc := range testCases { + cmd := NewPruneCommand( + test.NewFakeCli(&fakeClient{ + volumePruneFunc: tc.volumePruneFunc, + }, ioutil.Discard), + ) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestVolumePruneForce(t *testing.T) { + testCases := []struct { + name string + volumePruneFunc func(args filters.Args) (types.VolumesPruneReport, error) + }{ + { + name: "empty", + }, + { + name: "deletedVolumes", + volumePruneFunc: simplePruneFunc, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := NewPruneCommand( + test.NewFakeCli(&fakeClient{ + volumePruneFunc: tc.volumePruneFunc, + }, buf), + ) + cmd.Flags().Set("force", "true") + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), fmt.Sprintf("volume-prune.%s.golden", tc.name)) + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) + } +} +func TestVolumePrunePromptYes(t *testing.T) { + if runtime.GOOS == "windows" { + // FIXME(vdemeester) make it work.. + t.Skip("skipping this test on Windows") + } + for _, input := range []string{"y", "Y"} { + buf := new(bytes.Buffer) + cli := test.NewFakeCli(&fakeClient{ + volumePruneFunc: simplePruneFunc, + }, buf) + + cli.SetIn(ioutil.NopCloser(strings.NewReader(input))) + cmd := NewPruneCommand( + cli, + ) + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "volume-prune-yes.golden") + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) + } +} + +func TestVolumePrunePromptNo(t *testing.T) { + if runtime.GOOS == "windows" { + // FIXME(vdemeester) make it work.. + t.Skip("skipping this test on Windows") + } + for _, input := range []string{"n", "N", "no", "anything", "really"} { + buf := new(bytes.Buffer) + cli := test.NewFakeCli(&fakeClient{ + volumePruneFunc: simplePruneFunc, + }, buf) + + cli.SetIn(ioutil.NopCloser(strings.NewReader(input))) + cmd := NewPruneCommand( + cli, + ) + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "volume-prune-no.golden") + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) + } +} + +func simplePruneFunc(args filters.Args) (types.VolumesPruneReport, error) { + return types.VolumesPruneReport{ + VolumesDeleted: []string{ + "foo", "bar", "baz", + }, + SpaceReclaimed: 2000, + }, nil +} diff --git a/components/cli/command/volume/remove.go b/components/cli/command/volume/remove.go index f464bb3e1a..c1267f1eab 100644 --- a/components/cli/command/volume/remove.go +++ b/components/cli/command/volume/remove.go @@ -2,12 +2,12 @@ package volume import ( "fmt" - - "golang.org/x/net/context" + "strings" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type removeOptions struct { @@ -16,7 +16,7 @@ type removeOptions struct { volumes []string } -func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { +func newRemoveCommand(dockerCli command.Cli) *cobra.Command { var opts removeOptions cmd := &cobra.Command{ @@ -38,22 +38,22 @@ func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runRemove(dockerCli *command.DockerCli, opts *removeOptions) error { +func runRemove(dockerCli command.Cli, opts *removeOptions) error { client := dockerCli.Client() ctx := context.Background() - status := 0 + + var errs []string for _, name := range opts.volumes { if err := client.VolumeRemove(ctx, name, opts.force); err != nil { - fmt.Fprintf(dockerCli.Err(), "%s\n", err) - status = 1 + errs = append(errs, err.Error()) continue } fmt.Fprintf(dockerCli.Out(), "%s\n", name) } - if status != 0 { - return cli.StatusError{StatusCode: status} + if len(errs) > 0 { + return fmt.Errorf("%s", strings.Join(errs, "\n")) } return nil } diff --git a/components/cli/command/volume/remove_test.go b/components/cli/command/volume/remove_test.go new file mode 100644 index 0000000000..b2a106c22d --- /dev/null +++ b/components/cli/command/volume/remove_test.go @@ -0,0 +1,47 @@ +package volume + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + + "github.com/docker/docker/cli/internal/test" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestVolumeRemoveErrors(t *testing.T) { + testCases := []struct { + args []string + volumeRemoveFunc func(volumeID string, force bool) error + expectedError string + }{ + { + expectedError: "requires at least 1 argument", + }, + { + args: []string{"nodeID"}, + volumeRemoveFunc: func(volumeID string, force bool) error { + return fmt.Errorf("error removing the volume") + }, + expectedError: "error removing the volume", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newRemoveCommand( + test.NewFakeCli(&fakeClient{ + volumeRemoveFunc: tc.volumeRemoveFunc, + }, buf)) + cmd.SetArgs(tc.args) + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestNodeRemoveMultiple(t *testing.T) { + buf := new(bytes.Buffer) + cmd := newRemoveCommand(test.NewFakeCli(&fakeClient{}, buf)) + cmd.SetArgs([]string{"volume1", "volume2"}) + assert.NilError(t, cmd.Execute()) +} diff --git a/components/cli/command/volume/testdata/volume-inspect-with-format.json-template.golden b/components/cli/command/volume/testdata/volume-inspect-with-format.json-template.golden new file mode 100644 index 0000000000..2393cd01d4 --- /dev/null +++ b/components/cli/command/volume/testdata/volume-inspect-with-format.json-template.golden @@ -0,0 +1 @@ +{"foo":"bar"} diff --git a/components/cli/command/volume/testdata/volume-inspect-with-format.simple-template.golden b/components/cli/command/volume/testdata/volume-inspect-with-format.simple-template.golden new file mode 100644 index 0000000000..4833bbb039 --- /dev/null +++ b/components/cli/command/volume/testdata/volume-inspect-with-format.simple-template.golden @@ -0,0 +1 @@ +volume diff --git a/components/cli/command/volume/testdata/volume-inspect-without-format.multiple-volume-with-labels.golden b/components/cli/command/volume/testdata/volume-inspect-without-format.multiple-volume-with-labels.golden new file mode 100644 index 0000000000..19cad5024c --- /dev/null +++ b/components/cli/command/volume/testdata/volume-inspect-without-format.multiple-volume-with-labels.golden @@ -0,0 +1,22 @@ +[ + { + "Driver": "local", + "Labels": { + "foo": "bar" + }, + "Mountpoint": "/data/volume", + "Name": "foo", + "Options": null, + "Scope": "local" + }, + { + "Driver": "local", + "Labels": { + "foo": "bar" + }, + "Mountpoint": "/data/volume", + "Name": "bar", + "Options": null, + "Scope": "local" + } +] diff --git a/components/cli/command/volume/testdata/volume-inspect-without-format.single-volume.golden b/components/cli/command/volume/testdata/volume-inspect-without-format.single-volume.golden new file mode 100644 index 0000000000..22d0c5a659 --- /dev/null +++ b/components/cli/command/volume/testdata/volume-inspect-without-format.single-volume.golden @@ -0,0 +1,10 @@ +[ + { + "Driver": "local", + "Labels": null, + "Mountpoint": "/data/volume", + "Name": "volume", + "Options": null, + "Scope": "local" + } +] diff --git a/components/cli/command/volume/testdata/volume-list-with-config-format.golden b/components/cli/command/volume/testdata/volume-list-with-config-format.golden new file mode 100644 index 0000000000..72fa0bd4d7 --- /dev/null +++ b/components/cli/command/volume/testdata/volume-list-with-config-format.golden @@ -0,0 +1,3 @@ +baz local foo=bar +foo bar +volume local diff --git a/components/cli/command/volume/testdata/volume-list-with-format.golden b/components/cli/command/volume/testdata/volume-list-with-format.golden new file mode 100644 index 0000000000..72fa0bd4d7 --- /dev/null +++ b/components/cli/command/volume/testdata/volume-list-with-format.golden @@ -0,0 +1,3 @@ +baz local foo=bar +foo bar +volume local diff --git a/components/cli/command/volume/testdata/volume-list-without-format.golden b/components/cli/command/volume/testdata/volume-list-without-format.golden new file mode 100644 index 0000000000..9cf779e827 --- /dev/null +++ b/components/cli/command/volume/testdata/volume-list-without-format.golden @@ -0,0 +1,4 @@ +DRIVER VOLUME NAME +local baz +bar foo +local volume diff --git a/components/cli/command/volume/testdata/volume-prune-no.golden b/components/cli/command/volume/testdata/volume-prune-no.golden new file mode 100644 index 0000000000..df5a315973 --- /dev/null +++ b/components/cli/command/volume/testdata/volume-prune-no.golden @@ -0,0 +1,2 @@ +WARNING! This will remove all volumes not used by at least one container. +Are you sure you want to continue? [y/N] Total reclaimed space: 0B diff --git a/components/cli/command/volume/testdata/volume-prune-yes.golden b/components/cli/command/volume/testdata/volume-prune-yes.golden new file mode 100644 index 0000000000..9f6054e92a --- /dev/null +++ b/components/cli/command/volume/testdata/volume-prune-yes.golden @@ -0,0 +1,7 @@ +WARNING! This will remove all volumes not used by at least one container. +Are you sure you want to continue? [y/N] Deleted Volumes: +foo +bar +baz + +Total reclaimed space: 2kB diff --git a/components/cli/command/volume/testdata/volume-prune.deletedVolumes.golden b/components/cli/command/volume/testdata/volume-prune.deletedVolumes.golden new file mode 100644 index 0000000000..fbe996c74d --- /dev/null +++ b/components/cli/command/volume/testdata/volume-prune.deletedVolumes.golden @@ -0,0 +1,6 @@ +Deleted Volumes: +foo +bar +baz + +Total reclaimed space: 2kB diff --git a/components/cli/command/volume/testdata/volume-prune.empty.golden b/components/cli/command/volume/testdata/volume-prune.empty.golden new file mode 100644 index 0000000000..6c537e1ac2 --- /dev/null +++ b/components/cli/command/volume/testdata/volume-prune.empty.golden @@ -0,0 +1 @@ +Total reclaimed space: 0B diff --git a/components/cli/internal/test/builders/doc.go b/components/cli/internal/test/builders/doc.go new file mode 100644 index 0000000000..eac991c2e4 --- /dev/null +++ b/components/cli/internal/test/builders/doc.go @@ -0,0 +1,3 @@ +// Package builders helps you create struct for your unit test while keeping them expressive. +// +package builders diff --git a/components/cli/internal/test/builders/node.go b/components/cli/internal/test/builders/node.go index 63fdebba12..040955785c 100644 --- a/components/cli/internal/test/builders/node.go +++ b/components/cli/internal/test/builders/node.go @@ -8,6 +8,9 @@ import ( // Node creates a node with default values. // Any number of node function builder can be pass to augment it. +// +// n1 := Node() // Returns a default node +// n2 := Node(NodeID("foo"), NodeHostname("bar"), Leader()) func Node(builders ...func(*swarm.Node)) *swarm.Node { t1 := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) node := &swarm.Node{ diff --git a/components/cli/internal/test/builders/volume.go b/components/cli/internal/test/builders/volume.go new file mode 100644 index 0000000000..9b84df4238 --- /dev/null +++ b/components/cli/internal/test/builders/volume.go @@ -0,0 +1,43 @@ +package builders + +import ( + "github.com/docker/docker/api/types" +) + +// Volume creates a volume with default values. +// Any number of volume function builder can be pass to augment it. +func Volume(builders ...func(volume *types.Volume)) *types.Volume { + volume := &types.Volume{ + Name: "volume", + Driver: "local", + Mountpoint: "/data/volume", + Scope: "local", + } + + for _, builder := range builders { + builder(volume) + } + + return volume +} + +// VolumeLabels sets the volume labels +func VolumeLabels(labels map[string]string) func(volume *types.Volume) { + return func(volume *types.Volume) { + volume.Labels = labels + } +} + +// VolumeName sets the volume labels +func VolumeName(name string) func(volume *types.Volume) { + return func(volume *types.Volume) { + volume.Name = name + } +} + +// VolumeDriver sets the volume driver +func VolumeDriver(name string) func(volume *types.Volume) { + return func(volume *types.Volume) { + volume.Driver = name + } +} diff --git a/components/cli/internal/test/cli.go b/components/cli/internal/test/cli.go index 06ab053e98..72de42586d 100644 --- a/components/cli/internal/test/cli.go +++ b/components/cli/internal/test/cli.go @@ -1,21 +1,23 @@ -// Package test is a test-only package that can be used by other cli package to write unit test package test import ( "io" "io/ioutil" + "strings" "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/config/configfile" "github.com/docker/docker/client" - "strings" ) // FakeCli emulates the default DockerCli type FakeCli struct { command.DockerCli - client client.APIClient - out io.Writer - in io.ReadCloser + client client.APIClient + configfile *configfile.ConfigFile + out io.Writer + err io.Writer + in io.ReadCloser } // NewFakeCli returns a Cli backed by the fakeCli @@ -23,6 +25,7 @@ func NewFakeCli(client client.APIClient, out io.Writer) *FakeCli { return &FakeCli{ client: client, out: out, + err: ioutil.Discard, in: ioutil.NopCloser(strings.NewReader("")), } } @@ -32,17 +35,37 @@ func (c *FakeCli) SetIn(in io.ReadCloser) { c.in = in } +// SetErr sets the standard error stream th cli should write on +func (c *FakeCli) SetErr(err io.Writer) { + c.err = err +} + +// SetConfigfile sets the "fake" config file +func (c *FakeCli) SetConfigfile(configfile *configfile.ConfigFile) { + c.configfile = configfile +} + // Client returns a docker API client func (c *FakeCli) Client() client.APIClient { return c.client } -// Out returns the output stream the cli should write on +// Out returns the output stream (stdout) the cli should write on func (c *FakeCli) Out() *command.OutStream { return command.NewOutStream(c.out) } -// In returns thi input stream the cli will use +// Err returns the output stream (stderr) the cli should write on +func (c *FakeCli) Err() io.Writer { + return c.err +} + +// In returns the input stream the cli will use func (c *FakeCli) In() *command.InStream { return command.NewInStream(c.in) } + +// ConfigFile returns the cli configfile object (to get client configuration) +func (c *FakeCli) ConfigFile() *configfile.ConfigFile { + return c.configfile +} diff --git a/components/cli/internal/test/doc.go b/components/cli/internal/test/doc.go new file mode 100644 index 0000000000..41601bd8f1 --- /dev/null +++ b/components/cli/internal/test/doc.go @@ -0,0 +1,5 @@ +// Package test is a test-only package that can be used by other cli package to write unit test. +// +// It as an internal package and cannot be used outside of github.com/docker/docker/cli package. +// +package test From 3d78fa63857a3a88f0fc39bc747d41a696a0ec32 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Thu, 19 Jan 2017 15:27:37 -0800 Subject: [PATCH 623/978] Topology-aware scheduling This adds support for placement preferences in Swarm services. - Convert PlacementPreferences between GRPC API and HTTP API - Add --placement-pref, --placement-pref-add and --placement-pref-rm to CLI - Add support for placement preferences in service inspect --pretty - Add integration test Signed-off-by: Aaron Lehmann Upstream-commit: 21d5d1fa9dfa652585f0398fe186557d406fedee Component: cli --- components/cli/command/formatter/service.go | 18 +++++- components/cli/command/service/create.go | 2 + components/cli/command/service/opts.go | 55 +++++++++++++++++-- components/cli/command/service/update.go | 44 ++++++++++++++- components/cli/command/service/update_test.go | 30 +++++++++- 5 files changed, 139 insertions(+), 10 deletions(-) diff --git a/components/cli/command/formatter/service.go b/components/cli/command/formatter/service.go index b13c5ee608..b8b476dd66 100644 --- a/components/cli/command/formatter/service.go +++ b/components/cli/command/formatter/service.go @@ -39,9 +39,12 @@ UpdateStatus: Message: {{ .UpdateStatusMessage }} {{- end }} Placement: -{{- if .TaskPlacementConstraints -}} +{{- if .TaskPlacementConstraints }} Constraints: {{ .TaskPlacementConstraints }} {{- end }} +{{- if .TaskPlacementPreferences }} + Preferences: {{ .TaskPlacementPreferences }} +{{- end }} {{- if .HasUpdateConfig }} UpdateConfig: Parallelism: {{ .UpdateParallelism }} @@ -211,6 +214,19 @@ func (ctx *serviceInspectContext) TaskPlacementConstraints() []string { return nil } +func (ctx *serviceInspectContext) TaskPlacementPreferences() []string { + if ctx.Service.Spec.TaskTemplate.Placement == nil { + return nil + } + var strings []string + for _, pref := range ctx.Service.Spec.TaskTemplate.Placement.Preferences { + if pref.Spread != nil { + strings = append(strings, "spread="+pref.Spread.SpreadDescriptor) + } + } + return strings +} + func (ctx *serviceInspectContext) HasUpdateConfig() bool { return ctx.Service.Spec.UpdateConfig != nil } diff --git a/components/cli/command/service/create.go b/components/cli/command/service/create.go index ab90868424..c2eb81727a 100644 --- a/components/cli/command/service/create.go +++ b/components/cli/command/service/create.go @@ -37,6 +37,8 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Var(&opts.envFile, flagEnvFile, "Read in a file of environment variables") flags.Var(&opts.mounts, flagMount, "Attach a filesystem mount to the service") flags.Var(&opts.constraints, flagConstraint, "Placement constraints") + flags.Var(&opts.placementPrefs, flagPlacementPref, "Add a placement preference") + flags.SetAnnotation(flagPlacementPref, "version", []string{"1.27"}) flags.Var(&opts.networks, flagNetwork, "Network attachments") flags.Var(&opts.secrets, flagSecret, "Specify secrets to expose to the service") flags.SetAnnotation(flagSecret, "version", []string{"1.25"}) diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index d8618e73ca..060f7017fa 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -1,6 +1,7 @@ package service import ( + "errors" "fmt" "strconv" "strings" @@ -117,6 +118,45 @@ func (f *floatValue) Value() float32 { return float32(*f) } +// placementPrefOpts holds a list of placement preferences. +type placementPrefOpts struct { + prefs []swarm.PlacementPreference + strings []string +} + +func (opts *placementPrefOpts) String() string { + if len(opts.strings) == 0 { + return "" + } + return fmt.Sprintf("%v", opts.strings) +} + +// Set validates the input value and adds it to the internal slices. +// Note: in the future strategies other than "spread", may be supported, +// as well as additional comma-separated options. +func (opts *placementPrefOpts) Set(value string) error { + fields := strings.Split(value, "=") + if len(fields) != 2 { + return errors.New(`placement preference must be of the format "="`) + } + if fields[0] != "spread" { + return fmt.Errorf("unsupported placement preference %s (only spread is supported)", fields[0]) + } + + opts.prefs = append(opts.prefs, swarm.PlacementPreference{ + Spread: &swarm.SpreadOver{ + SpreadDescriptor: fields[1], + }, + }) + opts.strings = append(opts.strings, value) + return nil +} + +// Type returns a string name for this Option type +func (opts *placementPrefOpts) Type() string { + return "pref" +} + type updateOptions struct { parallelism uint64 delay time.Duration @@ -283,11 +323,12 @@ type serviceOptions struct { replicas Uint64Opt mode string - restartPolicy restartPolicyOptions - constraints opts.ListOpts - update updateOptions - networks opts.ListOpts - endpoint endpointOptions + restartPolicy restartPolicyOptions + constraints opts.ListOpts + placementPrefs placementPrefOpts + update updateOptions + networks opts.ListOpts + endpoint endpointOptions registryAuth bool @@ -398,6 +439,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { RestartPolicy: opts.restartPolicy.ToRestartPolicy(), Placement: &swarm.Placement{ Constraints: opts.constraints.GetAll(), + Preferences: opts.placementPrefs.prefs, }, LogDriver: opts.logDriver.toLogDriver(), }, @@ -473,6 +515,9 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { } const ( + flagPlacementPref = "placement-pref" + flagPlacementPrefAdd = "placement-pref-add" + flagPlacementPrefRemove = "placement-pref-rm" flagConstraint = "constraint" flagConstraintRemove = "constraint-rm" flagConstraintAdd = "constraint-add" diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index 7f461c90a9..1300e5e381 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -69,6 +69,10 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.SetAnnotation(flagSecretAdd, "version", []string{"1.25"}) flags.Var(&serviceOpts.mounts, flagMountAdd, "Add or update a mount on a service") flags.Var(&serviceOpts.constraints, flagConstraintAdd, "Add or update a placement constraint") + flags.Var(&serviceOpts.placementPrefs, flagPlacementPrefAdd, "Add a placement preference") + flags.SetAnnotation(flagPlacementPrefAdd, "version", []string{"1.27"}) + flags.Var(&placementPrefOpts{}, flagPlacementPrefRemove, "Remove a placement preference") + flags.SetAnnotation(flagPlacementPrefRemove, "version", []string{"1.27"}) flags.Var(&serviceOpts.endpoint.publishPorts, flagPublishAdd, "Add or update a published port") flags.Var(&serviceOpts.groups, flagGroupAdd, "Add an additional supplementary user group to the container") flags.SetAnnotation(flagGroupAdd, "version", []string{"1.25"}) @@ -260,7 +264,14 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error { if task.Placement == nil { task.Placement = &swarm.Placement{} } - updatePlacement(flags, task.Placement) + updatePlacementConstraints(flags, task.Placement) + } + + if anyChanged(flags, flagPlacementPrefAdd, flagPlacementPrefRemove) { + if task.Placement == nil { + task.Placement = &swarm.Placement{} + } + updatePlacementPreferences(flags, task.Placement) } if err := updateReplicas(flags, &spec.Mode); err != nil { @@ -372,7 +383,7 @@ func anyChanged(flags *pflag.FlagSet, fields ...string) bool { return false } -func updatePlacement(flags *pflag.FlagSet, placement *swarm.Placement) { +func updatePlacementConstraints(flags *pflag.FlagSet, placement *swarm.Placement) { if flags.Changed(flagConstraintAdd) { values := flags.Lookup(flagConstraintAdd).Value.(*opts.ListOpts).GetAll() placement.Constraints = append(placement.Constraints, values...) @@ -391,6 +402,35 @@ func updatePlacement(flags *pflag.FlagSet, placement *swarm.Placement) { placement.Constraints = newConstraints } +func updatePlacementPreferences(flags *pflag.FlagSet, placement *swarm.Placement) { + var newPrefs []swarm.PlacementPreference + + if flags.Changed(flagPlacementPrefRemove) { + for _, existing := range placement.Preferences { + removed := false + for _, removal := range flags.Lookup(flagPlacementPrefRemove).Value.(*placementPrefOpts).prefs { + if removal.Spread != nil && existing.Spread != nil && removal.Spread.SpreadDescriptor == existing.Spread.SpreadDescriptor { + removed = true + break + } + } + if !removed { + newPrefs = append(newPrefs, existing) + } + } + } else { + newPrefs = placement.Preferences + } + + if flags.Changed(flagPlacementPrefAdd) { + for _, addition := range flags.Lookup(flagPlacementPrefAdd).Value.(*placementPrefOpts).prefs { + newPrefs = append(newPrefs, addition) + } + } + + placement.Preferences = newPrefs +} + func updateContainerLabels(flags *pflag.FlagSet, field *map[string]string) { if flags.Changed(flagContainerLabelAdd) { if *field == nil { diff --git a/components/cli/command/service/update_test.go b/components/cli/command/service/update_test.go index f2887e229d..422ab33dac 100644 --- a/components/cli/command/service/update_test.go +++ b/components/cli/command/service/update_test.go @@ -51,7 +51,7 @@ func TestUpdateLabelsRemoveALabelThatDoesNotExist(t *testing.T) { assert.Equal(t, len(labels), 1) } -func TestUpdatePlacement(t *testing.T) { +func TestUpdatePlacementConstraints(t *testing.T) { flags := newUpdateCommand(nil).Flags() flags.Set("constraint-add", "node=toadd") flags.Set("constraint-rm", "node!=toremove") @@ -60,12 +60,38 @@ func TestUpdatePlacement(t *testing.T) { Constraints: []string{"node!=toremove", "container=tokeep"}, } - updatePlacement(flags, placement) + updatePlacementConstraints(flags, placement) assert.Equal(t, len(placement.Constraints), 2) assert.Equal(t, placement.Constraints[0], "container=tokeep") assert.Equal(t, placement.Constraints[1], "node=toadd") } +func TestUpdatePlacementPrefs(t *testing.T) { + flags := newUpdateCommand(nil).Flags() + flags.Set("placement-pref-add", "spread=node.labels.dc") + flags.Set("placement-pref-rm", "spread=node.labels.rack") + + placement := &swarm.Placement{ + Preferences: []swarm.PlacementPreference{ + { + Spread: &swarm.SpreadOver{ + SpreadDescriptor: "node.labels.rack", + }, + }, + { + Spread: &swarm.SpreadOver{ + SpreadDescriptor: "node.labels.row", + }, + }, + }, + } + + updatePlacementPreferences(flags, placement) + assert.Equal(t, len(placement.Preferences), 2) + assert.Equal(t, placement.Preferences[0].Spread.SpreadDescriptor, "node.labels.row") + assert.Equal(t, placement.Preferences[1].Spread.SpreadDescriptor, "node.labels.dc") +} + func TestUpdateEnvironment(t *testing.T) { flags := newUpdateCommand(nil).Flags() flags.Set("env-add", "toadd=newenv") From 01c1fd80b7758859a77e97be6d2e475c2ee5d145 Mon Sep 17 00:00:00 2001 From: Anusha Ragunathan Date: Fri, 24 Feb 2017 15:35:10 -0800 Subject: [PATCH 624/978] 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 Upstream-commit: 14e8332f2d079bc9ec2824809df81c8ef556bdc3 Component: cli --- components/cli/plugin_install.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/plugin_install.go b/components/cli/plugin_install.go index 33876cc101..ce3e0506e5 100644 --- a/components/cli/plugin_install.go +++ b/components/cli/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 e1a103614c961083c54861ee656116859972b718 Mon Sep 17 00:00:00 2001 From: Doug Davis Date: Tue, 10 Jan 2017 19:27:55 -0800 Subject: [PATCH 625/978] Add the mediaType to the error Without this fix the error the client might see is: target is unknown which wasn't helpful to me when I saw this today. With this fix I now see: MediaType is unknown: 'text/html' which helped me track down the issue to the registry I was talking to. Signed-off-by: Doug Davis Upstream-commit: 05a3caff2309cf4b0334f74107f4d8c751028109 Component: cli --- components/cli/command/image/pull.go | 2 +- components/cli/command/plugin/install.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/command/image/pull.go b/components/cli/command/image/pull.go index 967beca86f..08e2e8b7e5 100644 --- a/components/cli/command/image/pull.go +++ b/components/cli/command/image/pull.go @@ -76,7 +76,7 @@ func runPull(dockerCli *command.DockerCli, opts pullOptions) error { err = imagePullPrivileged(ctx, dockerCli, authConfig, reference.FamiliarString(distributionRef), requestPrivilege, opts.all) } if err != nil { - if strings.Contains(err.Error(), "target is plugin") { + if strings.Contains(err.Error(), "when fetching 'plugin'") { return errors.New(err.Error() + " - Use `docker plugin install`") } return err diff --git a/components/cli/command/plugin/install.go b/components/cli/command/plugin/install.go index ebfe1f1eec..d15784f03f 100644 --- a/components/cli/command/plugin/install.go +++ b/components/cli/command/plugin/install.go @@ -152,7 +152,7 @@ func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error { responseBody, err := dockerCli.Client().PluginInstall(ctx, alias, options) if err != nil { - if strings.Contains(err.Error(), "target is image") { + if strings.Contains(err.Error(), "(image) when fetching") { return errors.New(err.Error() + " - Use `docker image pull`") } return err From 96ae1f807ebf9058ca47949050f16a73f8eddd5b Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Wed, 1 Mar 2017 01:28:33 +0800 Subject: [PATCH 626/978] 'docker daemon' deprecation message doesn't use the new versioning scheme Signed-off-by: yuexiao-wang Upstream-commit: 3f82787403dfedcde10aae809325417a1300dd94 Component: cli --- components/cli/command/system/info.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/system/info.go b/components/cli/command/system/info.go index d9fafd1aa1..28d69c286c 100644 --- a/components/cli/command/system/info.go +++ b/components/cli/command/system/info.go @@ -271,7 +271,7 @@ func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error { fmt.Fprintf(dockerCli.Out(), " %s\n", attribute) } // TODO: Engine labels with duplicate keys has been deprecated in 1.13 and will be error out - // after 3 release cycles (1.16). For now, a WARNING will be generated. The following will + // after 3 release cycles (17.12). For now, a WARNING will be generated. The following will // be removed eventually. labelMap := map[string]string{} for _, label := range info.Labels { From 1fcffa8b010148fe1daa2d4adf92444000bc5b13 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Fri, 3 Mar 2017 13:26:00 -0800 Subject: [PATCH 627/978] Fix error caused by overlapping merge of 30733 This fix fixes build error caused by overlapping merge of 30733 and 28213. Signed-off-by: Yong Tang Upstream-commit: e94294e902a649b66c77c5641524fc841265d2ce Component: cli --- components/cli/command/formatter/task.go | 23 +++++++++++-------- components/cli/command/formatter/task_test.go | 6 ++--- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/components/cli/command/formatter/task.go b/components/cli/command/formatter/task.go index caf7651515..2c6e7bb124 100644 --- a/components/cli/command/formatter/task.go +++ b/components/cli/command/formatter/task.go @@ -52,9 +52,22 @@ func TaskWrite(ctx Context, tasks []swarm.Task, names map[string]string, nodes m } return nil } - return ctx.Write(&taskContext{}, render) + taskCtx := taskContext{} + taskCtx.header = taskHeaderContext{ + "ID": taskIDHeader, + "Name": nameHeader, + "Image": imageHeader, + "Node": nodeHeader, + "DesiredState": desiredStateHeader, + "CurrentState": currentStateHeader, + "Error": errorHeader, + "Ports": portsHeader, + } + return ctx.Write(&taskCtx, render) } +type taskHeaderContext map[string]string + type taskContext struct { HeaderContext trunc bool @@ -68,7 +81,6 @@ func (c *taskContext) MarshalJSON() ([]byte, error) { } func (c *taskContext) ID() string { - c.AddHeader(taskIDHeader) if c.trunc { return stringid.TruncateID(c.task.ID) } @@ -76,12 +88,10 @@ func (c *taskContext) ID() string { } func (c *taskContext) Name() string { - c.AddHeader(nameHeader) return c.name } func (c *taskContext) Image() string { - c.AddHeader(imageHeader) image := c.task.Spec.ContainerSpec.Image if c.trunc { ref, err := reference.ParseNormalizedNamed(image) @@ -98,17 +108,14 @@ func (c *taskContext) Image() string { } func (c *taskContext) Node() string { - c.AddHeader(nodeHeader) return c.node } func (c *taskContext) DesiredState() string { - c.AddHeader(desiredStateHeader) return command.PrettyPrint(c.task.DesiredState) } func (c *taskContext) CurrentState() string { - c.AddHeader(currentStateHeader) return fmt.Sprintf("%s %s ago", command.PrettyPrint(c.task.Status.State), strings.ToLower(units.HumanDuration(time.Since(c.task.Status.Timestamp))), @@ -116,7 +123,6 @@ func (c *taskContext) CurrentState() string { } func (c *taskContext) Error() string { - c.AddHeader(errorHeader) // Trim and quote the error message. taskErr := c.task.Status.Err if c.trunc && len(taskErr) > maxErrLength { @@ -129,7 +135,6 @@ func (c *taskContext) Error() string { } func (c *taskContext) Ports() string { - c.AddHeader(portsHeader) if len(c.task.Status.PortStatus.Ports) == 0 { return "" } diff --git a/components/cli/command/formatter/task_test.go b/components/cli/command/formatter/task_test.go index c990f68619..8de9d66f57 100644 --- a/components/cli/command/formatter/task_test.go +++ b/components/cli/command/formatter/task_test.go @@ -32,10 +32,10 @@ taskID2 `, }, { - Context{Format: NewTaskFormat("table {{.Name}} {{.Node}} {{.Ports}}", false)}, + Context{Format: NewTaskFormat("table {{.Name}}\t{{.Node}}\t{{.Ports}}", false)}, `NAME NODE PORTS -foobar_baz foo1 -foobar_bar foo2 +foobar_baz foo1 +foobar_bar foo2 `, }, { From 3fc9649978535b39e0dd62c64345085c77b2fea8 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Wed, 15 Feb 2017 14:53:58 -0800 Subject: [PATCH 628/978] Add support for the "rollback" failure action Signed-off-by: Aaron Lehmann Upstream-commit: 5232868f46c0650c9a9cd565c65fbb0744dd91c6 Component: cli --- components/cli/command/service/opts.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index 36ab2c6301..b9ae89ad0c 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -487,7 +487,7 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { flags.DurationVar(&opts.update.delay, flagUpdateDelay, time.Duration(0), "Delay between updates (ns|us|ms|s|m|h) (default 0s)") flags.DurationVar(&opts.update.monitor, flagUpdateMonitor, time.Duration(0), "Duration after each task update to monitor for failure (ns|us|ms|s|m|h) (default 0s)") flags.SetAnnotation(flagUpdateMonitor, "version", []string{"1.25"}) - flags.StringVar(&opts.update.onFailure, flagUpdateFailureAction, "pause", `Action on update failure ("pause"|"continue")`) + flags.StringVar(&opts.update.onFailure, flagUpdateFailureAction, "pause", `Action on update failure ("pause"|"continue"|"rollback")`) flags.Var(&opts.update.maxFailureRatio, flagUpdateMaxFailureRatio, "Failure rate to tolerate during an update") flags.SetAnnotation(flagUpdateMaxFailureRatio, "version", []string{"1.25"}) From b3a86717f1af6f2ba10210bd4a001143b70c04a5 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Wed, 15 Feb 2017 16:04:30 -0800 Subject: [PATCH 629/978] Add support for rollback flags Signed-off-by: Aaron Lehmann Upstream-commit: 8de01fb7a8ff5589a57400684cd6fc07fbd89308 Component: cli --- components/cli/command/formatter/service.go | 44 ++++ .../cli/command/service/inspect_test.go | 1 - components/cli/command/service/opts.go | 188 ++++++++++-------- components/cli/command/service/update.go | 11 + 4 files changed, 160 insertions(+), 84 deletions(-) diff --git a/components/cli/command/formatter/service.go b/components/cli/command/formatter/service.go index 421728976f..98c760ed7f 100644 --- a/components/cli/command/formatter/service.go +++ b/components/cli/command/formatter/service.go @@ -57,6 +57,18 @@ UpdateConfig: {{- end }} Max failure ratio: {{ .UpdateMaxFailureRatio }} {{- end }} +{{- if .HasRollbackConfig }} +RollbackConfig: + Parallelism: {{ .RollbackParallelism }} +{{- if .HasRollbackDelay}} + Delay: {{ .RollbackDelay }} +{{- end }} + On failure: {{ .RollbackOnFailure }} +{{- if .HasRollbackMonitor}} + Monitoring Period: {{ .RollbackMonitor }} +{{- end }} + Max failure ratio: {{ .RollbackMaxFailureRatio }} +{{- end }} ContainerSpec: Image: {{ .ContainerImage }} {{- if .ContainerArgs }} @@ -259,6 +271,38 @@ func (ctx *serviceInspectContext) UpdateMaxFailureRatio() float32 { return ctx.Service.Spec.UpdateConfig.MaxFailureRatio } +func (ctx *serviceInspectContext) HasRollbackConfig() bool { + return ctx.Service.Spec.RollbackConfig != nil +} + +func (ctx *serviceInspectContext) RollbackParallelism() uint64 { + return ctx.Service.Spec.RollbackConfig.Parallelism +} + +func (ctx *serviceInspectContext) HasRollbackDelay() bool { + return ctx.Service.Spec.RollbackConfig.Delay.Nanoseconds() > 0 +} + +func (ctx *serviceInspectContext) RollbackDelay() time.Duration { + return ctx.Service.Spec.RollbackConfig.Delay +} + +func (ctx *serviceInspectContext) RollbackOnFailure() string { + return ctx.Service.Spec.RollbackConfig.FailureAction +} + +func (ctx *serviceInspectContext) HasRollbackMonitor() bool { + return ctx.Service.Spec.RollbackConfig.Monitor.Nanoseconds() > 0 +} + +func (ctx *serviceInspectContext) RollbackMonitor() time.Duration { + return ctx.Service.Spec.RollbackConfig.Monitor +} + +func (ctx *serviceInspectContext) RollbackMaxFailureRatio() float32 { + return ctx.Service.Spec.RollbackConfig.MaxFailureRatio +} + func (ctx *serviceInspectContext) ContainerImage() string { return ctx.Service.Spec.TaskTemplate.ContainerSpec.Image } diff --git a/components/cli/command/service/inspect_test.go b/components/cli/command/service/inspect_test.go index 34c41ee78a..94c96cc164 100644 --- a/components/cli/command/service/inspect_test.go +++ b/components/cli/command/service/inspect_test.go @@ -49,7 +49,6 @@ func formatServiceInspect(t *testing.T, format formatter.Format, now time.Time) Replicas: &two, }, }, - UpdateConfig: nil, Networks: []swarm.NetworkAttachmentConfig{ { Target: "5vpyomhb6ievnk0i0o60gcnei", diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index b9ae89ad0c..baaa58e1f0 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -165,6 +165,16 @@ type updateOptions struct { maxFailureRatio floatValue } +func (opts updateOptions) config() *swarm.UpdateConfig { + return &swarm.UpdateConfig{ + Parallelism: opts.parallelism, + Delay: opts.delay, + Monitor: opts.monitor, + FailureAction: opts.onFailure, + MaxFailureRatio: opts.maxFailureRatio.Value(), + } +} + type resourceOptions struct { limitCPU opts.NanoCPUs limitMemBytes opts.MemBytes @@ -328,6 +338,7 @@ type serviceOptions struct { constraints opts.ListOpts placementPrefs placementPrefOpts update updateOptions + rollback updateOptions networks opts.ListOpts endpoint endpointOptions @@ -445,16 +456,11 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { }, LogDriver: opts.logDriver.toLogDriver(), }, - Networks: convertNetworks(opts.networks.GetAll()), - Mode: serviceMode, - UpdateConfig: &swarm.UpdateConfig{ - Parallelism: opts.update.parallelism, - Delay: opts.update.delay, - Monitor: opts.update.monitor, - FailureAction: opts.update.onFailure, - MaxFailureRatio: opts.update.maxFailureRatio.Value(), - }, - EndpointSpec: opts.endpoint.ToEndpointSpec(), + Networks: convertNetworks(opts.networks.GetAll()), + Mode: serviceMode, + UpdateConfig: opts.update.config(), + RollbackConfig: opts.rollback.config(), + EndpointSpec: opts.endpoint.ToEndpointSpec(), } return service, nil @@ -491,6 +497,17 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { flags.Var(&opts.update.maxFailureRatio, flagUpdateMaxFailureRatio, "Failure rate to tolerate during an update") flags.SetAnnotation(flagUpdateMaxFailureRatio, "version", []string{"1.25"}) + flags.Uint64Var(&opts.rollback.parallelism, flagRollbackParallelism, 1, "Maximum number of tasks rolled back simultaneously (0 to roll back all at once)") + flags.SetAnnotation(flagRollbackParallelism, "version", []string{"1.27"}) + flags.DurationVar(&opts.rollback.delay, flagRollbackDelay, time.Duration(0), "Delay between task rollbacks (ns|us|ms|s|m|h) (default 0s)") + flags.SetAnnotation(flagRollbackDelay, "version", []string{"1.27"}) + flags.DurationVar(&opts.rollback.monitor, flagRollbackMonitor, time.Duration(0), "Duration after each task rollback to monitor for failure (ns|us|ms|s|m|h) (default 0s)") + flags.SetAnnotation(flagRollbackMonitor, "version", []string{"1.27"}) + flags.StringVar(&opts.rollback.onFailure, flagRollbackFailureAction, "pause", `Action on rollback failure ("pause"|"continue")`) + flags.SetAnnotation(flagRollbackFailureAction, "version", []string{"1.27"}) + flags.Var(&opts.rollback.maxFailureRatio, flagRollbackMaxFailureRatio, "Failure rate to tolerate during a rollback") + flags.SetAnnotation(flagRollbackMaxFailureRatio, "version", []string{"1.27"}) + flags.StringVar(&opts.endpoint.mode, flagEndpointMode, "vip", "Endpoint mode (vip or dnsrr)") flags.BoolVar(&opts.registryAuth, flagRegistryAuth, false, "Send registry authentication details to swarm agents") @@ -520,77 +537,82 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { } const ( - flagPlacementPref = "placement-pref" - flagPlacementPrefAdd = "placement-pref-add" - flagPlacementPrefRemove = "placement-pref-rm" - flagConstraint = "constraint" - flagConstraintRemove = "constraint-rm" - flagConstraintAdd = "constraint-add" - flagContainerLabel = "container-label" - flagContainerLabelRemove = "container-label-rm" - flagContainerLabelAdd = "container-label-add" - flagDNS = "dns" - flagDNSRemove = "dns-rm" - flagDNSAdd = "dns-add" - flagDNSOption = "dns-option" - flagDNSOptionRemove = "dns-option-rm" - flagDNSOptionAdd = "dns-option-add" - flagDNSSearch = "dns-search" - flagDNSSearchRemove = "dns-search-rm" - flagDNSSearchAdd = "dns-search-add" - flagEndpointMode = "endpoint-mode" - flagHost = "host" - flagHostAdd = "host-add" - flagHostRemove = "host-rm" - flagHostname = "hostname" - flagEnv = "env" - flagEnvFile = "env-file" - flagEnvRemove = "env-rm" - flagEnvAdd = "env-add" - flagGroup = "group" - flagGroupAdd = "group-add" - flagGroupRemove = "group-rm" - flagLabel = "label" - flagLabelRemove = "label-rm" - flagLabelAdd = "label-add" - flagLimitCPU = "limit-cpu" - flagLimitMemory = "limit-memory" - flagMode = "mode" - flagMount = "mount" - flagMountRemove = "mount-rm" - flagMountAdd = "mount-add" - flagName = "name" - flagNetwork = "network" - flagPublish = "publish" - flagPublishRemove = "publish-rm" - flagPublishAdd = "publish-add" - flagReadOnly = "read-only" - flagReplicas = "replicas" - flagReserveCPU = "reserve-cpu" - flagReserveMemory = "reserve-memory" - flagRestartCondition = "restart-condition" - flagRestartDelay = "restart-delay" - flagRestartMaxAttempts = "restart-max-attempts" - flagRestartWindow = "restart-window" - flagStopGracePeriod = "stop-grace-period" - flagStopSignal = "stop-signal" - flagTTY = "tty" - flagUpdateDelay = "update-delay" - flagUpdateFailureAction = "update-failure-action" - flagUpdateMaxFailureRatio = "update-max-failure-ratio" - flagUpdateMonitor = "update-monitor" - flagUpdateParallelism = "update-parallelism" - flagUser = "user" - flagWorkdir = "workdir" - flagRegistryAuth = "with-registry-auth" - flagLogDriver = "log-driver" - flagLogOpt = "log-opt" - flagHealthCmd = "health-cmd" - flagHealthInterval = "health-interval" - flagHealthRetries = "health-retries" - flagHealthTimeout = "health-timeout" - flagNoHealthcheck = "no-healthcheck" - flagSecret = "secret" - flagSecretAdd = "secret-add" - flagSecretRemove = "secret-rm" + flagPlacementPref = "placement-pref" + flagPlacementPrefAdd = "placement-pref-add" + flagPlacementPrefRemove = "placement-pref-rm" + flagConstraint = "constraint" + flagConstraintRemove = "constraint-rm" + flagConstraintAdd = "constraint-add" + flagContainerLabel = "container-label" + flagContainerLabelRemove = "container-label-rm" + flagContainerLabelAdd = "container-label-add" + flagDNS = "dns" + flagDNSRemove = "dns-rm" + flagDNSAdd = "dns-add" + flagDNSOption = "dns-option" + flagDNSOptionRemove = "dns-option-rm" + flagDNSOptionAdd = "dns-option-add" + flagDNSSearch = "dns-search" + flagDNSSearchRemove = "dns-search-rm" + flagDNSSearchAdd = "dns-search-add" + flagEndpointMode = "endpoint-mode" + flagHost = "host" + flagHostAdd = "host-add" + flagHostRemove = "host-rm" + flagHostname = "hostname" + flagEnv = "env" + flagEnvFile = "env-file" + flagEnvRemove = "env-rm" + flagEnvAdd = "env-add" + flagGroup = "group" + flagGroupAdd = "group-add" + flagGroupRemove = "group-rm" + flagLabel = "label" + flagLabelRemove = "label-rm" + flagLabelAdd = "label-add" + flagLimitCPU = "limit-cpu" + flagLimitMemory = "limit-memory" + flagMode = "mode" + flagMount = "mount" + flagMountRemove = "mount-rm" + flagMountAdd = "mount-add" + flagName = "name" + flagNetwork = "network" + flagPublish = "publish" + flagPublishRemove = "publish-rm" + flagPublishAdd = "publish-add" + flagReadOnly = "read-only" + flagReplicas = "replicas" + flagReserveCPU = "reserve-cpu" + flagReserveMemory = "reserve-memory" + flagRestartCondition = "restart-condition" + flagRestartDelay = "restart-delay" + flagRestartMaxAttempts = "restart-max-attempts" + flagRestartWindow = "restart-window" + flagRollbackDelay = "rollback-delay" + flagRollbackFailureAction = "rollback-failure-action" + flagRollbackMaxFailureRatio = "rollback-max-failure-ratio" + flagRollbackMonitor = "rollback-monitor" + flagRollbackParallelism = "rollback-parallelism" + flagStopGracePeriod = "stop-grace-period" + flagStopSignal = "stop-signal" + flagTTY = "tty" + flagUpdateDelay = "update-delay" + flagUpdateFailureAction = "update-failure-action" + flagUpdateMaxFailureRatio = "update-max-failure-ratio" + flagUpdateMonitor = "update-monitor" + flagUpdateParallelism = "update-parallelism" + flagUser = "user" + flagWorkdir = "workdir" + flagRegistryAuth = "with-registry-auth" + flagLogDriver = "log-driver" + flagLogOpt = "log-opt" + flagHealthCmd = "health-cmd" + flagHealthInterval = "health-interval" + flagHealthRetries = "health-retries" + flagHealthTimeout = "health-timeout" + flagNoHealthcheck = "no-healthcheck" + flagSecret = "secret" + flagSecretAdd = "secret-add" + flagSecretRemove = "secret-rm" ) diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index 0c19c0713e..b529331500 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -289,6 +289,17 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error { updateFloatValue(flagUpdateMaxFailureRatio, &spec.UpdateConfig.MaxFailureRatio) } + if anyChanged(flags, flagRollbackParallelism, flagRollbackDelay, flagRollbackMonitor, flagRollbackFailureAction, flagRollbackMaxFailureRatio) { + if spec.RollbackConfig == nil { + spec.RollbackConfig = &swarm.UpdateConfig{} + } + updateUint64(flagRollbackParallelism, &spec.RollbackConfig.Parallelism) + updateDuration(flagRollbackDelay, &spec.RollbackConfig.Delay) + updateDuration(flagRollbackMonitor, &spec.RollbackConfig.Monitor) + updateString(flagRollbackFailureAction, &spec.RollbackConfig.FailureAction) + updateFloatValue(flagRollbackMaxFailureRatio, &spec.RollbackConfig.MaxFailureRatio) + } + if flags.Changed(flagEndpointMode) { value, _ := flags.GetString(flagEndpointMode) if spec.EndpointSpec == nil { From 41bd9caf74246fe5f3f0b0c5dffead8c84e630b5 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Thu, 16 Feb 2017 09:27:01 -0800 Subject: [PATCH 630/978] 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 Upstream-commit: eafb5565c9cb876fb2de19c4d02bf4462f9d6310 Component: cli --- components/cli/service_update.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/components/cli/service_update.go b/components/cli/service_update.go index afa94d47e2..873a1e0556 100644 --- a/components/cli/service_update.go +++ b/components/cli/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 4009dade4305101544798c04a12d0d1ae1225a34 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Thu, 16 Feb 2017 09:27:01 -0800 Subject: [PATCH 631/978] 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 Upstream-commit: 78c204ef798c7380e11ba26e5cd231e04fc6efe4 Component: cli --- components/cli/command/service/update.go | 43 +++++++++++++++++++++--- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index b529331500..ab8391e038 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -1,6 +1,7 @@ package service import ( + "errors" "fmt" "sort" "strings" @@ -10,6 +11,7 @@ import ( "github.com/docker/docker/api/types/container" mounttypes "github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/api/types/versions" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/client" @@ -95,7 +97,6 @@ func newListOptsVar() *opts.ListOpts { func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID string) error { apiClient := dockerCli.Client() ctx := context.Background() - updateOpts := types.ServiceUpdateOptions{} service, _, err := apiClient.ServiceInspectWithRaw(ctx, serviceID) if err != nil { @@ -107,12 +108,44 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID str return err } + // There are two ways to do user-requested rollback. The old way is + // client-side, but with a sufficiently recent daemon we prefer + // server-side, because it will honor the rollback parameters. + var ( + clientSideRollback bool + serverSideRollback bool + ) + spec := &service.Spec if rollback { - spec = service.PreviousSpec - if spec == nil { - return fmt.Errorf("service does not have a previous specification to roll back to") + // Rollback can't be combined with other flags. + otherFlagsPassed := false + flags.VisitAll(func(f *pflag.Flag) { + if f.Name == "rollback" { + return + } + if flags.Changed(f.Name) { + otherFlagsPassed = true + } + }) + if otherFlagsPassed { + return errors.New("other flags may not be combined with --rollback") } + + if versions.LessThan(dockerCli.Client().ClientVersion(), "1.27") { + clientSideRollback = true + spec = service.PreviousSpec + if spec == nil { + return fmt.Errorf("service does not have a previous specification to roll back to") + } + } else { + serverSideRollback = true + } + } + + updateOpts := types.ServiceUpdateOptions{} + if serverSideRollback { + updateOpts.Rollback = "previous" } err = updateService(flags, spec) @@ -147,7 +180,7 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID str return err } updateOpts.EncodedRegistryAuth = encodedAuth - } else if rollback { + } else if clientSideRollback { updateOpts.RegistryAuthFrom = types.RegistryAuthFromPreviousSpec } else { updateOpts.RegistryAuthFrom = types.RegistryAuthFromSpec From d1cc836571347ac93e37d48e6a1c76dd8ae16625 Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Sun, 5 Mar 2017 15:25:11 +1100 Subject: [PATCH 632/978] cmd: docker: fix TestDaemonCommand In more recent versions of Cobra, `--help` parsing is done before anything else resulting in TestDaemonCommand not actually passing. I'm actually unsure if this test ever passed since it appears that !daemon is not being run as part of the test suite. Signed-off-by: Aleksa Sarai Upstream-commit: aa74f278667c29752d71071503098cf71988c054 Component: cli --- components/cli/daemon_none.go | 6 ++++-- components/cli/daemon_none_test.go | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/components/cli/daemon_none.go b/components/cli/daemon_none.go index 65f9f37be2..6fbd000125 100644 --- a/components/cli/daemon_none.go +++ b/components/cli/daemon_none.go @@ -12,8 +12,10 @@ import ( func newDaemonCommand() *cobra.Command { return &cobra.Command{ - Use: "daemon", - Hidden: true, + Use: "daemon", + Hidden: true, + Args: cobra.ArbitraryArgs, + DisableFlagParsing: true, RunE: func(cmd *cobra.Command, args []string) error { return runDaemon() }, diff --git a/components/cli/daemon_none_test.go b/components/cli/daemon_none_test.go index 32032fe1b3..bd42add986 100644 --- a/components/cli/daemon_none_test.go +++ b/components/cli/daemon_none_test.go @@ -10,7 +10,7 @@ import ( func TestDaemonCommand(t *testing.T) { cmd := newDaemonCommand() - cmd.SetArgs([]string{"--help"}) + cmd.SetArgs([]string{"--version"}) err := cmd.Execute() assert.Error(t, err, "Please run `dockerd`") From b6d1eb47816851700dce3a1e8600f5e7f516f643 Mon Sep 17 00:00:00 2001 From: Nikhil Chawla Date: Mon, 6 Mar 2017 17:31:04 +0530 Subject: [PATCH 633/978] Fixed the typo in the code Signed-off-by: Nikhil Chawla Upstream-commit: 80a8d7ca26b13745d6a3e83b9bc8d043b09d5ebf Component: cli --- components/cli/cobra.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/cobra.go b/components/cli/cobra.go index 962b314412..b01774f04a 100644 --- a/components/cli/cobra.go +++ b/components/cli/cobra.go @@ -30,7 +30,7 @@ func SetupRootCommand(rootCmd *cobra.Command) { // docker/docker/cli error messages func FlagErrorFunc(cmd *cobra.Command, err error) error { if err == nil { - return err + return nil } usage := "" From d9230d0bd1ae3d9373fc1dbabdb4e4635d369161 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 19 Jan 2017 16:48:30 -0500 Subject: [PATCH 634/978] Add expanded mount syntax to Compose schema and types. Signed-off-by: Daniel Nephin Upstream-commit: d2d48f3f6993012c03580285c25c86a2e899b685 Component: cli --- components/cli/compose/schema/bindata.go | 2 +- .../schema/data/config_schema_v3.1.json | 32 +++++++++- components/cli/compose/schema/schema.go | 62 +++++++++++++------ components/cli/compose/types/types.go | 22 ++++++- 4 files changed, 97 insertions(+), 21 deletions(-) diff --git a/components/cli/compose/schema/bindata.go b/components/cli/compose/schema/bindata.go index 0b5aa18b75..e4ef29bc72 100644 --- a/components/cli/compose/schema/bindata.go +++ b/components/cli/compose/schema/bindata.go @@ -89,7 +89,7 @@ func dataConfig_schema_v30Json() (*asset, error) { return a, nil } -var _dataConfig_schema_v31Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x5a\xcd\x8f\xdc\x28\x16\xbf\xd7\x5f\x61\x39\xb9\xa5\x3f\xb2\xda\x68\xa5\xcd\x6d\x8f\x7b\x9a\x39\x4f\xcb\xb1\x28\xfb\x95\x8b\x34\x06\x02\xb8\xd2\x95\xa8\xff\xf7\x11\xfe\x2a\x8c\xc1\xe0\x2e\xf7\x74\x34\x9a\x53\x77\x99\xdf\x03\xde\xf7\xe3\xc1\xcf\x5d\x92\xa4\xef\x65\x71\x84\x1a\xa5\x9f\x93\xf4\xa8\x14\xff\x7c\x7f\xff\x55\x32\x7a\xdb\x7d\xbd\x63\xa2\xba\x2f\x05\x3a\xa8\xdb\x8f\x9f\xee\xbb\x6f\xef\xd2\x1b\x4d\x87\x4b\x4d\x52\x30\x7a\xc0\x55\xde\x8d\xe4\xa7\x7f\xdf\xfd\xeb\x4e\x93\x77\x10\x75\xe6\xa0\x41\x6c\xff\x15\x0a\xd5\x7d\x13\xf0\xad\xc1\x02\x34\xf1\x43\x7a\x02\x21\x31\xa3\x69\x76\xb3\xd3\x63\x5c\x30\x0e\x42\x61\x90\xe9\xe7\x44\x6f\x2e\x49\x46\xc8\xf0\xc1\x98\x56\x2a\x81\x69\x95\xb6\x9f\x9f\xdb\x19\x92\x24\x95\x20\x4e\xb8\x30\x66\x18\xb7\xfa\xee\xfe\x32\xff\xfd\x08\xbb\xb1\x67\x35\x36\xdb\x7e\xe7\x48\x29\x10\xf4\xf7\xf9\xde\xda\xe1\x2f\x0f\xe8\xf6\xc7\xff\x6e\xff\xf8\x78\xfb\xdf\xbb\xfc\x36\xfb\xf0\x7e\x32\xac\xe5\x2b\xe0\xd0\x2d\x5f\xc2\x01\x53\xac\x30\xa3\xe3\xfa\xe9\x88\x7c\xee\xff\x7b\x1e\x17\x46\x65\xd9\x82\x11\x99\xac\x7d\x40\x44\xc2\x94\x67\x0a\xea\x3b\x13\x8f\x21\x9e\x47\xd8\x1b\xf1\xdc\xaf\xef\xe0\x79\xca\xce\x89\x91\xa6\x0e\x6a\x70\x40\xbd\x11\x33\xdd\xf2\xdb\xe8\x4f\x42\x21\x40\x85\x4d\xb6\x43\xbd\x99\xc5\xea\xe5\xaf\x63\x78\x37\x30\xbd\x88\xed\x10\xc6\xda\xed\x06\x27\xee\xed\x12\x95\xcb\xbd\xfc\xb2\x1a\x85\xe5\x91\x52\x09\x9c\xb0\xb3\xfe\xe6\x91\x47\x07\xa8\x81\xaa\x74\x14\x41\x92\xa4\xfb\x06\x93\xd2\x96\x28\xa3\xf0\x9b\x9e\xe2\xc1\xf8\x98\x24\x3f\xed\x48\x66\xcc\xd3\x8e\x4f\x7e\xf9\x15\x3e\x8e\x7b\x78\x19\xc7\x0b\x46\x15\x3c\xa9\x96\xa9\xe5\xa5\x3b\x11\xb0\xe2\x11\xc4\x01\x13\x88\xa5\x40\xa2\x92\x0b\x22\x23\x58\xaa\x9c\x89\xbc\xc4\x85\x4a\x9f\x2d\xf2\xd9\x7c\x61\x7b\x1a\x49\x8d\x5f\xd9\xce\x31\x61\x5a\x20\x9e\xa3\xb2\x9c\xf0\x81\x84\x40\xe7\xf4\x26\x49\xb1\x82\x5a\xba\x59\x4c\xd2\x86\xe2\x6f\x0d\xfc\xbf\x87\x28\xd1\x80\x3d\x6f\x29\x18\xdf\x7e\xe2\x4a\xb0\x86\xe7\x1c\x09\x6d\x60\xcb\xe2\x4f\x0b\x56\xd7\x88\x6e\x65\x75\x6b\xf8\x88\x90\x3c\xa3\x0a\x61\x0a\x22\xa7\xa8\x0e\x19\x92\xf6\x3a\xa0\xa5\xcc\xbb\x84\xbf\x68\x46\x87\xbc\xa3\x97\xd6\x04\x63\xf6\xdf\x54\x1f\x25\x5d\x32\xec\x6e\x1a\x6d\xda\x7a\x6f\xa9\x45\x98\x4b\x40\xa2\x38\xbe\x90\x9e\xd5\x08\xd3\x18\xd9\x01\x55\xe2\xcc\x19\xee\xec\xe5\x97\x33\x04\xa0\xa7\x7c\x8c\x25\xab\xc5\x00\xf4\x84\x05\xa3\xf5\xe0\x0d\x31\x01\x66\x0c\xf2\x9a\xfe\x89\x33\x09\xb6\x60\x2c\x06\xcd\xa1\x91\xd5\x89\x4c\x06\x8a\x87\x81\xf1\x9b\x24\xa5\x4d\xbd\x07\xa1\x6b\xd8\x09\xf2\xc0\x44\x8d\xf4\x66\x87\xb5\x8d\xe1\x89\xa4\x1d\x96\x67\x0a\xd0\xe4\x41\xa7\x75\x44\x72\x82\xe9\xe3\xf6\x26\x0e\x4f\x4a\xa0\xfc\xc8\xa4\x8a\x8f\xe1\x06\xf9\x11\x10\x51\xc7\xe2\x08\xc5\xe3\x02\xb9\x89\x9a\x50\x33\xa9\x62\x8c\x1c\xd7\xa8\x0a\x83\x78\x11\x82\x10\xb4\x07\xf2\x22\x3e\x37\x15\xbe\x31\x2d\xab\x2a\x0d\xf5\x59\xdc\xac\x72\xe9\x87\x43\x39\xbf\x14\xf8\x04\x22\x36\x81\x33\x7e\x29\xb8\xec\xc1\x70\x01\x92\x84\xab\xcf\x09\xf4\xcb\x5d\x57\x7c\x2e\x78\x55\xfb\x1f\x21\x69\x66\x97\x0b\x89\x95\xf7\x5d\x5f\x2c\x0e\xe3\x0a\x8a\x89\x56\x6a\x54\xe8\xba\x41\x80\xf4\xe8\xf5\x02\xed\x4f\x37\x79\xcd\x4a\x9f\x81\xce\xc0\xb6\x6c\xbc\x91\x7a\x75\x22\x4c\x5e\x54\x3f\x46\xa9\x2e\x78\x80\x08\x70\xe3\xdb\x5e\xec\x36\x2f\xdb\x0d\x9b\x58\x8b\x43\x04\x23\x09\x61\x67\xf7\x0a\x72\x32\x1b\xe6\xa7\x4f\x91\x36\xe1\xa2\xfd\xcf\x22\xad\x87\xd4\x3b\x67\x7c\x8d\x1c\x98\xea\xb2\x95\xd6\xdd\x5c\x1b\xc9\x02\xde\xf6\xca\x25\x3c\xc7\xa5\x3f\x56\xb4\x11\xc2\x74\x30\xce\x84\x9a\x79\xd7\xfa\x74\xef\xb3\x60\x53\x5c\x43\x9c\xba\x24\xfc\x6e\xf1\x99\x34\x66\xea\x8e\x22\x9a\xfb\x5f\xd0\x3f\xc2\x9e\x91\x2e\x44\x29\x07\x5a\x21\x51\xc1\xf4\x18\x82\xa9\x82\x0a\x84\x87\x80\x37\x7b\x82\xe5\x11\xca\x35\x34\x82\x29\x56\x30\x12\xe7\x18\xce\xe3\x67\xbc\x33\x4c\x27\xcc\xae\xae\xcd\xb8\xc0\x27\x4c\xa0\xb2\x38\xde\x33\x46\x00\xd1\x49\xa2\x10\x80\xca\x9c\x51\x72\x8e\x40\x4a\x85\x44\xf0\xf8\x27\xa1\x68\x04\x56\xe7\x9c\x71\xb5\x79\x55\x28\x8f\x75\x2e\xf1\x0f\x98\xfa\xde\xc5\xea\xfb\x89\x32\x6b\x43\x56\x3f\x2b\x79\x2d\xf7\xf3\x99\xed\x2b\xb9\x8d\x64\x8d\x28\xae\x73\x9c\x45\x7c\x33\x0d\x72\xcb\xe0\x6a\x0d\x78\xe6\xf0\xbd\x0a\x43\x35\xd4\xa2\xab\x38\x03\xb5\x3c\xcb\x42\xbd\xac\xb6\x96\xaa\xc4\x34\x67\x1c\x68\xd0\x37\xa4\x62\x3c\xaf\x04\x2a\x20\xe7\x20\x30\x73\x8a\x62\x12\x60\xcb\x46\x20\xbd\xfe\x7c\x1a\x89\x2b\x8a\xdc\x71\xc7\x80\xaa\x9a\x1f\x5e\xd8\x04\x50\x2a\xec\xec\x0d\xc1\x35\xf6\x3b\x8d\xc3\x6a\x23\xea\xb5\xae\x56\x73\x97\x68\x0b\xe5\x59\x54\xc8\x5e\x38\x21\x2c\x1f\x10\x22\x4e\x06\x47\x24\x56\xa4\x8e\xd6\x31\x0f\x9e\xfc\xe4\x3a\x37\x38\xf7\x35\xb9\x99\x6a\xe7\xbb\xe9\x37\x92\x39\xf1\xab\x4a\x2f\x7b\x1b\x99\xb7\xfa\x71\x3b\x55\x23\x83\x87\xb8\x16\x43\xe5\xd2\x01\x64\x84\x1a\x57\x2c\x9b\x66\x0b\x7d\xa8\xd1\x4e\x50\x62\xf7\x6e\x77\x16\x67\x2b\x2e\x49\xac\xfe\xc2\x30\x81\xab\xfb\x6f\x42\x83\xb7\x25\xcb\x37\x11\x3d\xc8\x7b\x4b\x80\x25\xda\x5b\xfd\x71\x97\x73\x6b\x6b\x14\xa7\x70\x8c\x11\xa0\x04\xb6\xf4\x32\x04\x6a\x33\x9e\x80\xfc\x35\x9b\x7c\x0a\xd7\xc0\x1a\x77\xc2\xdb\x99\xf6\xdd\x13\xa5\xc6\x2d\x4a\x40\xa9\x06\xd2\xd6\xe9\xc3\xa8\xd4\xe1\x2c\x10\x54\x5c\x8c\x93\x08\xe0\x04\x17\x48\x86\x02\xd1\x15\xcd\xa4\x86\x97\x48\x41\xde\xdd\xa2\xaf\x0a\xfd\x0b\x31\x9f\x23\x81\x08\x01\x82\x65\x1d\x13\x43\xd3\x12\x08\x3a\xbf\x28\x7d\xb6\xe4\x07\x84\x49\x23\x20\x47\x85\xea\x2f\xea\x03\x36\x97\xd6\x8c\x62\xc5\x9c\x11\x22\x6e\xc9\x1a\x3d\xe5\xc3\xb2\x2d\x24\x54\xd9\x4c\x8b\xfa\xd8\x3e\x90\x61\x09\x5d\xe1\xb7\x2e\x3b\x2f\xa8\xe8\x92\xeb\x3d\x16\x33\xac\x38\x63\x5d\x80\xd4\x91\x64\x6c\xd3\x05\xe9\x83\xa9\xa5\x3f\x65\xe4\x9c\x11\x5c\x9c\xb7\xe2\xb0\x60\xb4\x13\x72\x8c\x41\x5c\x69\x81\xda\x1c\x74\x29\x54\x73\x15\x74\xd6\x96\xe0\x3b\xa6\x25\xfb\xbe\x62\xc1\xed\x4c\x89\x13\x54\x80\x15\xef\xae\x15\xb4\x54\x02\x61\xaa\x56\xa7\xf3\x6b\xd9\xba\x22\x9b\x8f\xf6\x19\x88\xfa\x23\x2e\xfc\xea\xc1\x13\xe9\x0b\xde\x04\x7b\xb7\x35\xd4\x4c\x38\x0d\x70\x83\x67\x39\x21\x16\x07\xd8\x06\x59\x2d\xaa\xd9\xdf\xa3\x72\xc6\xb7\x3f\x6d\x84\x1b\xfa\x59\x38\x20\x61\x8e\xea\xad\xbc\x23\xfa\xfa\x23\x75\xe6\xe0\x64\xb9\x6f\x91\xf8\x7b\x17\xa1\x5d\x87\xf7\xde\x23\x64\xb3\xa7\x9e\x16\xc2\xfc\x94\xb1\x65\x53\x6c\xc3\xa0\x37\xdc\x5c\x7a\xb4\xfa\x30\xd6\xcc\x37\xa3\xac\xb2\x68\x15\x7b\xaf\x0d\xb7\xdb\x7f\x5b\xbe\xdb\x2d\x02\x57\x9d\x8f\x94\x42\xc5\x31\xea\x48\xb0\xb2\x68\xbc\x22\x0e\xf5\x4f\xd5\x02\x61\xa8\x47\xfd\x13\x85\xfe\x26\x36\xfb\xd7\xd9\x57\xff\x32\x30\xf8\x24\xaf\x45\xbd\x38\x8f\x47\xbc\x43\xfb\x05\x74\xf6\xd6\xaa\x98\xf6\x20\x0d\x95\xcc\xdb\x03\x4b\x92\x8c\xbe\x28\xed\x29\xb2\xe9\x36\x6c\x98\xe3\xf1\xf6\x34\x99\x2e\xf5\x9c\x06\x88\xe7\x2a\xc6\x5a\xb4\x17\xe2\x32\xe7\x1b\x06\x9b\xbb\x0f\x0b\x25\xc3\xd2\x83\x86\x57\xca\xb5\x1b\xf4\xf3\xdc\x3a\xb5\xce\x19\x83\x74\xe7\x0f\x72\x3d\xfe\x6f\xd0\xcf\x9e\xe7\x6a\x3e\xe9\x79\xd6\xbe\xfa\x39\xed\xc9\x76\x4f\x6b\xb3\x89\x7c\x2c\x48\xf7\x3c\xc8\x88\xee\x99\x79\xf4\xf2\xa9\xd1\xf9\x68\xd7\xee\x08\x0f\x8f\x67\x3d\x17\x20\x3b\xf3\x6f\xfb\xd0\x79\xf7\xbc\xfb\x33\x00\x00\xff\xff\xfa\xcc\x57\x15\x61\x31\x00\x00") +var _dataConfig_schema_v31Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x5b\xcd\x73\xdc\x28\x16\xbf\xf7\x5f\xa1\x52\x72\x8b\x3f\xb2\xb5\xa9\xad\xda\xdc\xf6\xb8\xa7\x99\xf3\xb8\x3a\x2a\x5a\x7a\xad\x26\x46\x40\x00\xb5\xdd\x49\xf9\x7f\x9f\xd2\x67\x03\x02\x81\xba\xe5\x38\x33\x35\x27\xdb\xe2\xf7\x80\xf7\xfd\x1e\xe0\x1f\x9b\x24\x49\xdf\xcb\xfc\x00\x15\x4a\x3f\x27\xe9\x41\x29\xfe\xf9\xfe\xfe\xab\x64\xf4\xb6\xfb\x7a\xc7\x44\x79\x5f\x08\xb4\x57\xb7\x1f\x3f\xdd\x77\xdf\xde\xa5\x37\x0d\x1d\x2e\x1a\x92\x9c\xd1\x3d\x2e\xb3\x6e\x24\x3b\xfe\xfb\xee\x5f\x77\x0d\x79\x07\x51\x27\x0e\x0d\x88\xed\xbe\x42\xae\xba\x6f\x02\xbe\xd5\x58\x40\x43\xfc\x90\x1e\x41\x48\xcc\x68\xba\xbd\xd9\x34\x63\x5c\x30\x0e\x42\x61\x90\xe9\xe7\xa4\xd9\x5c\x92\x8c\x90\xe1\x83\x36\xad\x54\x02\xd3\x32\x6d\x3f\xbf\xb4\x33\x24\x49\x2a\x41\x1c\x71\xae\xcd\x30\x6e\xf5\xdd\xfd\x79\xfe\xfb\x11\x76\x63\xcf\xaa\x6d\xb6\xfd\xce\x91\x52\x20\xe8\xef\xd3\xbd\xb5\xc3\x5f\x1e\xd0\xed\xf7\xff\xdd\xfe\xf1\xf1\xf6\xbf\x77\xd9\xed\xf6\xc3\x7b\x63\xb8\x91\xaf\x80\x7d\xb7\x7c\x01\x7b\x4c\xb1\xc2\x8c\x8e\xeb\xa7\x23\xf2\xa5\xff\xed\x65\x5c\x18\x15\x45\x0b\x46\xc4\x58\x7b\x8f\x88\x04\x93\x67\x0a\xea\x89\x89\xc7\x10\xcf\x23\xec\x8d\x78\xee\xd7\x77\xf0\x6c\xb2\x73\x64\xa4\xae\x82\x1a\x1c\x50\x6f\xc4\x4c\xb7\xfc\x3a\xfa\x93\x90\x0b\x50\x61\x93\xed\x50\x6f\x66\xb1\xcd\xf2\xd7\x31\xbc\x19\x98\x9e\xc5\x76\x08\x6d\xed\x76\x83\x86\x7b\xbb\x44\xe5\x72\x2f\xbf\xac\x46\x61\x79\xa4\x54\x00\x27\xec\xd4\x7c\xf3\xc8\xa3\x03\x54\x40\x55\x3a\x8a\x20\x49\xd2\x5d\x8d\x49\x61\x4b\x94\x51\xf8\xad\x99\xe2\x41\xfb\x98\x24\x3f\xec\x48\xa6\xcd\xd3\x8e\x1b\x7f\xf9\x15\x3e\x8e\x7b\x78\x19\xc7\x73\x46\x15\x3c\xab\x96\xa9\xf9\xa5\x3b\x11\xb0\xfc\x11\xc4\x1e\x13\x88\xa5\x40\xa2\x94\x33\x22\x23\x58\xaa\x8c\x89\xac\xc0\xb9\x4a\x5f\x2c\xf2\xc9\x7c\x61\x7b\x1a\x49\xb5\xbf\xb6\x1b\xc7\x84\x69\x8e\x78\x86\x8a\xc2\xe0\x03\x09\x81\x4e\xe9\x4d\x92\x62\x05\x95\x74\xb3\x98\xa4\x35\xc5\xdf\x6a\xf8\x7f\x0f\x51\xa2\x06\x7b\xde\x42\x30\xbe\xfe\xc4\xa5\x60\x35\xcf\x38\x12\x8d\x81\xcd\x8b\x3f\xcd\x59\x55\x21\xba\x96\xd5\x2d\xe1\x23\x42\xf2\x8c\x2a\x84\x29\x88\x8c\xa2\x2a\x64\x48\x8d\xd7\x01\x2d\x64\xd6\x25\xfc\x59\x33\xda\x67\x1d\xbd\xb4\x26\x18\xb3\xff\xaa\xfa\x28\xe8\x9c\x61\x77\xd3\x34\xa6\xdd\xec\x2d\xb5\x08\x33\x09\x48\xe4\x87\x0b\xe9\x59\x85\x30\x8d\x91\x1d\x50\x25\x4e\x9c\xe1\xce\x5e\x7e\x39\x43\x00\x7a\xcc\xc6\x58\xb2\x58\x0c\x40\x8f\x58\x30\x5a\x0d\xde\x10\x13\x60\xc6\x20\xdf\xd0\x3f\x73\x26\xc1\x16\x8c\xc5\xa0\x3e\x34\xb2\x6a\xc8\x64\xa0\x78\x18\x18\xbf\x49\x52\x5a\x57\x3b\x10\x4d\x0d\x6b\x20\xf7\x4c\x54\xa8\xd9\xec\xb0\xb6\x36\x6c\x48\xda\x61\x79\xba\x00\x75\x1e\x9a\xb4\x8e\x48\x46\x30\x7d\x5c\xdf\xc4\xe1\x59\x09\x94\x1d\x98\x54\xf1\x31\x5c\x23\x3f\x00\x22\xea\x90\x1f\x20\x7f\x9c\x21\xd7\x51\x06\x35\x93\x2a\xc6\xc8\x71\x85\xca\x30\x88\xe7\x21\x08\x41\x3b\x20\x17\xf1\xb9\xaa\xf0\xb5\x69\x59\x59\x36\x50\x9f\xc5\x4d\x2a\x97\x7e\x38\x94\xf3\x0b\x81\x8f\x20\x62\x13\x38\xe3\xe7\x82\xcb\x1e\x0c\x17\x20\x49\xb8\xfa\x34\xa0\x5f\xee\xba\xe2\x73\xc6\xab\xda\xdf\x08\x49\xb7\x76\xb9\x90\x58\x79\xdf\xf5\xc5\xe2\x30\xae\xa0\x30\xb4\x52\xa1\xbc\xa9\x1b\x04\x48\x8f\x5e\xcf\xd0\xbe\xbb\xc9\x2a\x56\xf8\x0c\x74\x02\xb6\x65\xe3\x8d\xd4\x8b\x13\x61\x72\x51\xfd\x18\xa5\xba\x60\x03\x11\xe0\xc6\xb7\xbd\xd8\x6d\x9e\xb7\x1b\x36\xb1\x16\x87\x08\x46\x12\xc2\xce\xee\x15\xa4\x31\x1b\xe6\xc7\x4f\x91\x36\xe1\xa2\xfd\xcf\x2c\xad\x87\xd4\x3b\x67\x7c\x8d\x1c\x98\xea\xbc\x95\xd6\xdd\x5c\x1b\xd9\x06\xbc\xed\x95\x4b\x78\x8e\x0b\x7f\xac\x68\x23\x84\xee\x60\x9c\x09\x35\xf1\xae\xe5\xe9\xde\x67\xc1\xba\xb8\x86\x38\x75\x4e\xf8\xdd\xe2\x13\x69\x4c\xd4\x1d\x45\x34\xf5\xbf\xa0\x7f\x84\x3d\x23\x9d\x89\x52\x0e\xb4\x42\xa2\x04\xb3\x0d\xc1\x54\x41\x09\xc2\x43\xc0\xeb\x1d\xc1\xf2\x00\xc5\x12\x1a\xc1\x14\xcb\x19\x89\x73\x0c\x67\xfb\x19\xef\x0c\xe6\x84\xdb\xab\x6b\x33\x2e\xf0\x11\x13\x28\x2d\x8e\x77\x8c\x11\x40\xd4\x48\x14\x02\x50\x91\x31\x4a\x4e\x11\x48\xa9\x90\x08\xb6\x7f\x12\xf2\x5a\x60\x75\xca\x18\x57\xab\x57\x85\xf2\x50\x65\x12\x7f\x07\xd3\xf7\xce\x56\xdf\x4f\xb4\xb5\x36\x64\x9d\x67\x25\xaf\xe5\x7e\x3e\xb3\x7d\x25\xb7\x91\xac\x16\xf9\x75\x8e\x33\x8b\xaf\xcd\x20\x37\x0f\x2e\x97\x80\x27\x0e\xdf\xab\x30\x54\x43\xcd\xba\x8a\x33\x50\xcb\x93\xcc\xd5\x65\xb5\xb5\x54\x05\xa6\x19\xe3\x40\x83\xbe\x21\x15\xe3\x59\x29\x50\x0e\x19\x07\x81\x99\x53\x14\x46\x80\x2d\x6a\x81\x9a\xf5\xa7\xd3\x48\x5c\x52\xe4\x8e\x3b\x1a\x54\x55\x7c\x7f\xe1\x21\x80\x52\x61\x67\xaf\x09\xae\xb0\xdf\x69\x1c\x56\x1b\x51\xaf\x75\xb5\x9a\xbb\x44\x9b\x29\xcf\xa2\x42\xf6\x4c\x87\x30\xdf\x20\x44\x74\x06\x07\x24\x16\xa4\x8e\xd6\x31\xf7\x9e\xfc\xe4\xea\x1b\x9c\xfb\x32\x6e\xa6\xda\xf9\x6e\xfa\x8d\x6c\x9d\xf8\x45\xa5\x97\xbd\x8d\xad\xb7\xfa\x71\x3b\x55\x2d\x83\x4d\x5c\x8b\xa1\x72\xae\x01\x19\xa1\xd3\x2b\x96\xe4\x2f\x11\xa1\x0d\x1d\xb5\x70\x87\x6e\x22\xe2\x78\xbf\x52\x64\xec\x7c\xed\xa8\x1f\x5d\x11\x68\x34\x3b\x3c\x39\xf0\x5d\x22\xc9\x38\x39\x8d\x28\x54\x76\xa1\x33\xba\x67\x89\x77\xbb\xfe\x22\xed\xa7\xb0\x42\x59\xce\xb8\x47\xca\xf1\x6c\x2c\xcd\x98\xd6\x29\xc4\x4c\x49\xe9\xf3\xfe\x27\x26\x1e\x9b\xdc\x52\x60\x77\x10\xd8\x58\x24\x0b\xee\x1e\xad\x63\xbb\x61\x02\xd7\xa5\x9a\x0e\x0d\x5e\x42\xce\x5f\xf0\xf5\x20\xef\xe5\x1b\x96\x68\x67\x5d\x3b\xb9\x72\x66\x13\xe4\xc5\x31\x9c\xba\x05\x28\x81\xad\x5b\x81\xa1\xfe\xd1\xd3\x34\xc8\x5f\xf3\xec\x5c\xe1\x0a\x58\xed\x8e\x28\x1b\xdd\x70\x7a\xa2\x54\xbb\x9c\x0c\x28\x55\x43\xda\x3a\x7d\x18\x95\x3a\xb4\xd8\x41\xc5\xc5\xe4\x1e\x01\x9c\xe0\x1c\xc9\x50\x7e\xbf\xe2\x8c\xb6\xe6\x05\x52\x90\x75\x8f\x53\x16\x55\x54\x33\xa5\x14\x47\x02\x11\x02\x04\xcb\x2a\xa6\x34\x49\x0b\x20\xe8\x74\x51\x55\xda\x92\xef\x11\x26\xb5\x80\x0c\xe5\xde\xc8\x6b\x51\x54\x8c\x62\xc5\x9c\x11\x22\x6e\xc9\x0a\x3d\x67\xc3\xb2\x2d\x24\xd4\x30\x98\xbd\x72\xec\xf1\xaa\x66\x09\x5d\x66\x5d\x56\xf4\xce\xa8\xe8\x5c\x42\x7b\x2c\x66\x58\x71\xc2\xba\x00\xd9\x44\x92\xf1\xf4\x3b\x48\x1f\x8c\xd9\x7d\xf3\x9e\x71\x46\x70\x7e\x5a\x8b\xc3\x9c\xd1\x4e\xc8\x31\x06\x71\xa5\x05\x36\xe6\xd0\x74\x18\x15\x57\x41\x67\x6d\x09\x9e\x30\x2d\xd8\xd3\x82\x05\xd7\x33\x25\x4e\x50\x0e\x56\xbc\xbb\x56\xd0\x52\x09\x84\xa9\x5a\x7c\xd9\x73\x2d\x5b\x57\x64\xf3\xd1\x3e\x03\x51\x7f\xc4\x05\xf3\xb8\x2f\xd2\xe7\xbc\x0e\x5e\x89\x54\x50\x31\xe1\x34\xc0\x15\x5e\xbb\x85\x58\x1c\x60\x2b\x64\xb5\xa8\x3b\xb4\x1e\x95\x31\xbe\x7e\x13\x1f\xbe\x27\xdb\x86\x03\x12\xe6\xa8\x5a\xcb\x3b\xa2\x6f\x15\x53\x67\x0e\x4e\xe6\x9b\xcd\xc4\xdf\x70\x86\x76\x1d\xde\x7b\x8f\x90\xf5\x8e\x7a\x7a\xb4\x69\x7d\xbf\xe6\x59\xf3\x8a\x41\x6f\x78\x10\xe0\xd1\xea\xc3\x58\x33\xdf\x8c\xb2\xda\x46\xab\xd8\x7b\x1b\xbf\xde\xfe\xdb\xf2\xdd\x3e\x79\x73\xd5\xf9\x48\x29\x94\x1f\xa2\x5a\x82\x85\x45\xe3\x15\x71\x68\xd2\xb8\x3a\xc3\x50\x8f\xfa\x27\x0a\xfd\x4d\x6c\xf6\xe7\xd9\x57\xff\xe0\x36\xf8\xd2\xb5\x45\x5d\x9c\xc7\x23\x9e\x77\xfe\x02\x3a\x7b\x6b\x55\x98\x47\xfb\x9a\x4a\xa6\xc7\x03\x73\x92\x8c\x7e\x7f\xd0\x53\x6c\xcd\x6d\xd8\x30\xc7\xff\x44\x98\xc9\x74\xee\xe2\x6f\x80\x78\x8e\xa3\xac\x45\x7b\x21\xce\x73\xbe\x62\xb0\xb9\xfb\x30\x53\x32\xcc\xbd\x13\x7a\xa5\x5c\xbb\xc2\xa5\xaa\x5b\xa7\x56\x9f\x31\x48\x77\xfa\xce\xdd\xe3\xff\x1a\xfd\xe4\xd5\x7b\xc3\x27\x3d\x4d\x8e\xaf\x7e\x98\xc7\xe8\xdd\x8b\xf5\xad\x21\x1f\x0b\xd2\xbd\xba\xd3\xa2\xfb\x56\x6f\xbd\x7c\x6a\x74\xbe\x85\xb7\x0f\xf1\x87\x37\xe9\x9e\x7b\xc5\x8d\xfe\xb3\xfd\xff\x81\xcd\xcb\xe6\xcf\x00\x00\x00\xff\xff\xea\x87\x24\xae\xb8\x34\x00\x00") func dataConfig_schema_v31JsonBytes() ([]byte, error) { return bindataRead( diff --git a/components/cli/compose/schema/data/config_schema_v3.1.json b/components/cli/compose/schema/data/config_schema_v3.1.json index b9d4221995..c5e48968e3 100644 --- a/components/cli/compose/schema/data/config_schema_v3.1.json +++ b/components/cli/compose/schema/data/config_schema_v3.1.json @@ -235,7 +235,37 @@ }, "user": {"type": "string"}, "userns_mode": {"type": "string"}, - "volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "volumes": { + "type": "array", + "items": { + "oneOf": [ + {"type": "string"}, + { + "type": "object", + "required": ["type"], + "properties": { + "type": {"type": "string"}, + "source": {"type": "string"}, + "target": {"type": "string"}, + "read_only": {"type": "boolean"}, + "bind": { + "type": "object", + "properties": { + "propagation": {"type": "string"} + } + }, + "volume": { + "type": "object", + "properties": { + "nocopy": {"type": "boolean"} + } + } + } + } + ], + "uniqueItems": true + } + }, "working_dir": {"type": "string"} }, "additionalProperties": false diff --git a/components/cli/compose/schema/schema.go b/components/cli/compose/schema/schema.go index ae33c77fbe..063956e3a1 100644 --- a/components/cli/compose/schema/schema.go +++ b/components/cli/compose/schema/schema.go @@ -78,18 +78,22 @@ func Validate(config map[string]interface{}, version string) error { func toError(result *gojsonschema.Result) error { err := getMostSpecificError(result.Errors()) - description := getDescription(err) - return fmt.Errorf("%s %s", err.Field(), description) + return err } -func getDescription(err gojsonschema.ResultError) string { - if err.Type() == "invalid_type" { - if expectedType, ok := err.Details()["expected"].(string); ok { +func getDescription(err validationError) string { + switch err.parent.Type() { + case "invalid_type": + if expectedType, ok := err.parent.Details()["expected"].(string); ok { return fmt.Sprintf("must be a %s", humanReadableType(expectedType)) } + case "number_one_of", "number_any_of": + if err.child == nil { + return err.parent.Description() + } + return err.child.Description() } - - return err.Description() + return err.parent.Description() } func humanReadableType(definition string) string { @@ -113,23 +117,45 @@ func humanReadableType(definition string) string { return definition } -func getMostSpecificError(errors []gojsonschema.ResultError) gojsonschema.ResultError { - var mostSpecificError gojsonschema.ResultError +type validationError struct { + parent gojsonschema.ResultError + child gojsonschema.ResultError +} - for _, err := range errors { - if mostSpecificError == nil { - mostSpecificError = err - } else if specificity(err) > specificity(mostSpecificError) { - mostSpecificError = err - } else if specificity(err) == specificity(mostSpecificError) { +func (err validationError) Error() string { + description := getDescription(err) + return fmt.Sprintf("%s %s", err.parent.Field(), description) +} + +func getMostSpecificError(errors []gojsonschema.ResultError) validationError { + mostSpecificError := 0 + for i, err := range errors { + if specificity(err) > specificity(errors[mostSpecificError]) { + mostSpecificError = i + continue + } + + if specificity(err) == specificity(errors[mostSpecificError]) { // Invalid type errors win in a tie-breaker for most specific field name - if err.Type() == "invalid_type" && mostSpecificError.Type() != "invalid_type" { - mostSpecificError = err + if err.Type() == "invalid_type" && errors[mostSpecificError].Type() != "invalid_type" { + mostSpecificError = i } } } - return mostSpecificError + if mostSpecificError+1 == len(errors) { + return validationError{parent: errors[mostSpecificError]} + } + + switch errors[mostSpecificError].Type() { + case "number_one_of", "number_any_of": + return validationError{ + parent: errors[mostSpecificError], + child: errors[mostSpecificError+1], + } + default: + return validationError{parent: errors[mostSpecificError]} + } } func specificity(err gojsonschema.ResultError) int { diff --git a/components/cli/compose/types/types.go b/components/cli/compose/types/types.go index ba11faa138..d5454ec2f6 100644 --- a/components/cli/compose/types/types.go +++ b/components/cli/compose/types/types.go @@ -119,7 +119,7 @@ type ServiceConfig struct { Tty bool `mapstructure:"tty"` Ulimits map[string]*UlimitsConfig User string - Volumes []string + Volumes []ServiceVolumeConfig WorkingDir string `mapstructure:"working_dir"` } @@ -223,6 +223,26 @@ type ServicePortConfig struct { Protocol string } +// ServiceVolumeConfig are references to a volume used by a service +type ServiceVolumeConfig struct { + Type string + Source string + Target string + ReadOnly string `mapstructure:"read_only"` + Bind *ServiceVolumeBind + Volume *ServiceVolumeVolume +} + +// ServiceVolumeBind are options for a service volume of type bind +type ServiceVolumeBind struct { + Propogation string +} + +// ServiceVolumeVolume are options for a service volume of type volume +type ServiceVolumeVolume struct { + NoCopy bool `mapstructure:"nocopy"` +} + // ServiceSecretConfig is the secret configuration for a service type ServiceSecretConfig struct { Source string From 48d10d2797dda4683ece3b398fd7a93e2d361acc Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 24 Jan 2017 16:53:36 -0500 Subject: [PATCH 635/978] Parse a volume spec on the client, with support for windows drives Signed-off-by: Daniel Nephin Upstream-commit: a442213b9229fac39d096133c5e8a2da1102ea3c Component: cli --- components/cli/compose/loader/volume.go | 119 ++++++++++++++++ components/cli/compose/loader/volume_test.go | 134 +++++++++++++++++++ components/cli/compose/types/types.go | 2 +- 3 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 components/cli/compose/loader/volume.go create mode 100644 components/cli/compose/loader/volume_test.go diff --git a/components/cli/compose/loader/volume.go b/components/cli/compose/loader/volume.go new file mode 100644 index 0000000000..3f33492ea7 --- /dev/null +++ b/components/cli/compose/loader/volume.go @@ -0,0 +1,119 @@ +package loader + +import ( + "strings" + "unicode" + "unicode/utf8" + + "github.com/docker/docker/api/types/mount" + "github.com/docker/docker/cli/compose/types" + "github.com/pkg/errors" +) + +func parseVolume(spec string) (types.ServiceVolumeConfig, error) { + volume := types.ServiceVolumeConfig{} + + switch len(spec) { + case 0: + return volume, errors.New("invalid empty volume spec") + case 1, 2: + volume.Target = spec + volume.Type = string(mount.TypeVolume) + return volume, nil + } + + buffer := []rune{} + for _, char := range spec { + switch { + case isWindowsDrive(char, buffer, volume): + buffer = append(buffer, char) + case char == ':': + if err := populateFieldFromBuffer(char, buffer, &volume); err != nil { + return volume, errors.Wrapf(err, "invalid spec: %s", spec) + } + buffer = []rune{} + default: + buffer = append(buffer, char) + } + } + + if err := populateFieldFromBuffer(rune(0), buffer, &volume); err != nil { + return volume, errors.Wrapf(err, "invalid spec: %s", spec) + } + populateType(&volume) + return volume, nil +} + +func isWindowsDrive(char rune, buffer []rune, volume types.ServiceVolumeConfig) bool { + return char == ':' && len(buffer) == 1 && unicode.IsLetter(buffer[0]) +} + +func populateFieldFromBuffer(char rune, buffer []rune, volume *types.ServiceVolumeConfig) error { + strBuffer := string(buffer) + switch { + case len(buffer) == 0: + return errors.New("empty section between colons") + // Anonymous volume + case volume.Source == "" && char == rune(0): + volume.Target = strBuffer + return nil + case volume.Source == "": + volume.Source = strBuffer + return nil + case volume.Target == "": + volume.Target = strBuffer + return nil + case char == ':': + return errors.New("too many colons") + } + for _, option := range strings.Split(strBuffer, ",") { + switch option { + case "ro": + volume.ReadOnly = true + case "nocopy": + volume.Volume = &types.ServiceVolumeVolume{NoCopy: true} + default: + if isBindOption(option) { + volume.Bind = &types.ServiceVolumeBind{Propagation: option} + } else { + return errors.Errorf("unknown option: %s", option) + } + } + } + return nil +} + +func isBindOption(option string) bool { + for _, propagation := range mount.Propagations { + if mount.Propagation(option) == propagation { + return true + } + } + return false +} + +func populateType(volume *types.ServiceVolumeConfig) { + switch { + // Anonymous volume + case volume.Source == "": + volume.Type = string(mount.TypeVolume) + case isFilePath(volume.Source): + volume.Type = string(mount.TypeBind) + default: + volume.Type = string(mount.TypeVolume) + } +} + +func isFilePath(source string) bool { + switch source[0] { + case '.', '/', '~': + return true + } + + // Windows absolute path + first, next := utf8.DecodeRuneInString(source) + if unicode.IsLetter(first) && source[next] == ':' { + return true + } + return false +} diff --git a/components/cli/compose/loader/volume_test.go b/components/cli/compose/loader/volume_test.go new file mode 100644 index 0000000000..0735d5a54a --- /dev/null +++ b/components/cli/compose/loader/volume_test.go @@ -0,0 +1,134 @@ +package loader + +import ( + "testing" + + "github.com/docker/docker/cli/compose/types" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestParseVolumeAnonymousVolume(t *testing.T) { + for _, path := range []string{"/path", "/path/foo"} { + volume, err := parseVolume(path) + expected := types.ServiceVolumeConfig{Type: "volume", Target: path} + assert.NilError(t, err) + assert.DeepEqual(t, volume, expected) + } +} + +func TestParseVolumeAnonymousVolumeWindows(t *testing.T) { + for _, path := range []string{"C:\\path", "Z:\\path\\foo"} { + volume, err := parseVolume(path) + expected := types.ServiceVolumeConfig{Type: "volume", Target: path} + assert.NilError(t, err) + assert.DeepEqual(t, volume, expected) + } +} + +func TestParseVolumeTooManyColons(t *testing.T) { + _, err := parseVolume("/foo:/foo:ro:foo") + assert.Error(t, err, "too many colons") +} + +func TestParseVolumeShortVolumes(t *testing.T) { + for _, path := range []string{".", "/a"} { + volume, err := parseVolume(path) + expected := types.ServiceVolumeConfig{Type: "volume", Target: path} + assert.NilError(t, err) + assert.DeepEqual(t, volume, expected) + } +} + +func TestParseVolumeMissingSource(t *testing.T) { + for _, spec := range []string{":foo", "/foo::ro"} { + _, err := parseVolume(spec) + assert.Error(t, err, "empty section between colons") + } +} + +func TestParseVolumeBindMount(t *testing.T) { + for _, path := range []string{"./foo", "~/thing", "../other", "/foo", "/home/user"} { + volume, err := parseVolume(path + ":/target") + expected := types.ServiceVolumeConfig{ + Type: "bind", + Source: path, + Target: "/target", + } + assert.NilError(t, err) + assert.DeepEqual(t, volume, expected) + } +} + +func TestParseVolumeRelativeBindMountWindows(t *testing.T) { + for _, path := range []string{ + "./foo", + "~/thing", + "../other", + "D:\\path", "/home/user", + } { + volume, err := parseVolume(path + ":d:\\target") + expected := types.ServiceVolumeConfig{ + Type: "bind", + Source: path, + Target: "d:\\target", + } + assert.NilError(t, err) + assert.DeepEqual(t, volume, expected) + } +} + +func TestParseVolumeWithBindOptions(t *testing.T) { + volume, err := parseVolume("/source:/target:slave") + expected := types.ServiceVolumeConfig{ + Type: "bind", + Source: "/source", + Target: "/target", + Bind: &types.ServiceVolumeBind{Propagation: "slave"}, + } + assert.NilError(t, err) + assert.DeepEqual(t, volume, expected) +} + +func TestParseVolumeWithBindOptionsWindows(t *testing.T) { + volume, err := parseVolume("C:\\source\\foo:D:\\target:ro,rprivate") + expected := types.ServiceVolumeConfig{ + Type: "bind", + Source: "C:\\source\\foo", + Target: "D:\\target", + ReadOnly: true, + Bind: &types.ServiceVolumeBind{Propagation: "rprivate"}, + } + assert.NilError(t, err) + assert.DeepEqual(t, volume, expected) +} + +func TestParseVolumeWithInvalidVolumeOptions(t *testing.T) { + _, err := parseVolume("name:/target:bogus") + assert.Error(t, err, "invalid spec: name:/target:bogus: unknown option: bogus") +} + +func TestParseVolumeWithVolumeOptions(t *testing.T) { + volume, err := parseVolume("name:/target:nocopy") + expected := types.ServiceVolumeConfig{ + Type: "volume", + Source: "name", + Target: "/target", + Volume: &types.ServiceVolumeVolume{NoCopy: true}, + } + assert.NilError(t, err) + assert.DeepEqual(t, volume, expected) +} + +func TestParseVolumeWithReadOnly(t *testing.T) { + for _, path := range []string{"./foo", "/home/user"} { + volume, err := parseVolume(path + ":/target:ro") + expected := types.ServiceVolumeConfig{ + Type: "bind", + Source: path, + Target: "/target", + ReadOnly: true, + } + assert.NilError(t, err) + assert.DeepEqual(t, volume, expected) + } +} diff --git a/components/cli/compose/types/types.go b/components/cli/compose/types/types.go index d5454ec2f6..307b5fd907 100644 --- a/components/cli/compose/types/types.go +++ b/components/cli/compose/types/types.go @@ -235,7 +235,7 @@ type ServiceVolumeConfig struct { // ServiceVolumeBind are options for a service volume of type bind type ServiceVolumeBind struct { - Propogation string + Propagation string } // ServiceVolumeVolume are options for a service volume of type volume From 53c21bb298c26cbb87d51710f0d193ce290052ef Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 24 Jan 2017 12:09:53 -0500 Subject: [PATCH 636/978] Support expanded mounts in Compose loader Add a test for loading expanded mount format. Signed-off-by: Daniel Nephin Upstream-commit: 29f39ea24432d63c413cda75214db7eef8e409eb Component: cli --- components/cli/compose/loader/loader.go | 38 ++++++++++-------- components/cli/compose/loader/loader_test.go | 42 ++++++++++++++++---- components/cli/compose/types/types.go | 2 +- 3 files changed, 58 insertions(+), 24 deletions(-) diff --git a/components/cli/compose/loader/loader.go b/components/cli/compose/loader/loader.go index 2ccef7198d..bdc837d9b6 100644 --- a/components/cli/compose/loader/loader.go +++ b/components/cli/compose/loader/loader.go @@ -251,6 +251,8 @@ func transformHook( return transformMappingOrList(data, "="), nil case reflect.TypeOf(types.MappingWithColon{}): return transformMappingOrList(data, ":"), nil + case reflect.TypeOf(types.ServiceVolumeConfig{}): + return transformServiceVolumeConfig(data) } return data, nil } @@ -329,10 +331,7 @@ func loadService(name string, serviceDict types.Dict, workingDir string) (*types return nil, err } - if err := resolveVolumePaths(serviceConfig.Volumes, workingDir); err != nil { - return nil, err - } - + resolveVolumePaths(serviceConfig.Volumes, workingDir) return serviceConfig, nil } @@ -365,22 +364,15 @@ func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string) e return nil } -func resolveVolumePaths(volumes []string, workingDir string) error { - for i, mapping := range volumes { - parts := strings.SplitN(mapping, ":", 2) - if len(parts) == 1 { +func resolveVolumePaths(volumes []types.ServiceVolumeConfig, workingDir string) { + for i, volume := range volumes { + if volume.Type != "bind" { continue } - if strings.HasPrefix(parts[0], ".") { - parts[0] = absPath(workingDir, parts[0]) - } - parts[0] = expandUser(parts[0]) - - volumes[i] = strings.Join(parts, ":") + volume.Source = absPath(workingDir, expandUser(volume.Source)) + volumes[i] = volume } - - return nil } // TODO: make this more robust @@ -533,6 +525,20 @@ func transformServiceSecret(data interface{}) (interface{}, error) { } } +func transformServiceVolumeConfig(data interface{}) (interface{}, error) { + switch value := data.(type) { + case string: + return parseVolume(value) + case types.Dict: + return data, nil + case map[string]interface{}: + return data, nil + default: + return data, fmt.Errorf("invalid type %T for service volume", value) + } + +} + func transformServiceNetworkMap(value interface{}) (interface{}, error) { if list, ok := value.([]interface{}); ok { mapValue := map[interface{}]interface{}{} diff --git a/components/cli/compose/loader/loader_test.go b/components/cli/compose/loader/loader_test.go index 53f4280b64..126832a3b2 100644 --- a/components/cli/compose/loader/loader_test.go +++ b/components/cli/compose/loader/loader_test.go @@ -837,13 +837,13 @@ func TestFullExample(t *testing.T) { }, }, User: "someone", - Volumes: []string{ - "/var/lib/mysql", - "/opt/data:/var/lib/mysql", - fmt.Sprintf("%s:/code", workingDir), - fmt.Sprintf("%s/static:/var/www/html", workingDir), - fmt.Sprintf("%s/configs:/etc/configs/:ro", homeDir), - "datavolume:/var/lib/mysql", + Volumes: []types.ServiceVolumeConfig{ + {Target: "/var/lib/mysql", Type: "volume"}, + {Source: "/opt/data", Target: "/var/lib/mysql", Type: "bind"}, + {Source: workingDir, Target: "/code", Type: "bind"}, + {Source: workingDir + "/static", Target: "/var/www/html", Type: "bind"}, + {Source: homeDir + "/configs", Target: "/etc/configs/", Type: "bind", ReadOnly: true}, + {Source: "datavolume", Target: "/var/lib/mysql", Type: "volume"}, }, WorkingDir: "/code", } @@ -1041,3 +1041,31 @@ services: assert.Equal(t, 1, len(config.Services)) assert.Equal(t, expected, config.Services[0].Ports) } + +func TestLoadExpandedMountFormat(t *testing.T) { + config, err := loadYAML(` +version: "3.1" +services: + web: + image: busybox + volumes: + - type: volume + source: foo + target: /target + read_only: true +volumes: + foo: {} +`) + assert.NoError(t, err) + + expected := types.ServiceVolumeConfig{ + Type: "volume", + Source: "foo", + Target: "/target", + ReadOnly: true, + } + + assert.Equal(t, 1, len(config.Services)) + assert.Equal(t, 1, len(config.Services[0].Volumes)) + assert.Equal(t, expected, config.Services[0].Volumes[0]) +} diff --git a/components/cli/compose/types/types.go b/components/cli/compose/types/types.go index 307b5fd907..dce13c928a 100644 --- a/components/cli/compose/types/types.go +++ b/components/cli/compose/types/types.go @@ -228,7 +228,7 @@ type ServiceVolumeConfig struct { Type string Source string Target string - ReadOnly string `mapstructure:"read_only"` + ReadOnly bool `mapstructure:"read_only"` Bind *ServiceVolumeBind Volume *ServiceVolumeVolume } From 19021e1a48b508484bf37ecbc8c53848cf69d0c7 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 27 Jan 2017 16:56:45 -0500 Subject: [PATCH 637/978] Convert new compose volume type to swarm mount type Signed-off-by: Daniel Nephin Upstream-commit: 63c3221dd3a3b15ec1111dec4500cad99312e8a0 Component: cli --- components/cli/compose/convert/volume.go | 145 ++++++------------ components/cli/compose/convert/volume_test.go | 111 +++++++++----- components/cli/compose/schema/schema.go | 7 +- 3 files changed, 128 insertions(+), 135 deletions(-) diff --git a/components/cli/compose/convert/volume.go b/components/cli/compose/convert/volume.go index 53c50958fa..682b44377a 100644 --- a/components/cli/compose/convert/volume.go +++ b/components/cli/compose/convert/volume.go @@ -1,21 +1,19 @@ package convert import ( - "fmt" - "strings" - "github.com/docker/docker/api/types/mount" composetypes "github.com/docker/docker/cli/compose/types" + "github.com/pkg/errors" ) type volumes map[string]composetypes.VolumeConfig // Volumes from compose-file types to engine api types -func Volumes(serviceVolumes []string, stackVolumes volumes, namespace Namespace) ([]mount.Mount, error) { +func Volumes(serviceVolumes []composetypes.ServiceVolumeConfig, stackVolumes volumes, namespace Namespace) ([]mount.Mount, error) { var mounts []mount.Mount - for _, volumeSpec := range serviceVolumes { - mount, err := convertVolumeToMount(volumeSpec, stackVolumes, namespace) + for _, volumeConfig := range serviceVolumes { + mount, err := convertVolumeToMount(volumeConfig, stackVolumes, namespace) if err != nil { return nil, err } @@ -24,108 +22,65 @@ func Volumes(serviceVolumes []string, stackVolumes volumes, namespace Namespace) return mounts, nil } -func convertVolumeToMount(volumeSpec string, stackVolumes volumes, namespace Namespace) (mount.Mount, error) { - var source, target string - var mode []string +func convertVolumeToMount( + volume composetypes.ServiceVolumeConfig, + stackVolumes volumes, + namespace Namespace, +) (mount.Mount, error) { + result := mount.Mount{ + Type: mount.Type(volume.Type), + Source: volume.Source, + Target: volume.Target, + ReadOnly: volume.ReadOnly, + } - // TODO: split Windows path mappings properly - parts := strings.SplitN(volumeSpec, ":", 3) + // Anonymous volumes + if volume.Source == "" { + return result, nil + } + if volume.Type == "volume" && volume.Bind != nil { + return result, errors.New("bind options are incompatible with type volume") + } + if volume.Type == "bind" && volume.Volume != nil { + return result, errors.New("volume options are incompatible with type bind") + } - for _, part := range parts { - if strings.TrimSpace(part) == "" { - return mount.Mount{}, fmt.Errorf("invalid volume: %s", volumeSpec) + if volume.Bind != nil { + result.BindOptions = &mount.BindOptions{ + Propagation: mount.Propagation(volume.Bind.Propagation), } } - - switch len(parts) { - case 3: - source = parts[0] - target = parts[1] - mode = strings.Split(parts[2], ",") - case 2: - source = parts[0] - target = parts[1] - case 1: - target = parts[0] + // Binds volumes + if volume.Type == "bind" { + return result, nil } - if source == "" { - // Anonymous volume - return mount.Mount{ - Type: mount.TypeVolume, - Target: target, - }, nil - } - - // TODO: catch Windows paths here - if strings.HasPrefix(source, "/") { - return mount.Mount{ - Type: mount.TypeBind, - Source: source, - Target: target, - ReadOnly: isReadOnly(mode), - BindOptions: getBindOptions(mode), - }, nil - } - - stackVolume, exists := stackVolumes[source] + stackVolume, exists := stackVolumes[volume.Source] if !exists { - return mount.Mount{}, fmt.Errorf("undefined volume: %s", source) + return result, errors.Errorf("undefined volume: %s", volume.Source) } - var volumeOptions *mount.VolumeOptions - if stackVolume.External.Name != "" { - volumeOptions = &mount.VolumeOptions{ - NoCopy: isNoCopy(mode), - } - source = stackVolume.External.Name - } else { - volumeOptions = &mount.VolumeOptions{ - Labels: AddStackLabel(namespace, stackVolume.Labels), - NoCopy: isNoCopy(mode), - } + result.Source = namespace.Scope(volume.Source) + result.VolumeOptions = &mount.VolumeOptions{} - if stackVolume.Driver != "" { - volumeOptions.DriverConfig = &mount.Driver{ - Name: stackVolume.Driver, - Options: stackVolume.DriverOpts, - } - } - source = namespace.Scope(source) + if volume.Volume != nil { + result.VolumeOptions.NoCopy = volume.Volume.NoCopy } - return mount.Mount{ - Type: mount.TypeVolume, - Source: source, - Target: target, - ReadOnly: isReadOnly(mode), - VolumeOptions: volumeOptions, - }, nil -} -func modeHas(mode []string, field string) bool { - for _, item := range mode { - if item == field { - return true + // External named volumes + if stackVolume.External.External { + result.Source = stackVolume.External.Name + return result, nil + } + + result.VolumeOptions.Labels = AddStackLabel(namespace, stackVolume.Labels) + if stackVolume.Driver != "" || stackVolume.DriverOpts != nil { + result.VolumeOptions.DriverConfig = &mount.Driver{ + Name: stackVolume.Driver, + Options: stackVolume.DriverOpts, } } - return false -} -func isReadOnly(mode []string) bool { - return modeHas(mode, "ro") -} - -func isNoCopy(mode []string) bool { - return modeHas(mode, "nocopy") -} - -func getBindOptions(mode []string) *mount.BindOptions { - for _, item := range mode { - for _, propagation := range mount.Propagations { - if mount.Propagation(item) == propagation { - return &mount.BindOptions{Propagation: mount.Propagation(item)} - } - } - } - return nil + // Named volumes + return result, nil } diff --git a/components/cli/compose/convert/volume_test.go b/components/cli/compose/convert/volume_test.go index d218e7c2f5..705f03f404 100644 --- a/components/cli/compose/convert/volume_test.go +++ b/components/cli/compose/convert/volume_test.go @@ -8,51 +8,48 @@ import ( "github.com/docker/docker/pkg/testutil/assert" ) -func TestIsReadOnly(t *testing.T) { - assert.Equal(t, isReadOnly([]string{"foo", "bar", "ro"}), true) - assert.Equal(t, isReadOnly([]string{"ro"}), true) - assert.Equal(t, isReadOnly([]string{}), false) - assert.Equal(t, isReadOnly([]string{"foo", "rw"}), false) - assert.Equal(t, isReadOnly([]string{"foo"}), false) -} - -func TestIsNoCopy(t *testing.T) { - assert.Equal(t, isNoCopy([]string{"foo", "bar", "nocopy"}), true) - assert.Equal(t, isNoCopy([]string{"nocopy"}), true) - assert.Equal(t, isNoCopy([]string{}), false) - assert.Equal(t, isNoCopy([]string{"foo", "rw"}), false) -} - -func TestGetBindOptions(t *testing.T) { - opts := getBindOptions([]string{"slave"}) - expected := mount.BindOptions{Propagation: mount.PropagationSlave} - assert.Equal(t, *opts, expected) -} - -func TestGetBindOptionsNone(t *testing.T) { - opts := getBindOptions([]string{"ro"}) - assert.Equal(t, opts, (*mount.BindOptions)(nil)) -} - func TestConvertVolumeToMountAnonymousVolume(t *testing.T) { - stackVolumes := volumes{} - namespace := NewNamespace("foo") + config := composetypes.ServiceVolumeConfig{ + Type: "volume", + Target: "/foo/bar", + } expected := mount.Mount{ Type: mount.TypeVolume, Target: "/foo/bar", } - mount, err := convertVolumeToMount("/foo/bar", stackVolumes, namespace) + mount, err := convertVolumeToMount(config, volumes{}, NewNamespace("foo")) assert.NilError(t, err) assert.DeepEqual(t, mount, expected) } -func TestConvertVolumeToMountInvalidFormat(t *testing.T) { +func TestConvertVolumeToMountConflictingOptionsBind(t *testing.T) { namespace := NewNamespace("foo") - invalids := []string{"::", "::cc", ":bb:", "aa::", "aa::cc", "aa:bb:", " : : ", " : :cc", " :bb: ", "aa: : ", "aa: :cc", "aa:bb: "} - for _, vol := range invalids { - _, err := convertVolumeToMount(vol, volumes{}, namespace) - assert.Error(t, err, "invalid volume: "+vol) + + config := composetypes.ServiceVolumeConfig{ + Type: "volume", + Source: "foo", + Target: "/target", + Bind: &composetypes.ServiceVolumeBind{ + Propagation: "slave", + }, } + _, err := convertVolumeToMount(config, volumes{}, namespace) + assert.Error(t, err, "bind options are incompatible") +} + +func TestConvertVolumeToMountConflictingOptionsVolume(t *testing.T) { + namespace := NewNamespace("foo") + + config := composetypes.ServiceVolumeConfig{ + Type: "bind", + Source: "/foo", + Target: "/target", + Volume: &composetypes.ServiceVolumeVolume{ + NoCopy: true, + }, + } + _, err := convertVolumeToMount(config, volumes{}, namespace) + assert.Error(t, err, "volume options are incompatible") } func TestConvertVolumeToMountNamedVolume(t *testing.T) { @@ -84,9 +81,19 @@ func TestConvertVolumeToMountNamedVolume(t *testing.T) { "opt": "value", }, }, + NoCopy: true, }, } - mount, err := convertVolumeToMount("normal:/foo:ro", stackVolumes, namespace) + config := composetypes.ServiceVolumeConfig{ + Type: "volume", + Source: "normal", + Target: "/foo", + ReadOnly: true, + Volume: &composetypes.ServiceVolumeVolume{ + NoCopy: true, + }, + } + mount, err := convertVolumeToMount(config, stackVolumes, namespace) assert.NilError(t, err) assert.DeepEqual(t, mount, expected) } @@ -109,7 +116,12 @@ func TestConvertVolumeToMountNamedVolumeExternal(t *testing.T) { NoCopy: false, }, } - mount, err := convertVolumeToMount("outside:/foo", stackVolumes, namespace) + config := composetypes.ServiceVolumeConfig{ + Type: "volume", + Source: "outside", + Target: "/foo", + } + mount, err := convertVolumeToMount(config, stackVolumes, namespace) assert.NilError(t, err) assert.DeepEqual(t, mount, expected) } @@ -132,7 +144,15 @@ func TestConvertVolumeToMountNamedVolumeExternalNoCopy(t *testing.T) { NoCopy: true, }, } - mount, err := convertVolumeToMount("outside:/foo:nocopy", stackVolumes, namespace) + config := composetypes.ServiceVolumeConfig{ + Type: "volume", + Source: "outside", + Target: "/foo", + Volume: &composetypes.ServiceVolumeVolume{ + NoCopy: true, + }, + } + mount, err := convertVolumeToMount(config, stackVolumes, namespace) assert.NilError(t, err) assert.DeepEqual(t, mount, expected) } @@ -147,13 +167,26 @@ func TestConvertVolumeToMountBind(t *testing.T) { ReadOnly: true, BindOptions: &mount.BindOptions{Propagation: mount.PropagationShared}, } - mount, err := convertVolumeToMount("/bar:/foo:ro,shared", stackVolumes, namespace) + config := composetypes.ServiceVolumeConfig{ + Type: "bind", + Source: "/bar", + Target: "/foo", + ReadOnly: true, + Bind: &composetypes.ServiceVolumeBind{Propagation: "shared"}, + } + mount, err := convertVolumeToMount(config, stackVolumes, namespace) assert.NilError(t, err) assert.DeepEqual(t, mount, expected) } func TestConvertVolumeToMountVolumeDoesNotExist(t *testing.T) { namespace := NewNamespace("foo") - _, err := convertVolumeToMount("unknown:/foo:ro", volumes{}, namespace) + config := composetypes.ServiceVolumeConfig{ + Type: "volume", + Source: "unknown", + Target: "/foo", + ReadOnly: true, + } + _, err := convertVolumeToMount(config, volumes{}, namespace) assert.Error(t, err, "undefined volume: unknown") } diff --git a/components/cli/compose/schema/schema.go b/components/cli/compose/schema/schema.go index 063956e3a1..9a70dc2aaa 100644 --- a/components/cli/compose/schema/schema.go +++ b/components/cli/compose/schema/schema.go @@ -81,13 +81,18 @@ func toError(result *gojsonschema.Result) error { return err } +const ( + jsonschemaOneOf = "number_one_of" + jsonschemaAnyOf = "number_any_of" +) + func getDescription(err validationError) string { switch err.parent.Type() { case "invalid_type": if expectedType, ok := err.parent.Details()["expected"].(string); ok { return fmt.Sprintf("must be a %s", humanReadableType(expectedType)) } - case "number_one_of", "number_any_of": + case jsonschemaOneOf, jsonschemaAnyOf: if err.child == nil { return err.parent.Description() } From 92345732e90dc3648aa3ab139c4eea1f035a9055 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 6 Mar 2017 15:07:20 -0500 Subject: [PATCH 638/978] Some things just need to be line wrapped. Signed-off-by: Daniel Nephin Upstream-commit: 2238492c513152f7f97c05082ca68d44b33c1e97 Component: cli --- components/cli/command/image/build.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go index 4639833a9e..9fde671418 100644 --- a/components/cli/command/image/build.go +++ b/components/cli/command/image/build.go @@ -138,7 +138,6 @@ func (out *lastProgressOutput) WriteProgress(prog progress.Progress) error { } func runBuild(dockerCli *command.DockerCli, options buildOptions) error { - var ( buildCtx io.ReadCloser err error @@ -333,7 +332,11 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { // Windows: show error message about modified file permissions if the // daemon isn't running Windows. if response.OSType != "windows" && runtime.GOOS == "windows" && !options.quiet { - fmt.Fprintln(dockerCli.Out(), `SECURITY WARNING: You are building a Docker image from Windows against a non-Windows Docker host. All files and directories added to build context will have '-rwxr-xr-x' permissions. It is recommended to double check and reset permissions for sensitive files and directories.`) + fmt.Fprintln(dockerCli.Out(), "SECURITY WARNING: You are building a Docker "+ + "image from Windows against a non-Windows Docker host. All files and "+ + "directories added to build context will have '-rwxr-xr-x' permissions. "+ + "It is recommended to double check and reset permissions for sensitive "+ + "files and directories.") } // Everything worked so if -q was provided the output from the daemon From 20f2e36290ab090037ba4d79ff6623e41e665363 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 6 Mar 2017 16:01:04 -0500 Subject: [PATCH 639/978] Use opts.MemBytes for flags. Signed-off-by: Daniel Nephin Upstream-commit: e43a97cd386bd5dab092b19ebbd037a8202d353d Component: cli --- components/cli/command/container/opts.go | 79 ++++--------------- components/cli/command/container/opts_test.go | 33 ++++---- components/cli/command/container/update.go | 62 +++------------ components/cli/command/image/build.go | 34 ++------ 4 files changed, 49 insertions(+), 159 deletions(-) diff --git a/components/cli/command/container/opts.go b/components/cli/command/container/opts.go index 0413ae5f71..72d416379d 100644 --- a/components/cli/command/container/opts.go +++ b/components/cli/command/container/opts.go @@ -18,7 +18,6 @@ import ( "github.com/docker/docker/pkg/signal" runconfigopts "github.com/docker/docker/runconfig/opts" "github.com/docker/go-connections/nat" - units "github.com/docker/go-units" "github.com/spf13/pflag" ) @@ -72,10 +71,10 @@ type containerOptions struct { containerIDFile string entrypoint string hostname string - memoryString string - memoryReservation string - memorySwap string - kernelMemory string + memory opts.MemBytes + memoryReservation opts.MemBytes + memorySwap opts.MemSwapBytes + kernelMemory opts.MemBytes user string workingDir string cpuCount int64 @@ -89,7 +88,7 @@ type containerOptions struct { cpusetCpus string cpusetMems string blkioWeight uint16 - ioMaxBandwidth string + ioMaxBandwidth opts.MemBytes ioMaxIOps uint64 swappiness int64 netMode string @@ -254,12 +253,12 @@ func addFlags(flags *pflag.FlagSet) *containerOptions { flags.Var(&copts.deviceReadIOps, "device-read-iops", "Limit read rate (IO per second) from a device") flags.Var(&copts.deviceWriteBps, "device-write-bps", "Limit write rate (bytes per second) to a device") flags.Var(&copts.deviceWriteIOps, "device-write-iops", "Limit write rate (IO per second) to a device") - flags.StringVar(&copts.ioMaxBandwidth, "io-maxbandwidth", "", "Maximum IO bandwidth limit for the system drive (Windows only)") + flags.Var(&copts.ioMaxBandwidth, "io-maxbandwidth", "Maximum IO bandwidth limit for the system drive (Windows only)") flags.Uint64Var(&copts.ioMaxIOps, "io-maxiops", 0, "Maximum IOps limit for the system drive (Windows only)") - flags.StringVar(&copts.kernelMemory, "kernel-memory", "", "Kernel memory limit") - flags.StringVarP(&copts.memoryString, "memory", "m", "", "Memory limit") - flags.StringVar(&copts.memoryReservation, "memory-reservation", "", "Memory soft limit") - flags.StringVar(&copts.memorySwap, "memory-swap", "", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap") + flags.Var(&copts.kernelMemory, "kernel-memory", "Kernel memory limit") + flags.VarP(&copts.memory, "memory", "m", "Memory limit") + flags.Var(&copts.memoryReservation, "memory-reservation", "Memory soft limit") + flags.Var(&copts.memorySwap, "memory-swap", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap") flags.Int64Var(&copts.swappiness, "memory-swappiness", -1, "Tune container memory swappiness (0 to 100)") flags.BoolVar(&copts.oomKillDisable, "oom-kill-disable", false, "Disable OOM Killer") flags.IntVar(&copts.oomScoreAdj, "oom-score-adj", 0, "Tune host's OOM preferences (-1000 to 1000)") @@ -308,59 +307,11 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*container.Config, *c var err error - var memory int64 - if copts.memoryString != "" { - memory, err = units.RAMInBytes(copts.memoryString) - if err != nil { - return nil, nil, nil, err - } - } - - var memoryReservation int64 - if copts.memoryReservation != "" { - memoryReservation, err = units.RAMInBytes(copts.memoryReservation) - if err != nil { - return nil, nil, nil, err - } - } - - var memorySwap int64 - if copts.memorySwap != "" { - if copts.memorySwap == "-1" { - memorySwap = -1 - } else { - memorySwap, err = units.RAMInBytes(copts.memorySwap) - if err != nil { - return nil, nil, nil, err - } - } - } - - var kernelMemory int64 - if copts.kernelMemory != "" { - kernelMemory, err = units.RAMInBytes(copts.kernelMemory) - if err != nil { - return nil, nil, nil, err - } - } - swappiness := copts.swappiness if swappiness != -1 && (swappiness < 0 || swappiness > 100) { return nil, nil, nil, fmt.Errorf("invalid value: %d. Valid memory swappiness range is 0-100", swappiness) } - // TODO FIXME units.RAMInBytes should have a uint64 version - var maxIOBandwidth int64 - if copts.ioMaxBandwidth != "" { - maxIOBandwidth, err = units.RAMInBytes(copts.ioMaxBandwidth) - if err != nil { - return nil, nil, nil, err - } - if maxIOBandwidth < 0 { - return nil, nil, nil, fmt.Errorf("invalid value: %s. Maximum IO Bandwidth must be positive", copts.ioMaxBandwidth) - } - } - var binds []string volumes := copts.volumes.GetMap() // add any bind targets to the list of container volumes @@ -530,11 +481,11 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*container.Config, *c resources := container.Resources{ CgroupParent: copts.cgroupParent, - Memory: memory, - MemoryReservation: memoryReservation, - MemorySwap: memorySwap, + Memory: copts.memory.Value(), + MemoryReservation: copts.memoryReservation.Value(), + MemorySwap: copts.memorySwap.Value(), MemorySwappiness: &copts.swappiness, - KernelMemory: kernelMemory, + KernelMemory: copts.kernelMemory.Value(), OomKillDisable: &copts.oomKillDisable, NanoCPUs: copts.cpus.Value(), CPUCount: copts.cpuCount, @@ -554,7 +505,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*container.Config, *c BlkioDeviceReadIOps: copts.deviceReadIOps.GetList(), BlkioDeviceWriteIOps: copts.deviceWriteIOps.GetList(), IOMaximumIOps: copts.ioMaxIOps, - IOMaximumBandwidth: uint64(maxIOBandwidth), + IOMaximumBandwidth: uint64(copts.ioMaxBandwidth), Ulimits: copts.ulimits.GetList(), DeviceCgroupRules: copts.deviceCgroupRules.GetAll(), Devices: deviceMappings, diff --git a/components/cli/command/container/opts_test.go b/components/cli/command/container/opts_test.go index 6ba83c29d1..1448dae8db 100644 --- a/components/cli/command/container/opts_test.go +++ b/components/cli/command/container/opts_test.go @@ -13,6 +13,7 @@ import ( "github.com/docker/docker/api/types/container" networktypes "github.com/docker/docker/api/types/network" + "github.com/docker/docker/pkg/testutil/assert" "github.com/docker/docker/runconfig" "github.com/docker/go-connections/nat" "github.com/spf13/pflag" @@ -235,28 +236,24 @@ func TestParseWithMacAddress(t *testing.T) { func TestParseWithMemory(t *testing.T) { invalidMemory := "--memory=invalid" - validMemory := "--memory=1G" - if _, _, _, err := parseRun([]string{invalidMemory, "img", "cmd"}); err != nil && err.Error() != "invalid size: 'invalid'" { - t.Fatalf("Expected an error with '%v' Memory, got '%v'", invalidMemory, err) - } - if _, hostconfig := mustParse(t, validMemory); hostconfig.Memory != 1073741824 { - t.Fatalf("Expected the config to have '1G' as Memory, got '%v'", hostconfig.Memory) - } + _, _, _, err := parseRun([]string{invalidMemory, "img", "cmd"}) + assert.Error(t, err, invalidMemory) + + _, hostconfig := mustParse(t, "--memory=1G") + assert.Equal(t, hostconfig.Memory, int64(1073741824)) } func TestParseWithMemorySwap(t *testing.T) { invalidMemory := "--memory-swap=invalid" - validMemory := "--memory-swap=1G" - anotherValidMemory := "--memory-swap=-1" - if _, _, _, err := parseRun([]string{invalidMemory, "img", "cmd"}); err == nil || err.Error() != "invalid size: 'invalid'" { - t.Fatalf("Expected an error with '%v' MemorySwap, got '%v'", invalidMemory, err) - } - if _, hostconfig := mustParse(t, validMemory); hostconfig.MemorySwap != 1073741824 { - t.Fatalf("Expected the config to have '1073741824' as MemorySwap, got '%v'", hostconfig.MemorySwap) - } - if _, hostconfig := mustParse(t, anotherValidMemory); hostconfig.MemorySwap != -1 { - t.Fatalf("Expected the config to have '-1' as MemorySwap, got '%v'", hostconfig.MemorySwap) - } + + _, _, _, err := parseRun([]string{invalidMemory, "img", "cmd"}) + assert.Error(t, err, invalidMemory) + + _, hostconfig := mustParse(t, "--memory-swap=1G") + assert.Equal(t, hostconfig.MemorySwap, int64(1073741824)) + + _, hostconfig = mustParse(t, "--memory-swap=-1") + assert.Equal(t, hostconfig.MemorySwap, int64(-1)) } func TestParseHostname(t *testing.T) { diff --git a/components/cli/command/container/update.go b/components/cli/command/container/update.go index 4a1220a262..b2a44975b3 100644 --- a/components/cli/command/container/update.go +++ b/components/cli/command/container/update.go @@ -8,8 +8,8 @@ import ( containertypes "github.com/docker/docker/api/types/container" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/opts" runconfigopts "github.com/docker/docker/runconfig/opts" - "github.com/docker/go-units" "github.com/spf13/cobra" "golang.org/x/net/context" ) @@ -23,10 +23,10 @@ type updateOptions struct { cpusetCpus string cpusetMems string cpuShares int64 - memoryString string - memoryReservation string - memorySwap string - kernelMemory string + memory opts.MemBytes + memoryReservation opts.MemBytes + memorySwap opts.MemSwapBytes + kernelMemory opts.MemBytes restartPolicy string nFlag int @@ -60,10 +60,10 @@ func NewUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.StringVar(&opts.cpusetCpus, "cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)") flags.StringVar(&opts.cpusetMems, "cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)") flags.Int64VarP(&opts.cpuShares, "cpu-shares", "c", 0, "CPU shares (relative weight)") - flags.StringVarP(&opts.memoryString, "memory", "m", "", "Memory limit") - flags.StringVar(&opts.memoryReservation, "memory-reservation", "", "Memory soft limit") - flags.StringVar(&opts.memorySwap, "memory-swap", "", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap") - flags.StringVar(&opts.kernelMemory, "kernel-memory", "", "Kernel memory limit") + flags.VarP(&opts.memory, "memory", "m", "Memory limit") + flags.Var(&opts.memoryReservation, "memory-reservation", "Memory soft limit") + flags.Var(&opts.memorySwap, "memory-swap", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap") + flags.Var(&opts.kernelMemory, "kernel-memory", "Kernel memory limit") flags.StringVar(&opts.restartPolicy, "restart", "", "Restart policy to apply when a container exits") return cmd @@ -76,42 +76,6 @@ func runUpdate(dockerCli *command.DockerCli, opts *updateOptions) error { return errors.New("You must provide one or more flags when using this command.") } - var memory int64 - if opts.memoryString != "" { - memory, err = units.RAMInBytes(opts.memoryString) - if err != nil { - return err - } - } - - var memoryReservation int64 - if opts.memoryReservation != "" { - memoryReservation, err = units.RAMInBytes(opts.memoryReservation) - if err != nil { - return err - } - } - - var memorySwap int64 - if opts.memorySwap != "" { - if opts.memorySwap == "-1" { - memorySwap = -1 - } else { - memorySwap, err = units.RAMInBytes(opts.memorySwap) - if err != nil { - return err - } - } - } - - var kernelMemory int64 - if opts.kernelMemory != "" { - kernelMemory, err = units.RAMInBytes(opts.kernelMemory) - if err != nil { - return err - } - } - var restartPolicy containertypes.RestartPolicy if opts.restartPolicy != "" { restartPolicy, err = runconfigopts.ParseRestartPolicy(opts.restartPolicy) @@ -125,10 +89,10 @@ func runUpdate(dockerCli *command.DockerCli, opts *updateOptions) error { CpusetCpus: opts.cpusetCpus, CpusetMems: opts.cpusetMems, CPUShares: opts.cpuShares, - Memory: memory, - MemoryReservation: memoryReservation, - MemorySwap: memorySwap, - KernelMemory: kernelMemory, + Memory: opts.memory.Value(), + MemoryReservation: opts.memoryReservation.Value(), + MemorySwap: opts.memorySwap.Value(), + KernelMemory: opts.kernelMemory.Value(), CPUPeriod: opts.cpuPeriod, CPUQuota: opts.cpuQuota, CPURealtimePeriod: opts.cpuRealtimePeriod, diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go index 9fde671418..5f7d5d07a8 100644 --- a/components/cli/command/image/build.go +++ b/components/cli/command/image/build.go @@ -40,8 +40,8 @@ type buildOptions struct { buildArgs opts.ListOpts extraHosts opts.ListOpts ulimits *opts.UlimitOpt - memory string - memorySwap string + memory opts.MemBytes + memorySwap opts.MemSwapBytes shmSize opts.MemBytes cpuShares int64 cpuPeriod int64 @@ -89,8 +89,8 @@ func NewBuildCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Var(&options.buildArgs, "build-arg", "Set build-time variables") flags.Var(options.ulimits, "ulimit", "Ulimit options") flags.StringVarP(&options.dockerfileName, "file", "f", "", "Name of the Dockerfile (Default is 'PATH/Dockerfile')") - flags.StringVarP(&options.memory, "memory", "m", "", "Memory limit") - flags.StringVar(&options.memorySwap, "memory-swap", "", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap") + flags.VarP(&options.memory, "memory", "m", "Memory limit") + flags.Var(&options.memorySwap, "memory-swap", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap") flags.Var(&options.shmSize, "shm-size", "Size of /dev/shm") flags.Int64VarP(&options.cpuShares, "cpu-shares", "c", 0, "CPU shares (relative weight)") flags.Int64Var(&options.cpuPeriod, "cpu-period", 0, "Limit the CPU CFS (Completely Fair Scheduler) period") @@ -254,32 +254,10 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { var body io.Reader = progress.NewProgressReader(buildCtx, progressOutput, 0, "", "Sending build context to Docker daemon") - var memory int64 - if options.memory != "" { - parsedMemory, err := units.RAMInBytes(options.memory) - if err != nil { - return err - } - memory = parsedMemory - } - - var memorySwap int64 - if options.memorySwap != "" { - if options.memorySwap == "-1" { - memorySwap = -1 - } else { - parsedMemorySwap, err := units.RAMInBytes(options.memorySwap) - if err != nil { - return err - } - memorySwap = parsedMemorySwap - } - } - authConfigs, _ := dockerCli.GetAllCredentials() buildOptions := types.ImageBuildOptions{ - Memory: memory, - MemorySwap: memorySwap, + Memory: options.memory.Value(), + MemorySwap: options.memorySwap.Value(), Tags: options.tags.GetAll(), SuppressOutput: options.quiet, NoCache: options.noCache, From 22f2b4022766978edad86e752f112d5a4684b9fc Mon Sep 17 00:00:00 2001 From: James Nesbitt Date: Wed, 1 Mar 2017 10:52:00 +0200 Subject: [PATCH 640/978] exported cli compose loader parsing methods Signed-off-by: James Nesbitt Upstream-commit: b6f45eb18ea1ad9c7ebc87e684b08dad22be4cc4 Component: cli --- components/cli/compose/loader/loader.go | 30 ++++++++++++++++--------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/components/cli/compose/loader/loader.go b/components/cli/compose/loader/loader.go index 2ccef7198d..58757354ad 100644 --- a/components/cli/compose/loader/loader.go +++ b/components/cli/compose/loader/loader.go @@ -75,7 +75,7 @@ func Load(configDetails types.ConfigDetails) (*types.Config, error) { return nil, err } - servicesList, err := loadServices(servicesConfig, configDetails.WorkingDir) + servicesList, err := LoadServices(servicesConfig, configDetails.WorkingDir) if err != nil { return nil, err } @@ -89,7 +89,7 @@ func Load(configDetails types.ConfigDetails) (*types.Config, error) { return nil, err } - networksMapping, err := loadNetworks(networksConfig) + networksMapping, err := LoadNetworks(networksConfig) if err != nil { return nil, err } @@ -103,7 +103,7 @@ func Load(configDetails types.ConfigDetails) (*types.Config, error) { return nil, err } - volumesMapping, err := loadVolumes(volumesConfig) + volumesMapping, err := LoadVolumes(volumesConfig) if err != nil { return nil, err } @@ -117,7 +117,7 @@ func Load(configDetails types.ConfigDetails) (*types.Config, error) { return nil, err } - secretsMapping, err := loadSecrets(secretsConfig, configDetails.WorkingDir) + secretsMapping, err := LoadSecrets(secretsConfig, configDetails.WorkingDir) if err != nil { return nil, err } @@ -304,11 +304,13 @@ func formatInvalidKeyError(keyPrefix string, key interface{}) error { return fmt.Errorf("Non-string key %s: %#v", location, key) } -func loadServices(servicesDict types.Dict, workingDir string) ([]types.ServiceConfig, error) { +// LoadServices produces a ServiceConfig map from a compose file Dict +// the servicesDict is not validated if directly used. Use Load() to enable validation +func LoadServices(servicesDict types.Dict, workingDir string) ([]types.ServiceConfig, error) { var services []types.ServiceConfig for name, serviceDef := range servicesDict { - serviceConfig, err := loadService(name, serviceDef.(types.Dict), workingDir) + serviceConfig, err := LoadService(name, serviceDef.(types.Dict), workingDir) if err != nil { return nil, err } @@ -318,7 +320,9 @@ func loadServices(servicesDict types.Dict, workingDir string) ([]types.ServiceCo return services, nil } -func loadService(name string, serviceDict types.Dict, workingDir string) (*types.ServiceConfig, error) { +// LoadService produces a single ServiceConfig from a compose file Dict +// the serviceDict is not validated if directly used. Use Load() to enable validation +func LoadService(name string, serviceDict types.Dict, workingDir string) (*types.ServiceConfig, error) { serviceConfig := &types.ServiceConfig{} if err := transform(serviceDict, serviceConfig); err != nil { return nil, err @@ -405,7 +409,9 @@ func transformUlimits(data interface{}) (interface{}, error) { } } -func loadNetworks(source types.Dict) (map[string]types.NetworkConfig, error) { +// LoadNetworks produces a NetworkConfig map from a compose file Dict +// the source Dict is not validated if directly used. Use Load() to enable validation +func LoadNetworks(source types.Dict) (map[string]types.NetworkConfig, error) { networks := make(map[string]types.NetworkConfig) err := transform(source, &networks) if err != nil { @@ -420,7 +426,9 @@ func loadNetworks(source types.Dict) (map[string]types.NetworkConfig, error) { return networks, nil } -func loadVolumes(source types.Dict) (map[string]types.VolumeConfig, error) { +// LoadVolumes produces a VolumeConfig map from a compose file Dict +// the source Dict is not validated if directly used. Use Load() to enable validation +func LoadVolumes(source types.Dict) (map[string]types.VolumeConfig, error) { volumes := make(map[string]types.VolumeConfig) err := transform(source, &volumes) if err != nil { @@ -435,7 +443,9 @@ func loadVolumes(source types.Dict) (map[string]types.VolumeConfig, error) { return volumes, nil } -func loadSecrets(source types.Dict, workingDir string) (map[string]types.SecretConfig, error) { +// LoadSecrets produces a SecretConfig map from a compose file Dict +// the source Dict is not validated if directly used. Use Load() to enable validation +func LoadSecrets(source types.Dict, workingDir string) (map[string]types.SecretConfig, error) { secrets := make(map[string]types.SecretConfig) if err := transform(source, &secrets); err != nil { return secrets, err From ea19043c45473f4d73728c2d51d4d4f042f4f892 Mon Sep 17 00:00:00 2001 From: Arash Deshmeh Date: Mon, 20 Feb 2017 01:12:36 -0500 Subject: [PATCH 641/978] stack deploy exits with error if both 'external' and any other options are specified for volumes Signed-off-by: Arash Deshmeh Upstream-commit: 789652c41a175767b2e2ff9f3fd14535c43c8354 Component: cli --- components/cli/compose/loader/loader.go | 18 ++++++-- components/cli/compose/loader/loader_test.go | 44 ++++++++++++++++++++ 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/components/cli/compose/loader/loader.go b/components/cli/compose/loader/loader.go index 2ccef7198d..7fbcde6720 100644 --- a/components/cli/compose/loader/loader.go +++ b/components/cli/compose/loader/loader.go @@ -427,9 +427,21 @@ func loadVolumes(source types.Dict) (map[string]types.VolumeConfig, error) { return volumes, err } for name, volume := range volumes { - if volume.External.External && volume.External.Name == "" { - volume.External.Name = name - volumes[name] = volume + if volume.External.External { + template := "conflicting parameters \"external\" and %q specified for volume %q" + if volume.Driver != "" { + return nil, fmt.Errorf(template, "driver", name) + } + if len(volume.DriverOpts) > 0 { + return nil, fmt.Errorf(template, "driver_opts", name) + } + if len(volume.Labels) > 0 { + return nil, fmt.Errorf(template, "labels", name) + } + if volume.External.Name == "" { + volume.External.Name = name + volumes[name] = volume + } } } return volumes, nil diff --git a/components/cli/compose/loader/loader_test.go b/components/cli/compose/loader/loader_test.go index 53f4280b64..afa2882c32 100644 --- a/components/cli/compose/loader/loader_test.go +++ b/components/cli/compose/loader/loader_test.go @@ -541,6 +541,50 @@ services: assert.Contains(t, forbidden, "extends") } +func TestInvalidExternalAndDriverCombination(t *testing.T) { + _, err := loadYAML(` +version: "3" +volumes: + external_volume: + external: true + driver: foobar +`) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "conflicting parameters \"external\" and \"driver\" specified for volume") + assert.Contains(t, err.Error(), "external_volume") +} + +func TestInvalidExternalAndDirverOptsCombination(t *testing.T) { + _, err := loadYAML(` +version: "3" +volumes: + external_volume: + external: true + driver_opts: + beep: boop +`) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "conflicting parameters \"external\" and \"driver_opts\" specified for volume") + assert.Contains(t, err.Error(), "external_volume") +} + +func TestInvalidExternalAndLabelsCombination(t *testing.T) { + _, err := loadYAML(` +version: "3" +volumes: + external_volume: + external: true + labels: + - beep=boop +`) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "conflicting parameters \"external\" and \"labels\" specified for volume") + assert.Contains(t, err.Error(), "external_volume") +} + func durationPtr(value time.Duration) *time.Duration { return &value } From ffb09e625f1109048f74972f7f6fcfd6930e5e0b Mon Sep 17 00:00:00 2001 From: Andrea Luzzardi Date: Tue, 6 Dec 2016 18:57:22 -0800 Subject: [PATCH 642/978] service logs: Improve formatting - Align output. Previously, output would end up unaligned because of longer task names (e.g. web.1 vs web.10) - Truncate task IDs and add a --no-trunc option - Added a --no-ids option to remove IDs altogether - Got rid of the generic ID Resolver as we need more customization. Signed-off-by: Andrea Luzzardi Upstream-commit: 1d379b9691e8339020c463d4eee93b897aa8dc6c Component: cli --- .../cli/command/idresolver/idresolver.go | 22 +-- components/cli/command/service/logs.go | 130 ++++++++++++++---- 2 files changed, 107 insertions(+), 45 deletions(-) diff --git a/components/cli/command/idresolver/idresolver.go b/components/cli/command/idresolver/idresolver.go index 511b1a8f54..ad0d96735d 100644 --- a/components/cli/command/idresolver/idresolver.go +++ b/components/cli/command/idresolver/idresolver.go @@ -7,7 +7,6 @@ import ( "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/client" - "github.com/docker/docker/pkg/stringid" ) // IDResolver provides ID to Name resolution. @@ -27,7 +26,7 @@ func New(client client.APIClient, noResolve bool) *IDResolver { } func (r *IDResolver) get(ctx context.Context, t interface{}, id string) (string, error) { - switch t := t.(type) { + switch t.(type) { case swarm.Node: node, _, err := r.client.NodeInspectWithRaw(ctx, id) if err != nil { @@ -46,25 +45,6 @@ func (r *IDResolver) get(ctx context.Context, t interface{}, id string) (string, return id, nil } return service.Spec.Annotations.Name, nil - case swarm.Task: - // If the caller passes the full task there's no need to do a lookup. - if t.ID == "" { - var err error - - t, _, err = r.client.TaskInspectWithRaw(ctx, id) - if err != nil { - return id, nil - } - } - taskID := stringid.TruncateID(t.ID) - if t.ServiceID == "" { - return taskID, nil - } - service, err := r.Resolve(ctx, swarm.Service{}, t.ServiceID) - if err != nil { - return "", err - } - return fmt.Sprintf("%s.%d.%s", service, t.Slot, taskID), nil default: return "", fmt.Errorf("unsupported type") } diff --git a/components/cli/command/service/logs.go b/components/cli/command/service/logs.go index 19d3d9a488..2f3e6ca90d 100644 --- a/components/cli/command/service/logs.go +++ b/components/cli/command/service/logs.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "io" + "strconv" "strings" "golang.org/x/net/context" @@ -13,12 +14,16 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/idresolver" + "github.com/docker/docker/client" "github.com/docker/docker/pkg/stdcopy" + "github.com/docker/docker/pkg/stringid" "github.com/spf13/cobra" ) type logsOptions struct { noResolve bool + noTrunc bool + noIDs bool follow bool since string timestamps bool @@ -44,6 +49,8 @@ func newLogsCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names") + flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output") + flags.BoolVar(&opts.noIDs, "no-ids", false, "Do not include task IDs") flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output") flags.StringVar(&opts.since, "since", "", "Show logs since timestamp") flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps") @@ -66,26 +73,91 @@ func runLogs(dockerCli *command.DockerCli, opts *logsOptions) error { } client := dockerCli.Client() + + service, _, err := client.ServiceInspectWithRaw(ctx, opts.service) + if err != nil { + return err + } + responseBody, err := client.ServiceLogs(ctx, opts.service, options) if err != nil { return err } defer responseBody.Close() - resolver := idresolver.New(client, opts.noResolve) + var replicas uint64 + if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil { + replicas = *service.Spec.Mode.Replicated.Replicas + } + padding := len(strconv.FormatUint(replicas, 10)) - stdout := &logWriter{ctx: ctx, opts: opts, r: resolver, w: dockerCli.Out()} - stderr := &logWriter{ctx: ctx, opts: opts, r: resolver, w: dockerCli.Err()} + taskFormatter := newTaskFormatter(client, opts, padding) + + stdout := &logWriter{ctx: ctx, opts: opts, f: taskFormatter, w: dockerCli.Out()} + stderr := &logWriter{ctx: ctx, opts: opts, f: taskFormatter, w: dockerCli.Err()} // TODO(aluzzardi): Do an io.Copy for services with TTY enabled. _, err = stdcopy.StdCopy(stdout, stderr, responseBody) return err } +type taskFormatter struct { + client client.APIClient + opts *logsOptions + padding int + + r *idresolver.IDResolver + cache map[logContext]string +} + +func newTaskFormatter(client client.APIClient, opts *logsOptions, padding int) *taskFormatter { + return &taskFormatter{ + client: client, + opts: opts, + padding: padding, + r: idresolver.New(client, opts.noResolve), + cache: make(map[logContext]string), + } +} + +func (f *taskFormatter) format(ctx context.Context, logCtx logContext) (string, error) { + if cached, ok := f.cache[logCtx]; ok { + return cached, nil + } + + nodeName, err := f.r.Resolve(ctx, swarm.Node{}, logCtx.nodeID) + if err != nil { + return "", err + } + + serviceName, err := f.r.Resolve(ctx, swarm.Service{}, logCtx.serviceID) + if err != nil { + return "", err + } + + task, _, err := f.client.TaskInspectWithRaw(ctx, logCtx.taskID) + if err != nil { + return "", err + } + + taskName := fmt.Sprintf("%s.%d", serviceName, task.Slot) + if !f.opts.noIDs { + if f.opts.noTrunc { + taskName += fmt.Sprintf(".%s", task.ID) + } else { + taskName += fmt.Sprintf(".%s", stringid.TruncateID(task.ID)) + } + } + padding := strings.Repeat(" ", f.padding-len(strconv.FormatInt(int64(task.Slot), 10))) + formatted := fmt.Sprintf("%s@%s%s", taskName, nodeName, padding) + f.cache[logCtx] = formatted + return formatted, nil +} + type logWriter struct { ctx context.Context opts *logsOptions - r *idresolver.IDResolver + f *taskFormatter w io.Writer } @@ -102,7 +174,7 @@ func (lw *logWriter) Write(buf []byte) (int, error) { return 0, fmt.Errorf("invalid context in log message: %v", string(buf)) } - taskName, nodeName, err := lw.parseContext(string(parts[contextIndex])) + logCtx, err := lw.parseContext(string(parts[contextIndex])) if err != nil { return 0, err } @@ -115,8 +187,11 @@ func (lw *logWriter) Write(buf []byte) (int, error) { } if i == contextIndex { - // TODO(aluzzardi): Consider constant padding. - output = append(output, []byte(fmt.Sprintf("%s@%s |", taskName, nodeName))...) + formatted, err := lw.f.format(lw.ctx, logCtx) + if err != nil { + return 0, err + } + output = append(output, []byte(fmt.Sprintf("%s |", formatted))...) } else { output = append(output, part...) } @@ -129,35 +204,42 @@ func (lw *logWriter) Write(buf []byte) (int, error) { return len(buf), nil } -func (lw *logWriter) parseContext(input string) (string, string, error) { +func (lw *logWriter) parseContext(input string) (logContext, error) { context := make(map[string]string) components := strings.Split(input, ",") for _, component := range components { parts := strings.SplitN(component, "=", 2) if len(parts) != 2 { - return "", "", fmt.Errorf("invalid context: %s", input) + return logContext{}, fmt.Errorf("invalid context: %s", input) } context[parts[0]] = parts[1] } - taskID, ok := context["com.docker.swarm.task.id"] - if !ok { - return "", "", fmt.Errorf("missing task id in context: %s", input) - } - taskName, err := lw.r.Resolve(lw.ctx, swarm.Task{}, taskID) - if err != nil { - return "", "", err - } - nodeID, ok := context["com.docker.swarm.node.id"] if !ok { - return "", "", fmt.Errorf("missing node id in context: %s", input) - } - nodeName, err := lw.r.Resolve(lw.ctx, swarm.Node{}, nodeID) - if err != nil { - return "", "", err + return logContext{}, fmt.Errorf("missing node id in context: %s", input) } - return taskName, nodeName, nil + serviceID, ok := context["com.docker.swarm.service.id"] + if !ok { + return logContext{}, fmt.Errorf("missing service id in context: %s", input) + } + + taskID, ok := context["com.docker.swarm.task.id"] + if !ok { + return logContext{}, fmt.Errorf("missing task id in context: %s", input) + } + + return logContext{ + nodeID: nodeID, + serviceID: serviceID, + taskID: taskID, + }, nil +} + +type logContext struct { + nodeID string + serviceID string + taskID string } From ca1b295553212e938a06f55f79457c41a03bf2cb Mon Sep 17 00:00:00 2001 From: Drew Erny Date: Wed, 8 Mar 2017 16:28:21 -0800 Subject: [PATCH 643/978] Fixed concerns, updated, and rebased PR. Signed-off-by: Drew Erny Upstream-commit: 3cda347b3de0cbaa9bfe814a7052a7b9847bbceb Component: cli --- components/cli/command/service/logs.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/components/cli/command/service/logs.go b/components/cli/command/service/logs.go index 2f3e6ca90d..5e7cce3e2c 100644 --- a/components/cli/command/service/logs.go +++ b/components/cli/command/service/logs.go @@ -23,11 +23,10 @@ import ( type logsOptions struct { noResolve bool noTrunc bool - noIDs bool + noTaskIDs bool follow bool since string timestamps bool - details bool tail string service string @@ -50,11 +49,10 @@ func newLogsCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names") flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output") - flags.BoolVar(&opts.noIDs, "no-ids", false, "Do not include task IDs") + flags.BoolVar(&opts.noTaskIDs, "no-task-ids", false, "Do not include task IDs") flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output") flags.StringVar(&opts.since, "since", "", "Show logs since timestamp") flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps") - flags.BoolVar(&opts.details, "details", false, "Show extra details provided to logs") flags.StringVar(&opts.tail, "tail", "all", "Number of lines to show from the end of the logs") return cmd } @@ -69,7 +67,6 @@ func runLogs(dockerCli *command.DockerCli, opts *logsOptions) error { Timestamps: opts.timestamps, Follow: opts.follow, Tail: opts.tail, - Details: opts.details, } client := dockerCli.Client() @@ -86,10 +83,12 @@ func runLogs(dockerCli *command.DockerCli, opts *logsOptions) error { defer responseBody.Close() var replicas uint64 + padding := 1 if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil { + // if replicas are initialized, figure out if we need to pad them replicas = *service.Spec.Mode.Replicated.Replicas + padding = len(strconv.FormatUint(replicas, 10)) } - padding := len(strconv.FormatUint(replicas, 10)) taskFormatter := newTaskFormatter(client, opts, padding) @@ -141,7 +140,7 @@ func (f *taskFormatter) format(ctx context.Context, logCtx logContext) (string, } taskName := fmt.Sprintf("%s.%d", serviceName, task.Slot) - if !f.opts.noIDs { + if !f.opts.noTaskIDs { if f.opts.noTrunc { taskName += fmt.Sprintf(".%s", task.ID) } else { From c6680061914882d9bbf41530bf4ad65d91ea4eea Mon Sep 17 00:00:00 2001 From: Harald Albers Date: Thu, 9 Mar 2017 09:28:19 +0100 Subject: [PATCH 644/978] Fix description of `docker run|create --stop-signal` in help message Signed-off-by: Harald Albers Upstream-commit: 50a10e9bf44fbab40cef95a391d0cac9ed565928 Component: cli --- components/cli/command/container/opts.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/container/opts.go b/components/cli/command/container/opts.go index 0413ae5f71..564a49367a 100644 --- a/components/cli/command/container/opts.go +++ b/components/cli/command/container/opts.go @@ -173,7 +173,7 @@ func addFlags(flags *pflag.FlagSet) *containerOptions { flags.Var(&copts.labelsFile, "label-file", "Read in a line delimited file of labels") flags.BoolVar(&copts.readonlyRootfs, "read-only", false, "Mount the container's root filesystem as read only") flags.StringVar(&copts.restartPolicy, "restart", "no", "Restart policy to apply when a container exits") - flags.StringVar(&copts.stopSignal, "stop-signal", signal.DefaultStopSignal, fmt.Sprintf("Signal to stop a container, %v by default", signal.DefaultStopSignal)) + flags.StringVar(&copts.stopSignal, "stop-signal", signal.DefaultStopSignal, "Signal to stop a container") flags.IntVar(&copts.stopTimeout, "stop-timeout", 0, "Timeout (in seconds) to stop a container") flags.SetAnnotation("stop-timeout", "version", []string{"1.25"}) flags.Var(copts.sysctls, "sysctl", "Sysctl options") From 6925ee6fc16e4423d43107cffd7f82e7c8509433 Mon Sep 17 00:00:00 2001 From: Ying Li Date: Thu, 9 Mar 2017 10:45:15 -0800 Subject: [PATCH 645/978] Use either the system root pool or an empty cert pool with custom CA roots, and not a joint system+custom CA roots pool, when connecting from a docker client to a remote daemon. Signed-off-by: Ying Li Upstream-commit: 63bb7d89adb40d8658b42b9c6337ab247c76ebc4 Component: cli --- components/cli/command/cli.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/components/cli/command/cli.go b/components/cli/command/cli.go index 782c3a5074..be38b8acf7 100644 --- a/components/cli/command/cli.go +++ b/components/cli/command/cli.go @@ -243,8 +243,9 @@ func newHTTPClient(host string, tlsOptions *tlsconfig.Options) (*http.Client, er // let the api client configure the default transport. return nil, nil } - - config, err := tlsconfig.Client(*tlsOptions) + opts := *tlsOptions + opts.ExclusiveRootPools = true + config, err := tlsconfig.Client(opts) if err != nil { return nil, err } From 405e3ec554fa3205f5bf256dc7435f57b798b1ae Mon Sep 17 00:00:00 2001 From: allencloud Date: Thu, 2 Feb 2017 04:03:58 +0800 Subject: [PATCH 646/978] do not fail fast when executing inspect command Signed-off-by: allencloud Upstream-commit: 49570cf783033135dde52c3001f6eafee1446792 Component: cli --- components/cli/command/inspect/inspector.go | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/components/cli/command/inspect/inspector.go b/components/cli/command/inspect/inspector.go index 1e53671f84..a899da065b 100644 --- a/components/cli/command/inspect/inspector.go +++ b/components/cli/command/inspect/inspector.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io" + "strings" "text/template" "github.com/Sirupsen/logrus" @@ -60,17 +61,16 @@ func Inspect(out io.Writer, references []string, tmplStr string, getRef GetRefFu return cli.StatusError{StatusCode: 64, Status: err.Error()} } - var inspectErr error + var inspectErrs []string for _, ref := range references { element, raw, err := getRef(ref) if err != nil { - inspectErr = err - break + inspectErrs = append(inspectErrs, err.Error()) + continue } if err := inspector.Inspect(element, raw); err != nil { - inspectErr = err - break + inspectErrs = append(inspectErrs, err.Error()) } } @@ -78,8 +78,11 @@ func Inspect(out io.Writer, references []string, tmplStr string, getRef GetRefFu logrus.Errorf("%s\n", err) } - if inspectErr != nil { - return cli.StatusError{StatusCode: 1, Status: inspectErr.Error()} + if len(inspectErrs) != 0 { + return cli.StatusError{ + StatusCode: 1, + Status: strings.Join(inspectErrs, "\n"), + } } return nil } From 0a02d1454587d574c1b6362606b4920916194174 Mon Sep 17 00:00:00 2001 From: Ian Campbell Date: Mon, 6 Mar 2017 17:29:09 +0000 Subject: [PATCH 647/978] Correct CPU usage calculation in presence of offline CPUs and newer Linux In https://github.com/torvalds/linux/commit/5ca3726 (released in v4.7-rc1) the content of the `cpuacct.usage_percpu` file in sysfs was changed to include both online and offline cpus. This broke the arithmetic in the stats helpers used by `docker stats`, since it was using the length of the PerCPUUsage array as a proxy for the number of online CPUs. Add current number of online CPUs to types.StatsJSON and use it in the calculation. Keep a fallback to `len(v.CPUStats.CPUUsage.PercpuUsage)` so this code continues to work when talking to an older daemon. An old client talking to a new daemon will ignore the new field and behave as before. Fixes #28941. Signed-off-by: Ian Campbell Upstream-commit: 54a5077ca584bf860031d9d1e3a8cffdeda9d6c1 Component: cli --- components/cli/command/container/stats_helpers.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/components/cli/command/container/stats_helpers.go b/components/cli/command/container/stats_helpers.go index 5fad740435..3dc939a137 100644 --- a/components/cli/command/container/stats_helpers.go +++ b/components/cli/command/container/stats_helpers.go @@ -178,10 +178,14 @@ func calculateCPUPercentUnix(previousCPU, previousSystem uint64, v *types.StatsJ cpuDelta = float64(v.CPUStats.CPUUsage.TotalUsage) - float64(previousCPU) // calculate the change for the entire system between readings systemDelta = float64(v.CPUStats.SystemUsage) - float64(previousSystem) + onlineCPUs = float64(v.CPUStats.OnlineCPUs) ) + if onlineCPUs == 0.0 { + onlineCPUs = float64(len(v.CPUStats.CPUUsage.PercpuUsage)) + } if systemDelta > 0.0 && cpuDelta > 0.0 { - cpuPercent = (cpuDelta / systemDelta) * float64(len(v.CPUStats.CPUUsage.PercpuUsage)) * 100.0 + cpuPercent = (cpuDelta / systemDelta) * onlineCPUs * 100.0 } return cpuPercent } From aa331e22534a9ba3f869fc6ca55b2b08dff6a22c Mon Sep 17 00:00:00 2001 From: Drew Erny Date: Wed, 1 Mar 2017 16:37:25 -0800 Subject: [PATCH 648/978] Add tail and since to service logs This change adds the ability to do --tail and --since on docker service logs. It wires up the API endpoints to each other and fixes some older bugs. It adds integration tests for these new features. Signed-off-by: Drew Erny Upstream-commit: 7ce96255fdfeb5d8dfda76b3cc895bd6824f5050 Component: cli --- components/cli/command/service/logs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/service/logs.go b/components/cli/command/service/logs.go index 19d3d9a488..40e7bd41fd 100644 --- a/components/cli/command/service/logs.go +++ b/components/cli/command/service/logs.go @@ -45,7 +45,7 @@ func newLogsCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names") flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output") - flags.StringVar(&opts.since, "since", "", "Show logs since timestamp") + flags.StringVar(&opts.since, "since", "", "Show logs since timestamp (e.g. 2013-01-02T13:23:37) or relative (e.g. 42m for 42 minutes)") flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps") flags.BoolVar(&opts.details, "details", false, "Show extra details provided to logs") flags.StringVar(&opts.tail, "tail", "all", "Number of lines to show from the end of the logs") From a3d2ba14fc00f21988642f1d042ab690086310f4 Mon Sep 17 00:00:00 2001 From: Jeremy Chambers Date: Sun, 12 Feb 2017 13:22:01 -0600 Subject: [PATCH 649/978] Implements --format option for docker history command by creating a formatter Signed-off-by: Jeremy Chambers Adds to history documentation for --format Signed-off-by: Jeremy Chambers Adds MarshalJSON to historyContext for {{json .}} format Signed-off-by: Jeremy Chambers Adds back the --human option to history command Signed-off-by: Jeremy Chambers Cleans up formatter around --human option for history, Adds integration test for --format option of history Signed-off-by: Jeremy Chambers Adds test for history formatter checking full table results, Runs go fmt on touched files Signed-off-by: Jeremy Chambers Fixes lint errors in formatter/history Signed-off-by: Jeremy Chambers Runs go fmt on cli/command/formatter/history.go Signed-off-by: Jeremy Chambers sRemoves integration test for --format option of history Merges Created and CreatedSince in docker history formatter, Updates docs and tests Upstream-commit: 371839ee5cad72119af20f206a1b5b2118750943 Component: cli --- components/cli/command/formatter/history.go | 113 ++++++++++ .../cli/command/formatter/history_test.go | 213 ++++++++++++++++++ components/cli/command/image/history.go | 57 +---- 3 files changed, 337 insertions(+), 46 deletions(-) create mode 100644 components/cli/command/formatter/history.go create mode 100644 components/cli/command/formatter/history_test.go diff --git a/components/cli/command/formatter/history.go b/components/cli/command/formatter/history.go new file mode 100644 index 0000000000..2b7de399a0 --- /dev/null +++ b/components/cli/command/formatter/history.go @@ -0,0 +1,113 @@ +package formatter + +import ( + "strconv" + "strings" + "time" + + "github.com/docker/docker/api/types/image" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/stringutils" + units "github.com/docker/go-units" +) + +const ( + defaultHistoryTableFormat = "table {{.ID}}\t{{.CreatedSince}}\t{{.CreatedBy}}\t{{.Size}}\t{{.Comment}}" + nonHumanHistoryTableFormat = "table {{.ID}}\t{{.CreatedAt}}\t{{.CreatedBy}}\t{{.Size}}\t{{.Comment}}" + + historyIDHeader = "IMAGE" + createdByHeader = "CREATED BY" + commentHeader = "COMMENT" +) + +// NewHistoryFormat returns a format for rendering an HistoryContext +func NewHistoryFormat(source string, quiet bool, human bool) Format { + switch source { + case TableFormatKey: + switch { + case quiet: + return defaultQuietFormat + case !human: + return nonHumanHistoryTableFormat + default: + return defaultHistoryTableFormat + } + } + + return Format(source) +} + +// HistoryWrite writes the context +func HistoryWrite(ctx Context, human bool, histories []image.HistoryResponseItem) error { + render := func(format func(subContext subContext) error) error { + for _, history := range histories { + historyCtx := &historyContext{trunc: ctx.Trunc, h: history, human: human} + if err := format(historyCtx); err != nil { + return err + } + } + return nil + } + historyCtx := &historyContext{} + historyCtx.header = map[string]string{ + "ID": historyIDHeader, + "CreatedSince": createdSinceHeader, + "CreatedAt": createdAtHeader, + "CreatedBy": createdByHeader, + "Size": sizeHeader, + "Comment": commentHeader, + } + return ctx.Write(historyCtx, render) +} + +type historyContext struct { + HeaderContext + trunc bool + human bool + h image.HistoryResponseItem +} + +func (c *historyContext) MarshalJSON() ([]byte, error) { + return marshalJSON(c) +} + +func (c *historyContext) ID() string { + if c.trunc { + return stringid.TruncateID(c.h.ID) + } + return c.h.ID +} + +func (c *historyContext) CreatedAt() string { + var created string + created = units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(c.h.Created), 0))) + return created +} + +func (c *historyContext) CreatedSince() string { + var created string + created = units.HumanDuration(time.Now().UTC().Sub(time.Unix(int64(c.h.Created), 0))) + return created + " ago" +} + +func (c *historyContext) CreatedBy() string { + createdBy := strings.Replace(c.h.CreatedBy, "\t", " ", -1) + if c.trunc { + createdBy = stringutils.Ellipsis(createdBy, 45) + } + return createdBy +} + +func (c *historyContext) Size() string { + size := "" + if c.human { + size = units.HumanSizeWithPrecision(float64(c.h.Size), 3) + } else { + size = strconv.FormatInt(c.h.Size, 10) + } + return size +} + +func (c *historyContext) Comment() string { + return c.h.Comment +} diff --git a/components/cli/command/formatter/history_test.go b/components/cli/command/formatter/history_test.go new file mode 100644 index 0000000000..299fb1135b --- /dev/null +++ b/components/cli/command/formatter/history_test.go @@ -0,0 +1,213 @@ +package formatter + +import ( + "strconv" + "strings" + "testing" + "time" + + "bytes" + "github.com/docker/docker/api/types/image" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/stringutils" + "github.com/docker/docker/pkg/testutil/assert" +) + +type historyCase struct { + historyCtx historyContext + expValue string + call func() string +} + +func TestHistoryContext_ID(t *testing.T) { + id := stringid.GenerateRandomID() + + var ctx historyContext + cases := []historyCase{ + { + historyContext{ + h: image.HistoryResponseItem{ID: id}, + trunc: false, + }, id, ctx.ID, + }, + { + historyContext{ + h: image.HistoryResponseItem{ID: id}, + trunc: true, + }, stringid.TruncateID(id), ctx.ID, + }, + } + + for _, c := range cases { + ctx = c.historyCtx + v := c.call() + if strings.Contains(v, ",") { + compareMultipleValues(t, v, c.expValue) + } else if v != c.expValue { + t.Fatalf("Expected %s, was %s\n", c.expValue, v) + } + } +} + +func TestHistoryContext_CreatedSince(t *testing.T) { + unixTime := time.Now().AddDate(0, 0, -7).Unix() + expected := "7 days ago" + + var ctx historyContext + cases := []historyCase{ + { + historyContext{ + h: image.HistoryResponseItem{Created: unixTime}, + trunc: false, + human: true, + }, expected, ctx.CreatedSince, + }, + } + + for _, c := range cases { + ctx = c.historyCtx + v := c.call() + if strings.Contains(v, ",") { + compareMultipleValues(t, v, c.expValue) + } else if v != c.expValue { + t.Fatalf("Expected %s, was %s\n", c.expValue, v) + } + } +} + +func TestHistoryContext_CreatedBy(t *testing.T) { + withTabs := `/bin/sh -c apt-key adv --keyserver hkp://pgp.mit.edu:80 --recv-keys 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62 && echo "deb http://nginx.org/packages/mainline/debian/ jessie nginx" >> /etc/apt/sources.list && apt-get update && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates nginx=${NGINX_VERSION} nginx-module-xslt nginx-module-geoip nginx-module-image-filter nginx-module-perl nginx-module-njs gettext-base && rm -rf /var/lib/apt/lists/*` + expected := `/bin/sh -c apt-key adv --keyserver hkp://pgp.mit.edu:80 --recv-keys 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62 && echo "deb http://nginx.org/packages/mainline/debian/ jessie nginx" >> /etc/apt/sources.list && apt-get update && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates nginx=${NGINX_VERSION} nginx-module-xslt nginx-module-geoip nginx-module-image-filter nginx-module-perl nginx-module-njs gettext-base && rm -rf /var/lib/apt/lists/*` + + var ctx historyContext + cases := []historyCase{ + { + historyContext{ + h: image.HistoryResponseItem{CreatedBy: withTabs}, + trunc: false, + }, expected, ctx.CreatedBy, + }, + { + historyContext{ + h: image.HistoryResponseItem{CreatedBy: withTabs}, + trunc: true, + }, stringutils.Ellipsis(expected, 45), ctx.CreatedBy, + }, + } + + for _, c := range cases { + ctx = c.historyCtx + v := c.call() + if strings.Contains(v, ",") { + compareMultipleValues(t, v, c.expValue) + } else if v != c.expValue { + t.Fatalf("Expected %s, was %s\n", c.expValue, v) + } + } +} + +func TestHistoryContext_Size(t *testing.T) { + size := int64(182964289) + expected := "183MB" + + var ctx historyContext + cases := []historyCase{ + { + historyContext{ + h: image.HistoryResponseItem{Size: size}, + trunc: false, + human: true, + }, expected, ctx.Size, + }, { + historyContext{ + h: image.HistoryResponseItem{Size: size}, + trunc: false, + human: false, + }, strconv.Itoa(182964289), ctx.Size, + }, + } + + for _, c := range cases { + ctx = c.historyCtx + v := c.call() + if strings.Contains(v, ",") { + compareMultipleValues(t, v, c.expValue) + } else if v != c.expValue { + t.Fatalf("Expected %s, was %s\n", c.expValue, v) + } + } +} + +func TestHistoryContext_Comment(t *testing.T) { + comment := "Some comment" + + var ctx historyContext + cases := []historyCase{ + { + historyContext{ + h: image.HistoryResponseItem{Comment: comment}, + trunc: false, + }, comment, ctx.Comment, + }, + } + + for _, c := range cases { + ctx = c.historyCtx + v := c.call() + if strings.Contains(v, ",") { + compareMultipleValues(t, v, c.expValue) + } else if v != c.expValue { + t.Fatalf("Expected %s, was %s\n", c.expValue, v) + } + } +} + +func TestHistoryContext_Table(t *testing.T) { + out := bytes.NewBufferString("") + unixTime := time.Now().AddDate(0, 0, -1).Unix() + histories := []image.HistoryResponseItem{ + {ID: "imageID1", Created: unixTime, CreatedBy: "/bin/bash ls && npm i && npm run test && karma -c karma.conf.js start && npm start && more commands here && the list goes on", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}}, + {ID: "imageID2", Created: unixTime, CreatedBy: "/bin/bash echo", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}}, + {ID: "imageID3", Created: unixTime, CreatedBy: "/bin/bash ls", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}}, + {ID: "imageID4", Created: unixTime, CreatedBy: "/bin/bash grep", Size: int64(182964289), Comment: "Hi", Tags: []string{"image:tag2"}}, + } + expectedNoTrunc := `IMAGE CREATED CREATED BY SIZE COMMENT +imageID1 24 hours ago /bin/bash ls && npm i && npm run test && karma -c karma.conf.js start && npm start && more commands here && the list goes on 183MB Hi +imageID2 24 hours ago /bin/bash echo 183MB Hi +imageID3 24 hours ago /bin/bash ls 183MB Hi +imageID4 24 hours ago /bin/bash grep 183MB Hi +` + expectedTrunc := `IMAGE CREATED CREATED BY SIZE COMMENT +imageID1 24 hours ago /bin/bash ls && npm i && npm run test && k... 183MB Hi +imageID2 24 hours ago /bin/bash echo 183MB Hi +imageID3 24 hours ago /bin/bash ls 183MB Hi +imageID4 24 hours ago /bin/bash grep 183MB Hi +` + + contexts := []struct { + context Context + expected string + }{ + {Context{ + Format: NewHistoryFormat("table", false, true), + Trunc: true, + Output: out, + }, + expectedTrunc, + }, + {Context{ + Format: NewHistoryFormat("table", false, true), + Trunc: false, + Output: out, + }, + expectedNoTrunc, + }, + } + + for _, context := range contexts { + HistoryWrite(context.context, true, histories) + assert.Equal(t, out.String(), context.expected) + // Clean buffer + out.Reset() + } +} diff --git a/components/cli/command/image/history.go b/components/cli/command/image/history.go index 91c8f75a63..4d964b4d40 100644 --- a/components/cli/command/image/history.go +++ b/components/cli/command/image/history.go @@ -1,19 +1,11 @@ package image import ( - "fmt" - "strconv" - "strings" - "text/tabwriter" - "time" - "golang.org/x/net/context" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" - "github.com/docker/docker/pkg/stringid" - "github.com/docker/docker/pkg/stringutils" - "github.com/docker/go-units" + "github.com/docker/docker/cli/command/formatter" "github.com/spf13/cobra" ) @@ -23,6 +15,7 @@ type historyOptions struct { human bool quiet bool noTrunc bool + format string } // NewHistoryCommand creates a new `docker history` command @@ -44,6 +37,7 @@ func NewHistoryCommand(dockerCli *command.DockerCli) *cobra.Command { flags.BoolVarP(&opts.human, "human", "H", true, "Print sizes and dates in human readable format") flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only show numeric IDs") flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output") + flags.StringVar(&opts.format, "format", "", "Pretty-print images using a Go template") return cmd } @@ -56,44 +50,15 @@ func runHistory(dockerCli *command.DockerCli, opts historyOptions) error { return err } - w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0) - - if opts.quiet { - for _, entry := range history { - if opts.noTrunc { - fmt.Fprintf(w, "%s\n", entry.ID) - } else { - fmt.Fprintf(w, "%s\n", stringid.TruncateID(entry.ID)) - } - } - w.Flush() - return nil + format := opts.format + if len(format) == 0 { + format = formatter.TableFormatKey } - var imageID string - var createdBy string - var created string - var size string - - fmt.Fprintln(w, "IMAGE\tCREATED\tCREATED BY\tSIZE\tCOMMENT") - for _, entry := range history { - imageID = entry.ID - createdBy = strings.Replace(entry.CreatedBy, "\t", " ", -1) - if !opts.noTrunc { - createdBy = stringutils.Ellipsis(createdBy, 45) - imageID = stringid.TruncateID(entry.ID) - } - - if opts.human { - created = units.HumanDuration(time.Now().UTC().Sub(time.Unix(entry.Created, 0))) + " ago" - size = units.HumanSizeWithPrecision(float64(entry.Size), 3) - } else { - created = time.Unix(entry.Created, 0).Format(time.RFC3339) - size = strconv.FormatInt(entry.Size, 10) - } - - fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", imageID, created, createdBy, size, entry.Comment) + historyCtx := formatter.Context{ + Output: dockerCli.Out(), + Format: formatter.NewHistoryFormat(format, opts.quiet, opts.human), + Trunc: !opts.noTrunc, } - w.Flush() - return nil + return formatter.HistoryWrite(historyCtx, opts.human, history) } From 0069d4e4ab19c617e8f034eab97141159c42f175 Mon Sep 17 00:00:00 2001 From: Boaz Shuster Date: Tue, 7 Feb 2017 14:52:20 +0200 Subject: [PATCH 650/978] Hide command options that are related to Windows Signed-off-by: Boaz Shuster Upstream-commit: c7dd91faf5ac03ecd7f8632a719036517489fe1b Component: cli --- components/cli/command/cli.go | 7 +++++++ components/cli/command/container/opts.go | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/components/cli/command/cli.go b/components/cli/command/cli.go index 782c3a5074..783e516f3d 100644 --- a/components/cli/command/cli.go +++ b/components/cli/command/cli.go @@ -51,6 +51,7 @@ type DockerCli struct { keyFile string client client.APIClient hasExperimental bool + osType string defaultVersion string } @@ -59,6 +60,11 @@ func (cli *DockerCli) HasExperimental() bool { return cli.hasExperimental } +// OSType returns the operating system the daemon is running on. +func (cli *DockerCli) OSType() string { + return cli.osType +} + // DefaultVersion returns api.defaultVersion of DOCKER_API_VERSION if specified. func (cli *DockerCli) DefaultVersion() string { return cli.defaultVersion @@ -166,6 +172,7 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error { if ping, err := cli.client.Ping(context.Background()); err == nil { cli.hasExperimental = ping.Experimental + cli.osType = ping.OSType // since the new header was added in 1.25, assume server is 1.24 if header is not present. if ping.APIVersion == "" { diff --git a/components/cli/command/container/opts.go b/components/cli/command/container/opts.go index 16bb1aa434..4ce872b556 100644 --- a/components/cli/command/container/opts.go +++ b/components/cli/command/container/opts.go @@ -189,6 +189,7 @@ func addFlags(flags *pflag.FlagSet) *containerOptions { flags.Var(&copts.securityOpt, "security-opt", "Security Options") flags.StringVar(&copts.usernsMode, "userns", "", "User namespace to use") flags.StringVar(&copts.credentialSpec, "credentialspec", "", "Credential spec for managed service account (Windows only)") + flags.SetAnnotation("credentialspec", "ostype", []string{"windows"}) // Network and port publishing flag flags.Var(&copts.extraHosts, "add-host", "Add a custom host-to-IP mapping (host:ip)") @@ -239,7 +240,9 @@ func addFlags(flags *pflag.FlagSet) *containerOptions { flags.StringVar(&copts.cpusetCpus, "cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)") flags.StringVar(&copts.cpusetMems, "cpuset-mems", "", "MEMs in which to allow execution (0-3, 0,1)") flags.Int64Var(&copts.cpuCount, "cpu-count", 0, "CPU count (Windows only)") + flags.SetAnnotation("cpu-count", "ostype", []string{"windows"}) flags.Int64Var(&copts.cpuPercent, "cpu-percent", 0, "CPU percent (Windows only)") + flags.SetAnnotation("cpu-percent", "ostype", []string{"windows"}) flags.Int64Var(&copts.cpuPeriod, "cpu-period", 0, "Limit CPU CFS (Completely Fair Scheduler) period") flags.Int64Var(&copts.cpuQuota, "cpu-quota", 0, "Limit CPU CFS (Completely Fair Scheduler) quota") flags.Int64Var(&copts.cpuRealtimePeriod, "cpu-rt-period", 0, "Limit CPU real-time period in microseconds") @@ -254,7 +257,9 @@ func addFlags(flags *pflag.FlagSet) *containerOptions { flags.Var(&copts.deviceWriteBps, "device-write-bps", "Limit write rate (bytes per second) to a device") flags.Var(&copts.deviceWriteIOps, "device-write-iops", "Limit write rate (IO per second) to a device") flags.Var(&copts.ioMaxBandwidth, "io-maxbandwidth", "Maximum IO bandwidth limit for the system drive (Windows only)") + flags.SetAnnotation("io-maxbandwidth", "ostype", []string{"windows"}) flags.Uint64Var(&copts.ioMaxIOps, "io-maxiops", 0, "Maximum IOps limit for the system drive (Windows only)") + flags.SetAnnotation("io-maxiops", "ostype", []string{"windows"}) flags.Var(&copts.kernelMemory, "kernel-memory", "Kernel memory limit") flags.VarP(&copts.memory, "memory", "m", "Memory limit") flags.Var(&copts.memoryReservation, "memory-reservation", "Memory soft limit") From 1029e1dfc84d3821264df1362caaac86e1f5a0b2 Mon Sep 17 00:00:00 2001 From: Boaz Shuster Date: Tue, 7 Feb 2017 14:52:20 +0200 Subject: [PATCH 651/978] Hide command options that are related to Windows Signed-off-by: Boaz Shuster Upstream-commit: 818c54a2ca38f1ec38a03467e2ea573f19fbcd20 Component: cli --- components/cli/ping.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/components/cli/ping.go b/components/cli/ping.go index 150b1dc8d8..d6212ef8bb 100644 --- a/components/cli/ping.go +++ b/components/cli/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 1efe06c1d37f8f42823450fcfa02d1255c8d739e Mon Sep 17 00:00:00 2001 From: Boaz Shuster Date: Tue, 7 Feb 2017 14:52:20 +0200 Subject: [PATCH 652/978] Hide command options that are related to Windows Signed-off-by: Boaz Shuster Upstream-commit: e398a784660725e47c68f7ae30e52de4ee6f330d Component: cli --- components/cli/docker.go | 42 +++++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/components/cli/docker.go b/components/cli/docker.go index efc1cac25e..570a52a72e 100644 --- a/components/cli/docker.go +++ b/components/cli/docker.go @@ -49,7 +49,7 @@ func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command { if err := dockerCli.Initialize(opts); err != nil { return err } - return isSupported(cmd, dockerCli.Client().ClientVersion(), dockerCli.HasExperimental()) + return isSupported(cmd, dockerCli.Client().ClientVersion(), dockerCli.OSType(), dockerCli.HasExperimental()) }, } cli.SetupRootCommand(cmd) @@ -80,7 +80,7 @@ func setFlagErrorFunc(dockerCli *command.DockerCli, cmd *cobra.Command, flags *p flagErrorFunc := cmd.FlagErrorFunc() cmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error { initializeDockerCli(dockerCli, flags, opts) - if err := isSupported(cmd, dockerCli.Client().ClientVersion(), dockerCli.HasExperimental()); err != nil { + if err := isSupported(cmd, dockerCli.Client().ClientVersion(), dockerCli.OSType(), dockerCli.HasExperimental()); err != nil { return err } return flagErrorFunc(cmd, err) @@ -90,12 +90,12 @@ func setFlagErrorFunc(dockerCli *command.DockerCli, cmd *cobra.Command, flags *p func setHelpFunc(dockerCli *command.DockerCli, cmd *cobra.Command, flags *pflag.FlagSet, opts *cliflags.ClientOptions) { cmd.SetHelpFunc(func(ccmd *cobra.Command, args []string) { initializeDockerCli(dockerCli, flags, opts) - if err := isSupported(ccmd, dockerCli.Client().ClientVersion(), dockerCli.HasExperimental()); err != nil { + if err := isSupported(ccmd, dockerCli.Client().ClientVersion(), dockerCli.OSType(), dockerCli.HasExperimental()); err != nil { ccmd.Println(err) return } - hideUnsupportedFeatures(ccmd, dockerCli.Client().ClientVersion(), dockerCli.HasExperimental()) + hideUnsupportedFeatures(ccmd, dockerCli.Client().ClientVersion(), dockerCli.OSType(), dockerCli.HasExperimental()) if err := ccmd.Help(); err != nil { ccmd.Println(err) @@ -122,7 +122,7 @@ func setValidateArgs(dockerCli *command.DockerCli, cmd *cobra.Command, flags *pf cmdArgs := ccmd.Args ccmd.Args = func(cmd *cobra.Command, args []string) error { initializeDockerCli(dockerCli, flags, opts) - if err := isSupported(cmd, dockerCli.Client().ClientVersion(), dockerCli.HasExperimental()); err != nil { + if err := isSupported(cmd, dockerCli.Client().ClientVersion(), dockerCli.OSType(), dockerCli.HasExperimental()); err != nil { return err } return cmdArgs(cmd, args) @@ -198,7 +198,7 @@ func dockerPreRun(opts *cliflags.ClientOptions) { } } -func hideUnsupportedFeatures(cmd *cobra.Command, clientVersion string, hasExperimental bool) { +func hideUnsupportedFeatures(cmd *cobra.Command, clientVersion, osType string, hasExperimental bool) { cmd.Flags().VisitAll(func(f *pflag.Flag) { // hide experimental flags if !hasExperimental { @@ -208,10 +208,9 @@ func hideUnsupportedFeatures(cmd *cobra.Command, clientVersion string, hasExperi } // hide flags not supported by the server - if !isFlagSupported(f, clientVersion) { + if !isOSTypeSupported(f, osType) || !isVersionSupported(f, clientVersion) { f.Hidden = true } - }) for _, subcmd := range cmd.Commands() { @@ -229,7 +228,7 @@ func hideUnsupportedFeatures(cmd *cobra.Command, clientVersion string, hasExperi } } -func isSupported(cmd *cobra.Command, clientVersion string, hasExperimental bool) error { +func isSupported(cmd *cobra.Command, clientVersion, osType string, hasExperimental bool) error { // We check recursively so that, e.g., `docker stack ls` will return the same output as `docker stack` if !hasExperimental { for curr := cmd; curr != nil; curr = curr.Parent() { @@ -247,8 +246,12 @@ func isSupported(cmd *cobra.Command, clientVersion string, hasExperimental bool) cmd.Flags().VisitAll(func(f *pflag.Flag) { if f.Changed { - if !isFlagSupported(f, clientVersion) { - errs = append(errs, fmt.Sprintf("\"--%s\" requires API version %s, but the Docker daemon API version is %s", f.Name, getFlagVersion(f), clientVersion)) + if !isVersionSupported(f, clientVersion) { + errs = append(errs, fmt.Sprintf("\"--%s\" requires API version %s, but the Docker daemon API version is %s", f.Name, getFlagAnnotation(f, "version"), clientVersion)) + return + } + if !isOSTypeSupported(f, osType) { + errs = append(errs, fmt.Sprintf("\"--%s\" requires the Docker daemon to run on %s, but the Docker daemon is running on %s", f.Name, getFlagAnnotation(f, "ostype"), osType)) return } if _, ok := f.Annotations["experimental"]; ok && !hasExperimental { @@ -263,20 +266,27 @@ func isSupported(cmd *cobra.Command, clientVersion string, hasExperimental bool) return nil } -func getFlagVersion(f *pflag.Flag) string { - if flagVersion, ok := f.Annotations["version"]; ok && len(flagVersion) == 1 { - return flagVersion[0] +func getFlagAnnotation(f *pflag.Flag, annotation string) string { + if value, ok := f.Annotations[annotation]; ok && len(value) == 1 { + return value[0] } return "" } -func isFlagSupported(f *pflag.Flag, clientVersion string) bool { - if v := getFlagVersion(f); v != "" { +func isVersionSupported(f *pflag.Flag, clientVersion string) bool { + if v := getFlagAnnotation(f, "version"); v != "" { return versions.GreaterThanOrEqualTo(clientVersion, v) } return true } +func isOSTypeSupported(f *pflag.Flag, osType string) bool { + if v := getFlagAnnotation(f, "ostype"); v != "" && osType != "" { + return osType == v + } + return true +} + // hasTags return true if any of the command's parents has tags func hasTags(cmd *cobra.Command) bool { for curr := cmd; curr != nil; curr = curr.Parent() { From a4dea23f9cfc242bdbf83e1bf44dbe8d338f2e21 Mon Sep 17 00:00:00 2001 From: allencloud Date: Fri, 17 Feb 2017 13:34:49 +0800 Subject: [PATCH 653/978] support both endpoint modes in stack Signed-off-by: allencloud Upstream-commit: cd1cde6e77af142e69be02067c2f92bf531e2b86 Component: cli --- components/cli/compose/convert/service.go | 10 +++++++--- components/cli/compose/convert/service_test.go | 3 ++- components/cli/compose/types/types.go | 1 + 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/components/cli/compose/convert/service.go b/components/cli/compose/convert/service.go index 93b910967e..55368e2410 100644 --- a/components/cli/compose/convert/service.go +++ b/components/cli/compose/convert/service.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "sort" + "strings" "time" "github.com/docker/docker/api/types" @@ -54,7 +55,7 @@ func convertService( ) (swarm.ServiceSpec, error) { name := namespace.Scope(service.Name) - endpoint, err := convertEndpointSpec(service.Ports) + endpoint, err := convertEndpointSpec(service.EndpointMode, service.Ports) if err != nil { return swarm.ServiceSpec{}, err } @@ -374,7 +375,7 @@ func (a byPublishedPort) Len() int { return len(a) } func (a byPublishedPort) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a byPublishedPort) Less(i, j int) bool { return a[i].PublishedPort < a[j].PublishedPort } -func convertEndpointSpec(source []composetypes.ServicePortConfig) (*swarm.EndpointSpec, error) { +func convertEndpointSpec(endpointMode string, source []composetypes.ServicePortConfig) (*swarm.EndpointSpec, error) { portConfigs := []swarm.PortConfig{} for _, port := range source { portConfig := swarm.PortConfig{ @@ -387,7 +388,10 @@ func convertEndpointSpec(source []composetypes.ServicePortConfig) (*swarm.Endpoi } sort.Sort(byPublishedPort(portConfigs)) - return &swarm.EndpointSpec{Ports: portConfigs}, nil + return &swarm.EndpointSpec{ + Mode: swarm.ResolutionMode(strings.ToLower(endpointMode)), + Ports: portConfigs, + }, nil } func convertEnvironment(source map[string]string) []string { diff --git a/components/cli/compose/convert/service_test.go b/components/cli/compose/convert/service_test.go index 64ccfd038e..69fa90dbc2 100644 --- a/components/cli/compose/convert/service_test.go +++ b/components/cli/compose/convert/service_test.go @@ -156,9 +156,10 @@ func TestConvertEndpointSpec(t *testing.T) { Published: 80, }, } - endpoint, err := convertEndpointSpec(source) + endpoint, err := convertEndpointSpec("vip", source) expected := swarm.EndpointSpec{ + Mode: swarm.ResolutionMode(strings.ToLower("vip")), Ports: []swarm.PortConfig{ { TargetPort: 8080, diff --git a/components/cli/compose/types/types.go b/components/cli/compose/types/types.go index dce13c928a..d1d762900d 100644 --- a/components/cli/compose/types/types.go +++ b/components/cli/compose/types/types.go @@ -89,6 +89,7 @@ type ServiceConfig struct { DNS StringList DNSSearch StringList `mapstructure:"dns_search"` DomainName string `mapstructure:"domainname"` + EndpointMode string Entrypoint ShellCommand Environment MappingWithEquals EnvFile StringList `mapstructure:"env_file"` From 74ffacfbac8bf8ae7607357a582af98823d671fb Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 13 Mar 2017 14:53:13 -0400 Subject: [PATCH 654/978] Move endpoint_mode under deploy and add it to the schema. Signed-off-by: Daniel Nephin Upstream-commit: 33bfb1e5e5c5f492c1ec5677def64a4b052b6222 Component: cli --- components/cli/compose/convert/service.go | 2 +- components/cli/compose/schema/data/config_schema_v3.1.json | 1 + components/cli/compose/types/types.go | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/components/cli/compose/convert/service.go b/components/cli/compose/convert/service.go index 55368e2410..ece6d5c0f6 100644 --- a/components/cli/compose/convert/service.go +++ b/components/cli/compose/convert/service.go @@ -55,7 +55,7 @@ func convertService( ) (swarm.ServiceSpec, error) { name := namespace.Scope(service.Name) - endpoint, err := convertEndpointSpec(service.EndpointMode, service.Ports) + endpoint, err := convertEndpointSpec(service.Deploy.EndpointMode, service.Ports) if err != nil { return swarm.ServiceSpec{}, err } diff --git a/components/cli/compose/schema/data/config_schema_v3.1.json b/components/cli/compose/schema/data/config_schema_v3.1.json index c5e48968e3..72e1d61bb6 100644 --- a/components/cli/compose/schema/data/config_schema_v3.1.json +++ b/components/cli/compose/schema/data/config_schema_v3.1.json @@ -293,6 +293,7 @@ "type": ["object", "null"], "properties": { "mode": {"type": "string"}, + "endpoint_mode": {"type": "string"}, "replicas": {"type": "integer"}, "labels": {"$ref": "#/definitions/list_or_dict"}, "update_config": { diff --git a/components/cli/compose/types/types.go b/components/cli/compose/types/types.go index d1d762900d..e91b5a7ac8 100644 --- a/components/cli/compose/types/types.go +++ b/components/cli/compose/types/types.go @@ -89,7 +89,6 @@ type ServiceConfig struct { DNS StringList DNSSearch StringList `mapstructure:"dns_search"` DomainName string `mapstructure:"domainname"` - EndpointMode string Entrypoint ShellCommand Environment MappingWithEquals EnvFile StringList `mapstructure:"env_file"` @@ -157,6 +156,7 @@ type DeployConfig struct { Resources Resources RestartPolicy *RestartPolicy `mapstructure:"restart_policy"` Placement Placement + EndpointMode string } // HealthCheckConfig the healthcheck configuration for a service From 29b6bbfa5eecdbbd4a2dcb614ced3ebf9513bcc1 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 7 Mar 2017 17:19:54 -0500 Subject: [PATCH 655/978] Refactor container run cli command. Signed-off-by: Daniel Nephin Upstream-commit: 4e388c22d3c721093d844a95da2c4a97ef9a300b Component: cli --- components/cli/command/container/create.go | 11 +- components/cli/command/container/opts.go | 58 +++--- components/cli/command/container/opts_test.go | 7 +- components/cli/command/container/run.go | 166 ++++++++++-------- 4 files changed, 138 insertions(+), 104 deletions(-) diff --git a/components/cli/command/container/create.go b/components/cli/command/container/create.go index 9559ba0c05..ef894bad5a 100644 --- a/components/cli/command/container/create.go +++ b/components/cli/command/container/create.go @@ -8,7 +8,6 @@ import ( "github.com/docker/distribution/reference" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" - networktypes "github.com/docker/docker/api/types/network" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/image" @@ -57,12 +56,12 @@ func NewCreateCommand(dockerCli *command.DockerCli) *cobra.Command { } func runCreate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *createOptions, copts *containerOptions) error { - config, hostConfig, networkingConfig, err := parse(flags, copts) + containerConfig, err := parse(flags, copts) if err != nil { reportError(dockerCli.Err(), "create", err.Error(), true) return cli.StatusError{StatusCode: 125} } - response, err := createContainer(context.Background(), dockerCli, config, hostConfig, networkingConfig, hostConfig.ContainerIDFile, opts.name) + response, err := createContainer(context.Background(), dockerCli, containerConfig, opts.name) if err != nil { return err } @@ -146,7 +145,10 @@ func newCIDFile(path string) (*cidFile, error) { return &cidFile{path: path, file: f}, nil } -func createContainer(ctx context.Context, dockerCli *command.DockerCli, config *container.Config, hostConfig *container.HostConfig, networkingConfig *networktypes.NetworkingConfig, cidfile, name string) (*container.ContainerCreateCreatedBody, error) { +func createContainer(ctx context.Context, dockerCli *command.DockerCli, containerConfig *containerConfig, name string) (*container.ContainerCreateCreatedBody, error) { + config := containerConfig.Config + hostConfig := containerConfig.HostConfig + networkingConfig := containerConfig.NetworkingConfig stderr := dockerCli.Err() var ( @@ -155,6 +157,7 @@ func createContainer(ctx context.Context, dockerCli *command.DockerCli, config * namedRef reference.Named ) + cidfile := hostConfig.ContainerIDFile if cidfile != "" { var err error if containerIDFile, err = newCIDFile(cidfile); err != nil { diff --git a/components/cli/command/container/opts.go b/components/cli/command/container/opts.go index 4ce872b556..febddbc5d1 100644 --- a/components/cli/command/container/opts.go +++ b/components/cli/command/container/opts.go @@ -285,10 +285,16 @@ func addFlags(flags *pflag.FlagSet) *containerOptions { return copts } +type containerConfig struct { + Config *container.Config + HostConfig *container.HostConfig + NetworkingConfig *networktypes.NetworkingConfig +} + // parse parses the args for the specified command and generates a Config, // a HostConfig and returns them with the specified command. // If the specified args are not valid, it will return an error. -func parse(flags *pflag.FlagSet, copts *containerOptions) (*container.Config, *container.HostConfig, *networktypes.NetworkingConfig, error) { +func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, error) { var ( attachStdin = copts.attach.Get("stdin") attachStdout = copts.attach.Get("stdout") @@ -298,7 +304,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*container.Config, *c // Validate the input mac address if copts.macAddress != "" { if _, err := opts.ValidateMACAddress(copts.macAddress); err != nil { - return nil, nil, nil, fmt.Errorf("%s is not a valid mac address", copts.macAddress) + return nil, fmt.Errorf("%s is not a valid mac address", copts.macAddress) } } if copts.stdin { @@ -314,7 +320,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*container.Config, *c swappiness := copts.swappiness if swappiness != -1 && (swappiness < 0 || swappiness > 100) { - return nil, nil, nil, fmt.Errorf("invalid value: %d. Valid memory swappiness range is 0-100", swappiness) + return nil, fmt.Errorf("invalid value: %d. Valid memory swappiness range is 0-100", swappiness) } var binds []string @@ -359,13 +365,13 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*container.Config, *c ports, portBindings, err := nat.ParsePortSpecs(copts.publish.GetAll()) if err != nil { - return nil, nil, nil, err + return nil, err } // Merge in exposed ports to the map of published ports for _, e := range copts.expose.GetAll() { if strings.Contains(e, ":") { - return nil, nil, nil, fmt.Errorf("invalid port format for --expose: %s", e) + return nil, fmt.Errorf("invalid port format for --expose: %s", e) } //support two formats for expose, original format /[] or /[] proto, port := nat.SplitProtoPort(e) @@ -373,12 +379,12 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*container.Config, *c //if expose a port, the start and end port are the same start, end, err := nat.ParsePortRange(port) if err != nil { - return nil, nil, nil, fmt.Errorf("invalid range format for --expose: %s, error: %s", e, err) + return nil, fmt.Errorf("invalid range format for --expose: %s, error: %s", e, err) } for i := start; i <= end; i++ { p, err := nat.NewPort(proto, strconv.FormatUint(i, 10)) if err != nil { - return nil, nil, nil, err + return nil, err } if _, exists := ports[p]; !exists { ports[p] = struct{}{} @@ -391,7 +397,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*container.Config, *c for _, device := range copts.devices.GetAll() { deviceMapping, err := parseDevice(device) if err != nil { - return nil, nil, nil, err + return nil, err } deviceMappings = append(deviceMappings, deviceMapping) } @@ -399,53 +405,53 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*container.Config, *c // collect all the environment variables for the container envVariables, err := runconfigopts.ReadKVStrings(copts.envFile.GetAll(), copts.env.GetAll()) if err != nil { - return nil, nil, nil, err + return nil, err } // collect all the labels for the container labels, err := runconfigopts.ReadKVStrings(copts.labelsFile.GetAll(), copts.labels.GetAll()) if err != nil { - return nil, nil, nil, err + return nil, err } ipcMode := container.IpcMode(copts.ipcMode) if !ipcMode.Valid() { - return nil, nil, nil, fmt.Errorf("--ipc: invalid IPC mode") + return nil, fmt.Errorf("--ipc: invalid IPC mode") } pidMode := container.PidMode(copts.pidMode) if !pidMode.Valid() { - return nil, nil, nil, fmt.Errorf("--pid: invalid PID mode") + return nil, fmt.Errorf("--pid: invalid PID mode") } utsMode := container.UTSMode(copts.utsMode) if !utsMode.Valid() { - return nil, nil, nil, fmt.Errorf("--uts: invalid UTS mode") + return nil, fmt.Errorf("--uts: invalid UTS mode") } usernsMode := container.UsernsMode(copts.usernsMode) if !usernsMode.Valid() { - return nil, nil, nil, fmt.Errorf("--userns: invalid USER mode") + return nil, fmt.Errorf("--userns: invalid USER mode") } restartPolicy, err := runconfigopts.ParseRestartPolicy(copts.restartPolicy) if err != nil { - return nil, nil, nil, err + return nil, err } loggingOpts, err := parseLoggingOpts(copts.loggingDriver, copts.loggingOpts.GetAll()) if err != nil { - return nil, nil, nil, err + return nil, err } securityOpts, err := parseSecurityOpts(copts.securityOpt.GetAll()) if err != nil { - return nil, nil, nil, err + return nil, err } storageOpts, err := parseStorageOpts(copts.storageOpt.GetAll()) if err != nil { - return nil, nil, nil, err + return nil, err } // Healthcheck @@ -456,7 +462,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*container.Config, *c copts.healthRetries != 0 if copts.noHealthcheck { if haveHealthSettings { - return nil, nil, nil, fmt.Errorf("--no-healthcheck conflicts with --health-* options") + return nil, fmt.Errorf("--no-healthcheck conflicts with --health-* options") } test := strslice.StrSlice{"NONE"} healthConfig = &container.HealthConfig{Test: test} @@ -467,13 +473,13 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*container.Config, *c probe = strslice.StrSlice(args) } if copts.healthInterval < 0 { - return nil, nil, nil, fmt.Errorf("--health-interval cannot be negative") + return nil, fmt.Errorf("--health-interval cannot be negative") } if copts.healthTimeout < 0 { - return nil, nil, nil, fmt.Errorf("--health-timeout cannot be negative") + return nil, fmt.Errorf("--health-timeout cannot be negative") } if copts.healthRetries < 0 { - return nil, nil, nil, fmt.Errorf("--health-retries cannot be negative") + return nil, fmt.Errorf("--health-retries cannot be negative") } healthConfig = &container.HealthConfig{ @@ -588,7 +594,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*container.Config, *c } if copts.autoRemove && !hostConfig.RestartPolicy.IsNone() { - return nil, nil, nil, fmt.Errorf("Conflicting options: --restart and --rm") + return nil, fmt.Errorf("Conflicting options: --restart and --rm") } // only set this value if the user provided the flag, else it should default to nil @@ -640,7 +646,11 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*container.Config, *c networkingConfig.EndpointsConfig[string(hostConfig.NetworkMode)] = epConfig } - return config, hostConfig, networkingConfig, nil + return &containerConfig{ + Config: config, + HostConfig: hostConfig, + NetworkingConfig: networkingConfig, + }, nil } func parseLoggingOpts(loggingDriver string, loggingOpts []string) (map[string]string, error) { diff --git a/components/cli/command/container/opts_test.go b/components/cli/command/container/opts_test.go index 1448dae8db..3c7753cd00 100644 --- a/components/cli/command/container/opts_test.go +++ b/components/cli/command/container/opts_test.go @@ -51,7 +51,12 @@ func parseRun(args []string) (*container.Config, *container.HostConfig, *network if err := flags.Parse(args); err != nil { return nil, nil, nil, err } - return parse(flags, copts) + // TODO: fix tests to accept ContainerConfig + containerConfig, err := parse(flags, copts) + if err != nil { + return nil, nil, nil, err + } + return containerConfig.Config, containerConfig.HostConfig, containerConfig.NetworkingConfig, err } func parsetest(t *testing.T, args string) (*container.Config, *container.HostConfig, error) { diff --git a/components/cli/command/container/run.go b/components/cli/command/container/run.go index e805ca1a57..fe869f7958 100644 --- a/components/cli/command/container/run.go +++ b/components/cli/command/container/run.go @@ -12,9 +12,9 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" - opttypes "github.com/docker/docker/opts" "github.com/docker/docker/pkg/promise" "github.com/docker/docker/pkg/signal" "github.com/docker/libnetwork/resolvconf/dns" @@ -66,40 +66,44 @@ func NewRunCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions, copts *containerOptions) error { - stdout, stderr, stdin := dockerCli.Out(), dockerCli.Err(), dockerCli.In() - client := dockerCli.Client() - // TODO: pass this as an argument - cmdPath := "run" - - var ( - flAttach *opttypes.ListOpts - ErrConflictAttachDetach = errors.New("Conflicting options: -a and -d") - ) - - config, hostConfig, networkingConfig, err := parse(flags, copts) - - // just in case the parse does not exit - if err != nil { - reportError(stderr, cmdPath, err.Error(), true) - return cli.StatusError{StatusCode: 125} - } - +func warnOnOomKillDisable(hostConfig container.HostConfig, stderr io.Writer) { if hostConfig.OomKillDisable != nil && *hostConfig.OomKillDisable && hostConfig.Memory == 0 { fmt.Fprintln(stderr, "WARNING: Disabling the OOM killer on containers without setting a '-m/--memory' limit may be dangerous.") } +} - if len(hostConfig.DNS) > 0 { - // check the DNS settings passed via --dns against - // localhost regexp to warn if they are trying to - // set a DNS to a localhost address - for _, dnsIP := range hostConfig.DNS { - if dns.IsLocalhost(dnsIP) { - fmt.Fprintf(stderr, "WARNING: Localhost DNS setting (--dns=%s) may fail in containers.\n", dnsIP) - break - } +// check the DNS settings passed via --dns against localhost regexp to warn if +// they are trying to set a DNS to a localhost address +func warnOnLocalhostDNS(hostConfig container.HostConfig, stderr io.Writer) { + for _, dnsIP := range hostConfig.DNS { + if dns.IsLocalhost(dnsIP) { + fmt.Fprintf(stderr, "WARNING: Localhost DNS setting (--dns=%s) may fail in containers.\n", dnsIP) + return } } +} + +func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions, copts *containerOptions) error { + containerConfig, err := parse(flags, copts) + // just in case the parse does not exit + if err != nil { + reportError(dockerCli.Err(), "run", err.Error(), true) + return cli.StatusError{StatusCode: 125} + } + return runContainer(dockerCli, opts, copts, containerConfig) +} + +func runContainer(dockerCli *command.DockerCli, opts *runOptions, copts *containerOptions, containerConfig *containerConfig) error { + config := containerConfig.Config + hostConfig := containerConfig.HostConfig + stdout, stderr := dockerCli.Out(), dockerCli.Err() + client := dockerCli.Client() + + // TODO: pass this as an argument + cmdPath := "run" + + warnOnOomKillDisable(*hostConfig, stderr) + warnOnLocalhostDNS(*hostConfig, stderr) config.ArgsEscaped = false @@ -108,11 +112,8 @@ func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions return err } } else { - if fl := flags.Lookup("attach"); fl != nil { - flAttach = fl.Value.(*opttypes.ListOpts) - if flAttach.Len() != 0 { - return ErrConflictAttachDetach - } + if copts.attach.Len() != 0 { + return errors.New("Conflicting options: -a and -d") } config.AttachStdin = false @@ -135,7 +136,7 @@ func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions ctx, cancelFun := context.WithCancel(context.Background()) - createResponse, err := createContainer(ctx, dockerCli, config, hostConfig, networkingConfig, hostConfig.ContainerIDFile, opts.name) + createResponse, err := createContainer(ctx, dockerCli, containerConfig, opts.name) if err != nil { reportError(stderr, cmdPath, err.Error(), true) return runStartContainerErr(err) @@ -158,51 +159,15 @@ func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions } attach := config.AttachStdin || config.AttachStdout || config.AttachStderr if attach { - var ( - out, cerr io.Writer - in io.ReadCloser - ) - if config.AttachStdin { - in = stdin - } - if config.AttachStdout { - out = stdout - } - if config.AttachStderr { - if config.Tty { - cerr = stdout - } else { - cerr = stderr - } - } - if opts.detachKeys != "" { dockerCli.ConfigFile().DetachKeys = opts.detachKeys } - options := types.ContainerAttachOptions{ - Stream: true, - Stdin: config.AttachStdin, - Stdout: config.AttachStdout, - Stderr: config.AttachStderr, - DetachKeys: dockerCli.ConfigFile().DetachKeys, + close, err := attachContainer(ctx, dockerCli, &errCh, config, createResponse.ID) + defer close() + if err != nil { + return err } - - resp, errAttach := client.ContainerAttach(ctx, createResponse.ID, options) - if errAttach != nil && errAttach != httputil.ErrPersistEOF { - // ContainerAttach returns an ErrPersistEOF (connection closed) - // means server met an error and put it in Hijacked connection - // keep the error and read detailed error message from hijacked connection later - return errAttach - } - defer resp.Close() - - errCh = promise.Go(func() error { - if errHijack := holdHijackedConnection(ctx, dockerCli, config.Tty, in, out, cerr, resp); errHijack != nil { - return errHijack - } - return errAttach - }) } statusChan := waitExitOrRemoved(ctx, dockerCli, createResponse.ID, copts.autoRemove) @@ -252,6 +217,57 @@ func runRun(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *runOptions return nil } +func attachContainer( + ctx context.Context, + dockerCli *command.DockerCli, + errCh *chan error, + config *container.Config, + containerID string, +) (func(), error) { + stdout, stderr := dockerCli.Out(), dockerCli.Err() + var ( + out, cerr io.Writer + in io.ReadCloser + ) + if config.AttachStdin { + in = dockerCli.In() + } + if config.AttachStdout { + out = stdout + } + if config.AttachStderr { + if config.Tty { + cerr = stdout + } else { + cerr = stderr + } + } + + options := types.ContainerAttachOptions{ + Stream: true, + Stdin: config.AttachStdin, + Stdout: config.AttachStdout, + Stderr: config.AttachStderr, + DetachKeys: dockerCli.ConfigFile().DetachKeys, + } + + resp, errAttach := dockerCli.Client().ContainerAttach(ctx, containerID, options) + if errAttach != nil && errAttach != httputil.ErrPersistEOF { + // ContainerAttach returns an ErrPersistEOF (connection closed) + // means server met an error and put it in Hijacked connection + // keep the error and read detailed error message from hijacked connection later + return nil, errAttach + } + + *errCh = promise.Go(func() error { + if errHijack := holdHijackedConnection(ctx, dockerCli, config.Tty, in, out, cerr, resp); errHijack != nil { + return errHijack + } + return errAttach + }) + return resp.Close, nil +} + // reportError is a utility method that prints a user-friendly message // containing the error that occurred during parsing and a suggestion to get help func reportError(stderr io.Writer, name string, str string, withHelp bool) { From 6f96d531a0e10b804a1a3ce9766ff989af3bec59 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 13 Mar 2017 15:05:30 -0400 Subject: [PATCH 656/978] Add compose file version 3.2 Signed-off-by: Daniel Nephin Upstream-commit: 2e9b15143a6373d2882c05be88bd14992730814d Component: cli --- components/cli/compose/loader/loader_test.go | 18 +- components/cli/compose/schema/bindata.go | 25 +- .../schema/data/config_schema_v3.1.json | 50 +- .../schema/data/config_schema_v3.2.json | 473 ++++++++++++++++++ 4 files changed, 512 insertions(+), 54 deletions(-) create mode 100644 components/cli/compose/schema/data/config_schema_v3.2.json diff --git a/components/cli/compose/loader/loader_test.go b/components/cli/compose/loader/loader_test.go index 126832a3b2..dba87e5a59 100644 --- a/components/cli/compose/loader/loader_test.go +++ b/components/cli/compose/loader/loader_test.go @@ -933,7 +933,7 @@ func (sbn servicesByName) Less(i, j int) bool { return sbn[i].Name < sbn[j].Name func TestLoadAttachableNetwork(t *testing.T) { config, err := loadYAML(` -version: "3.1" +version: "3.2" networks: mynet1: driver: overlay @@ -941,7 +941,9 @@ networks: mynet2: driver: bridge `) - assert.NoError(t, err) + if !assert.NoError(t, err) { + return + } expected := map[string]types.NetworkConfig{ "mynet1": { @@ -959,7 +961,7 @@ networks: func TestLoadExpandedPortFormat(t *testing.T) { config, err := loadYAML(` -version: "3.1" +version: "3.2" services: web: image: busybox @@ -975,7 +977,9 @@ services: target: 22 published: 10022 `) - assert.NoError(t, err) + if !assert.NoError(t, err) { + return + } expected := []types.ServicePortConfig{ { @@ -1044,7 +1048,7 @@ services: func TestLoadExpandedMountFormat(t *testing.T) { config, err := loadYAML(` -version: "3.1" +version: "3.2" services: web: image: busybox @@ -1056,7 +1060,9 @@ services: volumes: foo: {} `) - assert.NoError(t, err) + if !assert.NoError(t, err) { + return + } expected := types.ServiceVolumeConfig{ Type: "volume", diff --git a/components/cli/compose/schema/bindata.go b/components/cli/compose/schema/bindata.go index e4ef29bc72..8857e36a85 100644 --- a/components/cli/compose/schema/bindata.go +++ b/components/cli/compose/schema/bindata.go @@ -2,6 +2,7 @@ // sources: // data/config_schema_v3.0.json // data/config_schema_v3.1.json +// data/config_schema_v3.2.json // DO NOT EDIT! package schema @@ -89,7 +90,7 @@ func dataConfig_schema_v30Json() (*asset, error) { return a, nil } -var _dataConfig_schema_v31Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x5b\xcd\x73\xdc\x28\x16\xbf\xf7\x5f\xa1\x52\x72\x8b\x3f\xb2\xb5\xa9\xad\xda\xdc\xf6\xb8\xa7\x99\xf3\xb8\x3a\x2a\x5a\x7a\xad\x26\x46\x40\x00\xb5\xdd\x49\xf9\x7f\x9f\xd2\x67\x03\x02\x81\xba\xe5\x38\x33\x35\x27\xdb\xe2\xf7\x80\xf7\xfd\x1e\xe0\x1f\x9b\x24\x49\xdf\xcb\xfc\x00\x15\x4a\x3f\x27\xe9\x41\x29\xfe\xf9\xfe\xfe\xab\x64\xf4\xb6\xfb\x7a\xc7\x44\x79\x5f\x08\xb4\x57\xb7\x1f\x3f\xdd\x77\xdf\xde\xa5\x37\x0d\x1d\x2e\x1a\x92\x9c\xd1\x3d\x2e\xb3\x6e\x24\x3b\xfe\xfb\xee\x5f\x77\x0d\x79\x07\x51\x27\x0e\x0d\x88\xed\xbe\x42\xae\xba\x6f\x02\xbe\xd5\x58\x40\x43\xfc\x90\x1e\x41\x48\xcc\x68\xba\xbd\xd9\x34\x63\x5c\x30\x0e\x42\x61\x90\xe9\xe7\xa4\xd9\x5c\x92\x8c\x90\xe1\x83\x36\xad\x54\x02\xd3\x32\x6d\x3f\xbf\xb4\x33\x24\x49\x2a\x41\x1c\x71\xae\xcd\x30\x6e\xf5\xdd\xfd\x79\xfe\xfb\x11\x76\x63\xcf\xaa\x6d\xb6\xfd\xce\x91\x52\x20\xe8\xef\xd3\xbd\xb5\xc3\x5f\x1e\xd0\xed\xf7\xff\xdd\xfe\xf1\xf1\xf6\xbf\x77\xd9\xed\xf6\xc3\x7b\x63\xb8\x91\xaf\x80\x7d\xb7\x7c\x01\x7b\x4c\xb1\xc2\x8c\x8e\xeb\xa7\x23\xf2\xa5\xff\xed\x65\x5c\x18\x15\x45\x0b\x46\xc4\x58\x7b\x8f\x88\x04\x93\x67\x0a\xea\x89\x89\xc7\x10\xcf\x23\xec\x8d\x78\xee\xd7\x77\xf0\x6c\xb2\x73\x64\xa4\xae\x82\x1a\x1c\x50\x6f\xc4\x4c\xb7\xfc\x3a\xfa\x93\x90\x0b\x50\x61\x93\xed\x50\x6f\x66\xb1\xcd\xf2\xd7\x31\xbc\x19\x98\x9e\xc5\x76\x08\x6d\xed\x76\x83\x86\x7b\xbb\x44\xe5\x72\x2f\xbf\xac\x46\x61\x79\xa4\x54\x00\x27\xec\xd4\x7c\xf3\xc8\xa3\x03\x54\x40\x55\x3a\x8a\x20\x49\xd2\x5d\x8d\x49\x61\x4b\x94\x51\xf8\xad\x99\xe2\x41\xfb\x98\x24\x3f\xec\x48\xa6\xcd\xd3\x8e\x1b\x7f\xf9\x15\x3e\x8e\x7b\x78\x19\xc7\x73\x46\x15\x3c\xab\x96\xa9\xf9\xa5\x3b\x11\xb0\xfc\x11\xc4\x1e\x13\x88\xa5\x40\xa2\x94\x33\x22\x23\x58\xaa\x8c\x89\xac\xc0\xb9\x4a\x5f\x2c\xf2\xc9\x7c\x61\x7b\x1a\x49\xb5\xbf\xb6\x1b\xc7\x84\x69\x8e\x78\x86\x8a\xc2\xe0\x03\x09\x81\x4e\xe9\x4d\x92\x62\x05\x95\x74\xb3\x98\xa4\x35\xc5\xdf\x6a\xf8\x7f\x0f\x51\xa2\x06\x7b\xde\x42\x30\xbe\xfe\xc4\xa5\x60\x35\xcf\x38\x12\x8d\x81\xcd\x8b\x3f\xcd\x59\x55\x21\xba\x96\xd5\x2d\xe1\x23\x42\xf2\x8c\x2a\x84\x29\x88\x8c\xa2\x2a\x64\x48\x8d\xd7\x01\x2d\x64\xd6\x25\xfc\x59\x33\xda\x67\x1d\xbd\xb4\x26\x18\xb3\xff\xaa\xfa\x28\xe8\x9c\x61\x77\xd3\x34\xa6\xdd\xec\x2d\xb5\x08\x33\x09\x48\xe4\x87\x0b\xe9\x59\x85\x30\x8d\x91\x1d\x50\x25\x4e\x9c\xe1\xce\x5e\x7e\x39\x43\x00\x7a\xcc\xc6\x58\xb2\x58\x0c\x40\x8f\x58\x30\x5a\x0d\xde\x10\x13\x60\xc6\x20\xdf\xd0\x3f\x73\x26\xc1\x16\x8c\xc5\xa0\x3e\x34\xb2\x6a\xc8\x64\xa0\x78\x18\x18\xbf\x49\x52\x5a\x57\x3b\x10\x4d\x0d\x6b\x20\xf7\x4c\x54\xa8\xd9\xec\xb0\xb6\x36\x6c\x48\xda\x61\x79\xba\x00\x75\x1e\x9a\xb4\x8e\x48\x46\x30\x7d\x5c\xdf\xc4\xe1\x59\x09\x94\x1d\x98\x54\xf1\x31\x5c\x23\x3f\x00\x22\xea\x90\x1f\x20\x7f\x9c\x21\xd7\x51\x06\x35\x93\x2a\xc6\xc8\x71\x85\xca\x30\x88\xe7\x21\x08\x41\x3b\x20\x17\xf1\xb9\xaa\xf0\xb5\x69\x59\x59\x36\x50\x9f\xc5\x4d\x2a\x97\x7e\x38\x94\xf3\x0b\x81\x8f\x20\x62\x13\x38\xe3\xe7\x82\xcb\x1e\x0c\x17\x20\x49\xb8\xfa\x34\xa0\x5f\xee\xba\xe2\x73\xc6\xab\xda\xdf\x08\x49\xb7\x76\xb9\x90\x58\x79\xdf\xf5\xc5\xe2\x30\xae\xa0\x30\xb4\x52\xa1\xbc\xa9\x1b\x04\x48\x8f\x5e\xcf\xd0\xbe\xbb\xc9\x2a\x56\xf8\x0c\x74\x02\xb6\x65\xe3\x8d\xd4\x8b\x13\x61\x72\x51\xfd\x18\xa5\xba\x60\x03\x11\xe0\xc6\xb7\xbd\xd8\x6d\x9e\xb7\x1b\x36\xb1\x16\x87\x08\x46\x12\xc2\xce\xee\x15\xa4\x31\x1b\xe6\xc7\x4f\x91\x36\xe1\xa2\xfd\xcf\x2c\xad\x87\xd4\x3b\x67\x7c\x8d\x1c\x98\xea\xbc\x95\xd6\xdd\x5c\x1b\xd9\x06\xbc\xed\x95\x4b\x78\x8e\x0b\x7f\xac\x68\x23\x84\xee\x60\x9c\x09\x35\xf1\xae\xe5\xe9\xde\x67\xc1\xba\xb8\x86\x38\x75\x4e\xf8\xdd\xe2\x13\x69\x4c\xd4\x1d\x45\x34\xf5\xbf\xa0\x7f\x84\x3d\x23\x9d\x89\x52\x0e\xb4\x42\xa2\x04\xb3\x0d\xc1\x54\x41\x09\xc2\x43\xc0\xeb\x1d\xc1\xf2\x00\xc5\x12\x1a\xc1\x14\xcb\x19\x89\x73\x0c\x67\xfb\x19\xef\x0c\xe6\x84\xdb\xab\x6b\x33\x2e\xf0\x11\x13\x28\x2d\x8e\x77\x8c\x11\x40\xd4\x48\x14\x02\x50\x91\x31\x4a\x4e\x11\x48\xa9\x90\x08\xb6\x7f\x12\xf2\x5a\x60\x75\xca\x18\x57\xab\x57\x85\xf2\x50\x65\x12\x7f\x07\xd3\xf7\xce\x56\xdf\x4f\xb4\xb5\x36\x64\x9d\x67\x25\xaf\xe5\x7e\x3e\xb3\x7d\x25\xb7\x91\xac\x16\xf9\x75\x8e\x33\x8b\xaf\xcd\x20\x37\x0f\x2e\x97\x80\x27\x0e\xdf\xab\x30\x54\x43\xcd\xba\x8a\x33\x50\xcb\x93\xcc\xd5\x65\xb5\xb5\x54\x05\xa6\x19\xe3\x40\x83\xbe\x21\x15\xe3\x59\x29\x50\x0e\x19\x07\x81\x99\x53\x14\x46\x80\x2d\x6a\x81\x9a\xf5\xa7\xd3\x48\x5c\x52\xe4\x8e\x3b\x1a\x54\x55\x7c\x7f\xe1\x21\x80\x52\x61\x67\xaf\x09\xae\xb0\xdf\x69\x1c\x56\x1b\x51\xaf\x75\xb5\x9a\xbb\x44\x9b\x29\xcf\xa2\x42\xf6\x4c\x87\x30\xdf\x20\x44\x74\x06\x07\x24\x16\xa4\x8e\xd6\x31\xf7\x9e\xfc\xe4\xea\x1b\x9c\xfb\x32\x6e\xa6\xda\xf9\x6e\xfa\x8d\x6c\x9d\xf8\x45\xa5\x97\xbd\x8d\xad\xb7\xfa\x71\x3b\x55\x2d\x83\x4d\x5c\x8b\xa1\x72\xae\x01\x19\xa1\xd3\x2b\x96\xe4\x2f\x11\xa1\x0d\x1d\xb5\x70\x87\x6e\x22\xe2\x78\xbf\x52\x64\xec\x7c\xed\xa8\x1f\x5d\x11\x68\x34\x3b\x3c\x39\xf0\x5d\x22\xc9\x38\x39\x8d\x28\x54\x76\xa1\x33\xba\x67\x89\x77\xbb\xfe\x22\xed\xa7\xb0\x42\x59\xce\xb8\x47\xca\xf1\x6c\x2c\xcd\x98\xd6\x29\xc4\x4c\x49\xe9\xf3\xfe\x27\x26\x1e\x9b\xdc\x52\x60\x77\x10\xd8\x58\x24\x0b\xee\x1e\xad\x63\xbb\x61\x02\xd7\xa5\x9a\x0e\x0d\x5e\x42\xce\x5f\xf0\xf5\x20\xef\xe5\x1b\x96\x68\x67\x5d\x3b\xb9\x72\x66\x13\xe4\xc5\x31\x9c\xba\x05\x28\x81\xad\x5b\x81\xa1\xfe\xd1\xd3\x34\xc8\x5f\xf3\xec\x5c\xe1\x0a\x58\xed\x8e\x28\x1b\xdd\x70\x7a\xa2\x54\xbb\x9c\x0c\x28\x55\x43\xda\x3a\x7d\x18\x95\x3a\xb4\xd8\x41\xc5\xc5\xe4\x1e\x01\x9c\xe0\x1c\xc9\x50\x7e\xbf\xe2\x8c\xb6\xe6\x05\x52\x90\x75\x8f\x53\x16\x55\x54\x33\xa5\x14\x47\x02\x11\x02\x04\xcb\x2a\xa6\x34\x49\x0b\x20\xe8\x74\x51\x55\xda\x92\xef\x11\x26\xb5\x80\x0c\xe5\xde\xc8\x6b\x51\x54\x8c\x62\xc5\x9c\x11\x22\x6e\xc9\x0a\x3d\x67\xc3\xb2\x2d\x24\xd4\x30\x98\xbd\x72\xec\xf1\xaa\x66\x09\x5d\x66\x5d\x56\xf4\xce\xa8\xe8\x5c\x42\x7b\x2c\x66\x58\x71\xc2\xba\x00\xd9\x44\x92\xf1\xf4\x3b\x48\x1f\x8c\xd9\x7d\xf3\x9e\x71\x46\x70\x7e\x5a\x8b\xc3\x9c\xd1\x4e\xc8\x31\x06\x71\xa5\x05\x36\xe6\xd0\x74\x18\x15\x57\x41\x67\x6d\x09\x9e\x30\x2d\xd8\xd3\x82\x05\xd7\x33\x25\x4e\x50\x0e\x56\xbc\xbb\x56\xd0\x52\x09\x84\xa9\x5a\x7c\xd9\x73\x2d\x5b\x57\x64\xf3\xd1\x3e\x03\x51\x7f\xc4\x05\xf3\xb8\x2f\xd2\xe7\xbc\x0e\x5e\x89\x54\x50\x31\xe1\x34\xc0\x15\x5e\xbb\x85\x58\x1c\x60\x2b\x64\xb5\xa8\x3b\xb4\x1e\x95\x31\xbe\x7e\x13\x1f\xbe\x27\xdb\x86\x03\x12\xe6\xa8\x5a\xcb\x3b\xa2\x6f\x15\x53\x67\x0e\x4e\xe6\x9b\xcd\xc4\xdf\x70\x86\x76\x1d\xde\x7b\x8f\x90\xf5\x8e\x7a\x7a\xb4\x69\x7d\xbf\xe6\x59\xf3\x8a\x41\x6f\x78\x10\xe0\xd1\xea\xc3\x58\x33\xdf\x8c\xb2\xda\x46\xab\xd8\x7b\x1b\xbf\xde\xfe\xdb\xf2\xdd\x3e\x79\x73\xd5\xf9\x48\x29\x94\x1f\xa2\x5a\x82\x85\x45\xe3\x15\x71\x68\xd2\xb8\x3a\xc3\x50\x8f\xfa\x27\x0a\xfd\x4d\x6c\xf6\xe7\xd9\x57\xff\xe0\x36\xf8\xd2\xb5\x45\x5d\x9c\xc7\x23\x9e\x77\xfe\x02\x3a\x7b\x6b\x55\x98\x47\xfb\x9a\x4a\xa6\xc7\x03\x73\x92\x8c\x7e\x7f\xd0\x53\x6c\xcd\x6d\xd8\x30\xc7\xff\x44\x98\xc9\x74\xee\xe2\x6f\x80\x78\x8e\xa3\xac\x45\x7b\x21\xce\x73\xbe\x62\xb0\xb9\xfb\x30\x53\x32\xcc\xbd\x13\x7a\xa5\x5c\xbb\xc2\xa5\xaa\x5b\xa7\x56\x9f\x31\x48\x77\xfa\xce\xdd\xe3\xff\x1a\xfd\xe4\xd5\x7b\xc3\x27\x3d\x4d\x8e\xaf\x7e\x98\xc7\xe8\xdd\x8b\xf5\xad\x21\x1f\x0b\xd2\xbd\xba\xd3\xa2\xfb\x56\x6f\xbd\x7c\x6a\x74\xbe\x85\xb7\x0f\xf1\x87\x37\xe9\x9e\x7b\xc5\x8d\xfe\xb3\xfd\xff\x81\xcd\xcb\xe6\xcf\x00\x00\x00\xff\xff\xea\x87\x24\xae\xb8\x34\x00\x00") +var _dataConfig_schema_v31Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x1a\xcb\x8e\xdb\x36\xf0\xee\xaf\x10\x94\xdc\xe2\xdd\x4d\xd1\xa0\x40\x73\xeb\xb1\xa7\xf6\xdc\x85\x23\xd0\xd2\x58\x66\x96\x22\x19\x92\x72\xd6\x09\xfc\xef\x05\xf5\x32\x45\x91\x22\x6d\x2b\xd9\x45\xd1\xd3\xae\xc5\x99\xe1\xbc\x67\x38\xe4\xf7\x55\x92\xa4\x6f\x65\xbe\x87\x0a\xa5\x1f\x93\x74\xaf\x14\xff\xf8\xf0\xf0\x59\x32\x7a\xd7\x7e\xbd\x67\xa2\x7c\x28\x04\xda\xa9\xbb\xf7\x1f\x1e\xda\x6f\x6f\xd2\xb5\xc6\xc3\x85\x46\xc9\x19\xdd\xe1\x32\x6b\x57\xb2\xc3\xaf\xf7\xbf\xdc\x6b\xf4\x16\x44\x1d\x39\x68\x20\xb6\xfd\x0c\xb9\x6a\xbf\x09\xf8\x52\x63\x01\x1a\xf9\x31\x3d\x80\x90\x98\xd1\x74\xb3\x5e\xe9\x35\x2e\x18\x07\xa1\x30\xc8\xf4\x63\xa2\x99\x4b\x92\x01\xa4\xff\x60\x90\x95\x4a\x60\x5a\xa6\xcd\xe7\x53\x43\x21\x49\x52\x09\xe2\x80\x73\x83\xc2\xc0\xea\x9b\x87\x33\xfd\x87\x01\x6c\x6d\x53\x35\x98\x6d\xbe\x73\xa4\x14\x08\xfa\xf7\x94\xb7\x66\xf9\xd3\x23\xba\xfb\xf6\xc7\xdd\x3f\xef\xef\x7e\xbf\xcf\xee\x36\xef\xde\x8e\x96\xb5\x7e\x05\xec\xda\xed\x0b\xd8\x61\x8a\x15\x66\x74\xd8\x3f\x1d\x20\x4f\xdd\x7f\xa7\x61\x63\x54\x14\x0d\x30\x22\xa3\xbd\x77\x88\x48\x18\xcb\x4c\x41\x7d\x65\xe2\x29\x24\xf3\x00\xf6\x42\x32\x77\xfb\x3b\x64\x1e\x8b\x73\x60\xa4\xae\x82\x16\xec\xa1\x5e\x48\x98\x76\xfb\x65\xec\x27\x21\x17\xa0\xc2\x2e\xdb\x42\xbd\x98\xc7\xea\xed\x6f\x13\x78\xd5\x0b\x3d\x0b\xdb\x42\x18\x7b\x37\x0c\x8e\xc2\xdb\xa5\x2a\x57\x78\xf9\x75\x35\x28\xcb\xa3\xa5\x02\x38\x61\x47\xfd\xcd\xa3\x8f\x16\xa0\x02\xaa\xd2\x41\x05\x49\x92\x6e\x6b\x4c\x0a\x5b\xa3\x8c\xc2\x5f\x9a\xc4\xa3\xf1\x31\x49\xbe\xdb\x99\xcc\xa0\xd3\xac\x8f\x7e\xf9\x0d\x3e\xac\x7b\x64\x19\xd6\x73\x46\x15\x3c\xab\x46\xa8\xf9\xad\x5b\x15\xb0\xfc\x09\xc4\x0e\x13\x88\xc5\x40\xa2\x94\x33\x2a\x23\x58\xaa\x8c\x89\xac\xc0\xb9\x4a\x4f\x16\xfa\x84\x5e\xd8\x9f\x06\x54\xe3\xd7\x66\xe5\x20\x98\xe6\x88\x67\xa8\x28\x46\x72\x20\x21\xd0\x31\x5d\x27\x29\x56\x50\x49\xb7\x88\x49\x5a\x53\xfc\xa5\x86\x3f\x3b\x10\x25\x6a\xb0\xe9\x16\x82\xf1\xe5\x09\x97\x82\xd5\x3c\xe3\x48\x68\x07\x9b\x57\x7f\x9a\xb3\xaa\x42\x74\x29\xaf\xbb\x44\x8e\x08\xcd\x33\xaa\x10\xa6\x20\x32\x8a\xaa\x90\x23\xe9\xa8\x03\x5a\xc8\xac\x2d\xf8\xb3\x6e\xb4\xcb\x5a\x7c\x69\x11\x18\xaa\xff\xa2\xf6\x28\xe8\x9c\x63\xb7\x64\xb4\x6b\x6b\xde\x52\x0b\x31\x93\x80\x44\xbe\xbf\x12\x9f\x55\x08\xd3\x18\xdd\x01\x55\xe2\xc8\x19\x6e\xfd\xe5\xd5\x39\x02\xd0\x43\x36\xe4\x92\x8b\xd5\x00\xf4\x80\x05\xa3\x55\x1f\x0d\x31\x09\x66\x48\xf2\x1a\xff\x99\x33\x09\xb6\x62\x2c\x01\xcd\xa5\x41\xd4\x91\x4e\x7a\x8c\xc7\x5e\xf0\x75\x92\xd2\xba\xda\x82\xd0\x3d\xec\x08\x72\xc7\x44\x85\x34\xb3\xfd\xde\xc6\xf2\x48\xd3\x0e\xcf\x33\x15\x68\xca\xa0\xcb\x3a\x22\x19\xc1\xf4\x69\x79\x17\x87\x67\x25\x50\xb6\x67\x52\xc5\xe7\x70\x03\x7d\x0f\x88\xa8\x7d\xbe\x87\xfc\x69\x06\xdd\x84\x1a\x61\x33\xa9\x62\x9c\x1c\x57\xa8\x0c\x03\xf1\x3c\x04\x42\xd0\x16\xc8\x55\x72\x2e\xaa\x7c\x83\x2c\x2b\x4b\x0d\xea\xf3\xb8\x49\xe7\xd2\x2d\x87\x6a\x7e\x21\xf0\x01\x44\x6c\x01\x67\xfc\xdc\x70\xd9\x8b\xe1\x06\x24\x09\x77\x9f\x23\xd0\x4f\xf7\x6d\xf3\x39\x13\x55\xcd\x7f\x84\xa4\x1b\xbb\x5d\x48\xac\xba\xef\xfa\x62\x49\x18\xd7\x50\x8c\xac\x52\xa1\x5c\xf7\x0d\x02\xa4\xc7\xae\x67\xd0\xee\x74\x93\x55\xac\xf0\x39\xe8\x04\xd8\xd6\x8d\x37\x53\x5f\x5c\x08\x93\xab\xfa\xc7\x28\xd3\x05\x0f\x10\x01\x69\x7c\xec\xc5\xb2\x79\x66\x37\xec\x62\x0d\x1c\x22\x18\x49\x08\x07\xbb\x57\x91\x23\x6a\x98\x1f\x3e\x44\xfa\x84\x0b\xf7\xb7\x59\x5c\x0f\xaa\x97\x66\x7c\x8f\x1c\x20\x75\x66\xa5\x09\x37\x17\x23\x9b\x40\xb4\xfd\xe0\x16\x9e\xe3\xc2\x9f\x2b\x9a\x0c\x61\x06\x18\x67\x42\x4d\xa2\xeb\xe7\x94\xfb\x76\xeb\x9b\xab\x3d\x17\xf8\x80\x09\x94\x30\x3e\xb5\x6c\x19\x23\x80\xe8\x28\xf5\x08\x40\x45\xc6\x28\x39\x46\x40\x4a\x85\x44\xf0\x40\x21\x21\xaf\x05\x56\xc7\x8c\x71\xb5\x78\x9f\x21\xf7\x55\x26\xf1\x37\x18\x5b\xf3\x9c\xef\x3b\x42\x1b\x8b\x21\x6b\x42\x72\xa5\x41\x7d\x29\x29\x1c\xc6\x8e\x44\x18\x4c\x54\xe1\x14\x95\x4a\x56\x8b\x3c\xf6\x80\xad\xf7\x44\xa2\x84\xd8\x23\xbc\x76\xb7\x71\xd8\xcc\x03\x97\x97\x00\x4f\x0a\x5d\x67\xc2\x50\x55\xb6\x7f\x9b\x79\xe5\xe4\x0c\x7d\x79\x94\xb9\xba\xae\x5b\x93\xaa\xc0\x34\x63\x1c\x68\x30\x36\xa4\x62\x3c\x2b\x05\xca\x21\xe3\x20\x30\x73\xaa\x62\x6d\x46\x7a\x51\x0b\xa4\xf7\x9f\x92\x91\xb8\xa4\x88\x84\xc2\x4c\x55\x7c\x77\xe5\xb1\x52\xa9\x70\xb0\xd7\x04\x57\xd8\x1f\x34\x0e\xaf\x8d\xe8\x00\xda\xea\xef\x2e\xfa\x33\x05\xff\xcc\x29\xa6\x0a\x4a\xed\x26\x53\xa7\x9a\xe9\x39\xe7\x5b\xce\x88\x5e\x73\x8f\xc4\xd8\xa0\x33\x7c\x24\x6d\x60\xee\x94\x1b\xc1\xd5\x89\x3a\xf9\x1a\xdd\x75\x34\xf4\xd6\x1d\x23\x1b\x27\xfc\x45\xc5\xdc\x66\x63\xe3\xad\xa7\xee\xa0\xaa\x65\xf0\x58\xd0\xc0\x50\x39\xd7\xd2\x0e\xa0\xc6\xd0\x7e\xd1\x6a\xa1\xdb\x64\x1d\x04\x05\x76\x73\xbb\xb2\x24\xbb\x60\xec\x6e\x9d\x58\x7b\x02\xae\x79\xb2\x09\x1a\x9c\xbf\xcf\xcf\xb6\x3b\x20\xef\xdc\x19\x4b\xb4\xb5\x26\xae\xae\xe0\xd6\xde\x28\x0e\xe1\x1c\x23\x40\x09\x6c\xd9\xa5\x4f\xd4\x66\x3e\x01\xf9\x3a\xc7\x46\x0a\x57\xc0\x6a\x77\xc1\x5b\x99\xfe\xdd\x21\xa5\xc6\x5c\x3e\x60\x54\x03\xd2\xb6\xe9\xe3\x60\xd4\xbe\xbb\x0c\x1a\x2e\x26\x48\x04\x70\x82\x73\x24\x43\x89\xe8\x86\xf1\x44\xcd\x0b\xa4\x20\x6b\xef\x65\x2f\x4a\xfd\x33\x39\x9f\x23\x81\x08\x01\x82\x65\x15\x93\x43\xd3\x02\x08\x3a\x5e\x55\x3e\x1b\xf4\x1d\xc2\xa4\x16\x90\xa1\x5c\x75\x57\xbf\x01\x9f\x4b\x2b\x46\xb1\x62\xce\x0c\x11\xb7\x65\x85\x9e\xb3\x7e\xdb\x06\x24\xd4\xd9\x8c\x9b\xfa\xd8\xc9\x82\xe1\x09\x6d\xe3\x77\x59\x75\x9e\x31\xd1\xb9\xd6\x7b\x3c\xa6\xdf\x71\x22\xba\x00\xa9\x33\xc9\x30\xf8\x09\xe2\x07\x4b\x4b\x77\xca\xc8\x38\x23\x38\x3f\x2e\x25\x61\xce\x68\xab\xe4\x18\x87\xb8\xd1\x03\xb5\x3b\xe8\x56\xa8\xe2\x2a\x18\xac\x0d\xc2\x57\x4c\x0b\xf6\xf5\x82\x0d\x97\x73\x25\x4e\x50\x0e\x56\xbe\xbb\x55\xd1\x52\x09\x84\xa9\xba\xb8\x9c\xdf\x2a\xd6\x0d\xd5\x7c\xf0\xcf\x40\xd6\x1f\xe0\xc2\xf7\xe8\x9e\x4c\x9f\xf3\x3a\x38\x0d\xac\xa0\x62\xc2\xe9\x80\x0b\x3c\xf4\x08\x89\xd8\x83\x2d\x50\xd5\xa2\xc6\xc7\x1d\x54\xc6\xf8\xf2\xa7\x8d\xf0\x88\x78\x13\x4e\x48\x98\xa3\x6a\xa9\xe8\x88\x1e\xa8\xa7\xce\x1a\x9c\xcc\xcf\x2d\x12\xff\xec\x22\xc4\x75\x98\xf7\x0e\x42\xd6\x5b\xea\x19\x21\x4c\x4f\x19\xae\x5b\xfe\xf8\x63\xca\xc9\x7f\x28\xb9\x2d\xe9\xf5\x77\x61\x1e\xab\x3e\x0e\x3d\xf3\x7a\xd0\xd5\x26\xda\xc4\xde\x8b\xa8\xe5\xf8\x6f\xda\x77\x7b\x44\xe0\xea\xf3\x2f\xec\x04\x6f\x48\x2e\xdd\x8b\xa6\x40\x6e\xe9\xa0\xfe\x4f\x2d\xff\x11\x47\xfc\x79\xfe\xd5\x3d\x20\x0b\xbe\xdc\x6a\xa0\xae\x2e\xce\x11\xcf\x95\x5e\x81\xcd\x5e\xda\x14\xe3\xc1\xa2\x61\x92\xe9\x99\x7f\x4e\x93\xd1\xf7\x69\x1d\xc6\x66\xcc\x86\x0d\xe6\x78\xe3\x3b\xae\x90\x73\x83\xa4\x1e\xc4\x73\xbf\x62\x6d\xda\x29\x71\x5e\xf2\x05\x93\xcd\xfd\xbb\x99\x3e\x60\xee\xde\xfb\x07\x15\xd0\x05\x86\x74\x6e\x9b\x5a\x87\x87\x5e\xbb\xd3\x77\x9b\x9e\xf8\x37\xf0\x27\xaf\x38\xb5\x9c\xf4\x38\x99\x49\x7d\x1f\x0f\x5a\xdb\x17\x98\x9b\x91\x7e\x2c\x90\xf6\x15\x89\x91\xdd\x37\xe6\x79\xca\x67\x46\xe7\xdb\x4e\x7b\xcc\xdb\xbf\xb1\xf4\xdc\x6a\xac\xcc\xbf\xcd\x7b\xd8\xd5\x69\xf5\x6f\x00\x00\x00\xff\xff\xfc\xf3\x11\x6a\x88\x2f\x00\x00") func dataConfig_schema_v31JsonBytes() ([]byte, error) { return bindataRead( @@ -109,6 +110,26 @@ func dataConfig_schema_v31Json() (*asset, error) { return a, nil } +var _dataConfig_schema_v32Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x5b\xcd\x73\xdc\x28\x16\xbf\xf7\x5f\xa1\x52\x72\x8b\x3f\xb2\xb5\xa9\xad\xda\xdc\xf6\xb8\xa7\x99\xf3\xb8\x3a\x2a\x1a\xbd\x56\x13\x4b\x40\x00\xb5\xdd\x49\xf9\x7f\x9f\xd2\x67\x03\x02\x81\xba\xe5\x38\x33\x35\x27\xdb\xe2\xf7\x80\xf7\xfd\x1e\xe0\x1f\x9b\x24\x49\xdf\x4b\x7c\x80\x0a\xa5\x9f\x93\xf4\xa0\x14\xff\x7c\x7f\xff\x55\x32\x7a\xdb\x7d\xbd\x63\xa2\xb8\xcf\x05\xda\xab\xdb\x8f\x9f\xee\xbb\x6f\xef\xd2\x9b\x86\x8e\xe4\x0d\x09\x66\x74\x4f\x8a\xac\x1b\xc9\x8e\xff\xbe\xfb\xd7\x5d\x43\xde\x41\xd4\x89\x43\x03\x62\xbb\xaf\x80\x55\xf7\x4d\xc0\xb7\x9a\x08\x68\x88\x1f\xd2\x23\x08\x49\x18\x4d\xb7\x37\x9b\x66\x8c\x0b\xc6\x41\x28\x02\x32\xfd\x9c\x34\x9b\x4b\x92\x11\x32\x7c\xd0\xa6\x95\x4a\x10\x5a\xa4\xed\xe7\x97\x76\x86\x24\x49\x25\x88\x23\xc1\xda\x0c\xe3\x56\xdf\xdd\x9f\xe7\xbf\x1f\x61\x37\xf6\xac\xda\x66\xdb\xef\x1c\x29\x05\x82\xfe\x3e\xdd\x5b\x3b\xfc\xe5\x01\xdd\x7e\xff\xdf\xed\x1f\x1f\x6f\xff\x7b\x97\xdd\x6e\x3f\xbc\x37\x86\x1b\xf9\x0a\xd8\x77\xcb\xe7\xb0\x27\x94\x28\xc2\xe8\xb8\x7e\x3a\x22\x5f\xfa\xdf\x5e\xc6\x85\x51\x9e\xb7\x60\x54\x1a\x6b\xef\x51\x29\xc1\xe4\x99\x82\x7a\x62\xe2\x31\xc4\xf3\x08\x7b\x23\x9e\xfb\xf5\x1d\x3c\x9b\xec\x1c\x59\x59\x57\x41\x0d\x0e\xa8\x37\x62\xa6\x5b\x7e\x1d\xfd\x49\xc0\x02\x54\xd8\x64\x3b\xd4\x9b\x59\x6c\xb3\xfc\x75\x0c\x6f\x06\xa6\x67\xb1\x1d\x42\x5b\xbb\xdd\xa0\xe1\xde\x2e\x51\xb9\xdc\xcb\x2f\xab\x51\x58\x1e\x29\xe5\xc0\x4b\x76\x6a\xbe\x79\xe4\xd1\x01\x2a\xa0\x2a\x1d\x45\x90\x24\xe9\xae\x26\x65\x6e\x4b\x94\x51\xf8\xad\x99\xe2\x41\xfb\x98\x24\x3f\xec\x48\xa6\xcd\xd3\x8e\x1b\x7f\xf9\x15\x3e\x8e\x7b\x78\x19\xc7\x31\xa3\x0a\x9e\x55\xcb\xd4\xfc\xd2\x9d\x08\x18\x7e\x04\xb1\x27\x25\xc4\x52\x20\x51\xc8\x19\x91\x95\x44\xaa\x8c\x89\x2c\x27\x58\x39\xe9\x31\xc2\x07\xc8\xf6\x82\x55\xc1\x59\xf6\x59\xb7\x0f\x99\xbe\x58\xf3\x4c\x26\x0e\x1b\xe6\x48\xaa\xfd\xb5\xdd\x38\x26\x4c\x31\xe2\x19\xca\x73\x43\x20\x48\x08\x74\x4a\x6f\x92\x94\x28\xa8\xa4\x5b\x56\x49\x5a\x53\xf2\xad\x86\xff\xf7\x10\x25\x6a\xb0\xe7\xcd\x05\xe3\xeb\x4f\x5c\x08\x56\xf3\x8c\x23\xd1\x58\xea\xbc\x1e\x53\xcc\xaa\x0a\xd1\xb5\xcc\x77\x09\x1f\x11\x92\x67\x54\x21\x42\x41\x64\x14\x55\x21\x8b\x6c\xdc\x17\x68\x2e\xb3\xae\x72\x88\xb5\x24\x63\x82\xb1\x8c\x58\x55\x1f\x39\x9d\xf3\x90\x6e\x9a\xc6\x47\x9a\xbd\xa5\x16\x61\x26\x01\x09\x7c\xb8\x90\x9e\x55\x88\xd0\x18\xd9\x01\x55\xe2\xc4\x19\xe9\xec\xe5\x97\x33\x04\xa0\xc7\x6c\x0c\x4a\x8b\xc5\x00\xf4\x48\x04\xa3\xd5\xe0\x0d\x71\x91\x4a\xa3\x7f\xe6\x4c\x82\x2d\x18\x8b\x41\x7d\x68\x64\xd5\x90\xc9\x40\xf1\x30\x30\x7e\x93\xa4\xb4\xae\x76\x20\x9a\x62\xd8\x40\xee\x99\xa8\x50\xb3\xd9\x61\x6d\x6d\xd8\x90\xb4\xc3\xf2\x74\x01\xea\x3c\x34\xf5\x01\x2a\xb3\x92\xd0\xc7\xf5\x4d\x1c\x9e\x95\x40\xd9\x81\x49\x75\x49\x32\x48\x0f\x80\x4a\x75\xc0\x07\xc0\x8f\x33\xe4\x3a\xca\xa0\x66\x52\xc5\x18\x39\xa9\x50\x11\x06\x71\x1c\x82\x94\x68\x07\xe5\x45\x7c\xae\x2a\x7c\x6d\x5a\x56\x14\x0d\xd4\x67\x71\x93\x12\xa8\x1f\x0e\x15\x0f\xb9\x20\x47\x10\xb1\x95\x00\xe3\xe7\xca\xcd\x1e\x0c\x57\x32\x49\xb8\x8c\x35\xa0\x5f\xee\xba\x2a\x76\xc6\xab\xda\xdf\xca\x32\xdd\xda\xe5\x42\x62\xe5\x7d\xd7\x17\x8b\xc3\xb8\x82\xc2\xd0\x4a\x85\x70\x53\x37\x08\x90\x1e\xbd\x9e\xa1\x7d\x9b\x94\x55\x2c\xf7\x19\xe8\x04\x6c\xcb\xc6\x1b\xa9\x17\x27\xc2\xe4\xa2\x42\x34\x4a\x75\xc1\x4e\x24\xc0\x8d\x6f\x7b\xb1\xdb\x3c\x6f\x37\x6c\x62\x2d\x0e\x95\x04\x49\x08\x3b\xbb\x57\x90\xc6\x6c\x84\x1f\x3f\x45\xda\x84\x8b\xf6\x3f\xb3\xb4\x1e\x52\xef\x9c\xf1\x35\x72\x60\xaa\xf3\x56\x5a\x77\x73\x6d\x64\x1b\xf0\xb6\x57\x2e\xe1\x39\xc9\xfd\xb1\xa2\x8d\x10\xba\x83\x71\x26\xd4\xc4\xbb\x96\xa7\x7b\x9f\x05\xeb\xe2\x1a\xe2\xd4\x39\xe1\x77\x8b\x4f\xa4\x31\x51\x77\x14\xd1\xd4\xff\x82\xfe\x11\xf6\x8c\x74\x26\x4a\x39\xd0\x0a\x89\x02\xcc\x36\x84\x50\x05\x05\x08\x0f\x01\xaf\x77\x25\x91\x07\xc8\x97\xd0\x08\xa6\x18\x66\x65\x9c\x63\x38\xfb\xd8\x78\x67\x30\x27\xdc\x5e\x5d\x9b\x71\x41\x8e\xa4\x84\xc2\xe2\x78\xc7\x58\x09\x88\x1a\x89\x42\x00\xca\x33\x46\xcb\x53\x04\x52\x2a\x24\x82\xed\x9f\x04\x5c\x0b\xa2\x4e\x19\xe3\x6a\xf5\xaa\x50\x1e\xaa\x4c\x92\xef\x60\xfa\xde\xd9\xea\xfb\x89\xb6\xd6\x86\xac\x83\xb1\xe4\xb5\xdc\xcf\x67\xb6\xaf\xe4\x36\x92\xd5\x02\x5f\xe7\x38\xb3\xf8\xda\x0c\x72\xf3\xe0\x62\x09\x78\xe2\xf0\xbd\x0a\x43\x35\xd4\xac\xab\x38\x03\xb5\x3c\x49\xac\x2e\xab\xad\xa5\xca\x09\xcd\x18\x07\x1a\xf4\x0d\xa9\x18\xcf\x0a\x81\x30\x64\x1c\x04\x61\x4e\x51\x18\x01\x36\xaf\x05\x6a\xd6\x9f\x4e\x23\x49\x41\x91\x3b\xee\x68\x50\x55\xf1\xfd\x85\x87\x00\x4a\x85\x9d\xbd\x2e\x49\x45\xfc\x4e\xe3\xb0\xda\x88\x7a\xad\xab\xd5\xdc\x25\xda\x4c\x79\x16\x15\xb2\x67\x3a\x84\xf9\x06\x21\xa2\x33\x38\x20\xb1\x20\x75\xb4\x8e\xb9\xf7\xe4\x27\x57\xdf\xe0\xdc\x97\x71\xc5\xd5\xce\x77\xd3\x6f\x64\xeb\xc4\x2f\x2a\xbd\xec\x6d\x6c\xbd\xd5\x8f\xdb\xa9\x6a\x19\x6c\xe2\x5a\x0c\x95\x73\x0d\xc8\x08\x9d\xde\xd5\x24\x7f\x89\x08\x6d\xe8\xa8\x85\x3b\x74\x13\x11\xc7\xfb\x95\x22\x63\xe7\x6b\x47\xfd\xe8\x8a\x40\xa3\xd9\x91\xc9\x81\xef\x12\x49\xc6\xc9\x69\x44\xa1\xa2\x0b\x9d\xd1\x3d\x4b\xbc\xdb\xf5\x37\x72\x3f\x85\x15\xca\x30\xe3\x1e\x29\xc7\xb3\xb1\x34\x63\x5a\xa7\x10\x33\x25\xa5\xcf\xfb\x9f\x98\x78\x6c\x72\x4b\x4e\xdc\x41\x60\x63\x91\x2c\xb8\xc4\xb4\x8e\xed\x86\x09\x5c\xb7\x73\x3a\x34\x78\x9b\x39\x7f\x53\xd8\x83\xbc\xb7\x78\x44\xa2\x9d\x75\x7f\xe5\xca\x99\x4d\x90\x17\xc7\x70\xea\x16\xa0\x04\xb1\x6e\x05\x86\xfa\x47\x4f\xd3\x20\x7f\xcd\xb3\x73\x45\x2a\x60\xb5\x3b\xa2\x6c\x74\xc3\xe9\x89\x52\xed\x96\x33\xa0\x54\x0d\x69\xeb\xf4\x61\x54\xea\xd0\x62\x07\x15\x17\x93\x7b\x80\xe6\xed\x2d\x45\x54\xa2\x12\xc0\x4b\x82\x91\x0c\x15\x03\x57\x1c\xe8\xd6\x3c\x47\x0a\xb2\xee\x49\xcc\xa2\xf2\x6b\xa6\xee\xe2\x48\xa0\xb2\x84\x92\xc8\x2a\xa6\x8e\x49\x73\x28\xd1\xe9\xa2\x12\xb6\x25\xdf\x23\x52\xd6\x02\x32\x84\xbd\x61\xda\xa2\xa8\x18\x25\x8a\x39\xc3\x49\xdc\x92\x15\x7a\xce\x86\x65\x5b\x48\xa8\xbb\x30\x1b\xeb\xd8\xb3\x58\xcd\x12\xba\x34\xbc\xac\x42\x9e\x51\xd1\xb9\xde\xf6\x58\xcc\xb0\xe2\x84\x75\x01\xb2\x09\x3b\xe3\x51\x79\x90\x3e\x18\xe0\xfb\x4e\x3f\xe3\xac\x24\xf8\xb4\x16\x87\x98\xd1\x4e\xc8\x31\x06\x71\xa5\x05\x36\xe6\xd0\xb4\x23\x15\x57\x41\x67\x6d\x09\x9e\x08\xcd\xd9\xd3\x82\x05\xd7\x33\x25\x5e\x22\x0c\x56\x70\xbc\x56\xd0\x52\x09\x44\xa8\x5a\x7c\x33\x74\x2d\x5b\x57\xa4\xfe\xd1\x3e\x03\x29\x62\xc4\x05\x93\xbe\x2f\x2d\x60\x5e\x07\xef\x4f\x2a\xa8\x98\x70\x1a\xe0\x0a\x6f\xec\x42\x2c\x0e\xb0\x15\x52\x60\xd4\x85\x5b\x8f\xca\x18\x5f\xbf\xe3\x0f\x5f\xaa\x6d\xc3\x01\x89\x70\x54\xad\xe5\x1d\xd1\x57\x90\xa9\x33\x07\x27\xf3\x9d\x69\xe2\xef\x4e\x43\xbb\x0e\xef\xbd\x47\xc8\x7a\x47\x3d\x0d\xdd\xb4\x19\x58\xf3\x60\x7a\xc5\xa0\x37\xbc\x1e\xf0\x68\xf5\x61\x2c\xb0\x6f\x46\x59\x6d\xa3\x55\xec\xbd\xba\x5f\x6f\xff\x6d\xad\x6f\x1f\xd3\xb9\x9a\x02\xa4\x14\xc2\x87\xa8\xfe\x61\x61\xd1\x78\x45\x1c\x9a\x74\xb9\xce\x30\xd4\xa3\xfe\x89\x42\x7f\x13\x9b\xfd\x79\xf6\xd5\x3f\xf3\x0d\xbe\xaf\x6d\x51\x17\xe7\xf1\x88\x47\xa5\xbf\x80\xce\xde\x5a\x15\xe6\x3d\x80\xa6\x92\xe9\x59\xc2\x9c\x24\x97\x3e\xa4\xdd\x9a\xdb\xb0\x61\x8e\xff\xc4\x30\x93\xe9\xdc\x2d\xe1\x00\xf1\x9c\x5d\x59\x8b\xf6\x42\x9c\xe7\x7c\xc5\x60\x73\xf7\x61\xa6\x64\x98\x7b\x54\xf4\x4a\xb9\x76\x85\x1b\x58\xb7\x4e\xad\x3e\x63\x90\xee\xf4\x75\xbd\xc7\xff\x35\xfa\xc9\x5b\xfb\x86\x4f\x7a\x9a\x9c\x75\xfd\x30\xcf\xdc\xbb\x77\xf2\x5b\x43\x3e\x16\xa4\x7b\xa2\xa7\x45\xf7\xad\xde\x7a\xf9\xd4\xe8\x7c\x81\x6f\x9f\xf8\x0f\x2f\xe1\x3d\x97\x90\x1b\xfd\x67\xfb\x5f\x0b\x9b\x97\xcd\x9f\x01\x00\x00\xff\xff\x0b\x42\x15\x69\x2e\x35\x00\x00") + +func dataConfig_schema_v32JsonBytes() ([]byte, error) { + return bindataRead( + _dataConfig_schema_v32Json, + "data/config_schema_v3.2.json", + ) +} + +func dataConfig_schema_v32Json() (*asset, error) { + bytes, err := dataConfig_schema_v32JsonBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "data/config_schema_v3.2.json", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + // Asset loads and returns the asset for the given name. // It returns an error if the asset could not be found or // could not be loaded. @@ -163,6 +184,7 @@ func AssetNames() []string { var _bindata = map[string]func() (*asset, error){ "data/config_schema_v3.0.json": dataConfig_schema_v30Json, "data/config_schema_v3.1.json": dataConfig_schema_v31Json, + "data/config_schema_v3.2.json": dataConfig_schema_v32Json, } // AssetDir returns the file names below a certain @@ -208,6 +230,7 @@ var _bintree = &bintree{nil, map[string]*bintree{ "data": &bintree{nil, map[string]*bintree{ "config_schema_v3.0.json": &bintree{dataConfig_schema_v30Json, map[string]*bintree{}}, "config_schema_v3.1.json": &bintree{dataConfig_schema_v31Json, map[string]*bintree{}}, + "config_schema_v3.2.json": &bintree{dataConfig_schema_v32Json, map[string]*bintree{}}, }}, }} diff --git a/components/cli/compose/schema/data/config_schema_v3.1.json b/components/cli/compose/schema/data/config_schema_v3.1.json index 72e1d61bb6..b7037485f9 100644 --- a/components/cli/compose/schema/data/config_schema_v3.1.json +++ b/components/cli/compose/schema/data/config_schema_v3.1.json @@ -167,20 +167,8 @@ "ports": { "type": "array", "items": { - "oneOf": [ - {"type": "number", "format": "ports"}, - {"type": "string", "format": "ports"}, - { - "type": "object", - "properties": { - "mode": {"type": "string"}, - "target": {"type": "integer"}, - "published": {"type": "integer"}, - "protocol": {"type": "string"} - }, - "additionalProperties": false - } - ] + "type": ["string", "number"], + "format": "ports" }, "uniqueItems": true }, @@ -235,37 +223,7 @@ }, "user": {"type": "string"}, "userns_mode": {"type": "string"}, - "volumes": { - "type": "array", - "items": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "required": ["type"], - "properties": { - "type": {"type": "string"}, - "source": {"type": "string"}, - "target": {"type": "string"}, - "read_only": {"type": "boolean"}, - "bind": { - "type": "object", - "properties": { - "propagation": {"type": "string"} - } - }, - "volume": { - "type": "object", - "properties": { - "nocopy": {"type": "boolean"} - } - } - } - } - ], - "uniqueItems": true - } - }, + "volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, "working_dir": {"type": "string"} }, "additionalProperties": false @@ -293,7 +251,6 @@ "type": ["object", "null"], "properties": { "mode": {"type": "string"}, - "endpoint_mode": {"type": "string"}, "replicas": {"type": "integer"}, "labels": {"$ref": "#/definitions/list_or_dict"}, "update_config": { @@ -381,7 +338,6 @@ "additionalProperties": false }, "internal": {"type": "boolean"}, - "attachable": {"type": "boolean"}, "labels": {"$ref": "#/definitions/list_or_dict"} }, "additionalProperties": false diff --git a/components/cli/compose/schema/data/config_schema_v3.2.json b/components/cli/compose/schema/data/config_schema_v3.2.json new file mode 100644 index 0000000000..e47c879a4d --- /dev/null +++ b/components/cli/compose/schema/data/config_schema_v3.2.json @@ -0,0 +1,473 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "config_schema_v3.1.json", + "type": "object", + "required": ["version"], + + "properties": { + "version": { + "type": "string" + }, + + "services": { + "id": "#/properties/services", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "$ref": "#/definitions/service" + } + }, + "additionalProperties": false + }, + + "networks": { + "id": "#/properties/networks", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "$ref": "#/definitions/network" + } + } + }, + + "volumes": { + "id": "#/properties/volumes", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "$ref": "#/definitions/volume" + } + }, + "additionalProperties": false + }, + + "secrets": { + "id": "#/properties/secrets", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "$ref": "#/definitions/secret" + } + }, + "additionalProperties": false + } + }, + + "additionalProperties": false, + + "definitions": { + + "service": { + "id": "#/definitions/service", + "type": "object", + + "properties": { + "deploy": {"$ref": "#/definitions/deployment"}, + "build": { + "oneOf": [ + {"type": "string"}, + { + "type": "object", + "properties": { + "context": {"type": "string"}, + "dockerfile": {"type": "string"}, + "args": {"$ref": "#/definitions/list_or_dict"}, + "cache_from": {"$ref": "#/definitions/list_of_strings"} + }, + "additionalProperties": false + } + ] + }, + "cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "cgroup_parent": {"type": "string"}, + "command": { + "oneOf": [ + {"type": "string"}, + {"type": "array", "items": {"type": "string"}} + ] + }, + "container_name": {"type": "string"}, + "depends_on": {"$ref": "#/definitions/list_of_strings"}, + "devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "dns": {"$ref": "#/definitions/string_or_list"}, + "dns_search": {"$ref": "#/definitions/string_or_list"}, + "domainname": {"type": "string"}, + "entrypoint": { + "oneOf": [ + {"type": "string"}, + {"type": "array", "items": {"type": "string"}} + ] + }, + "env_file": {"$ref": "#/definitions/string_or_list"}, + "environment": {"$ref": "#/definitions/list_or_dict"}, + + "expose": { + "type": "array", + "items": { + "type": ["string", "number"], + "format": "expose" + }, + "uniqueItems": true + }, + + "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "extra_hosts": {"$ref": "#/definitions/list_or_dict"}, + "healthcheck": {"$ref": "#/definitions/healthcheck"}, + "hostname": {"type": "string"}, + "image": {"type": "string"}, + "ipc": {"type": "string"}, + "labels": {"$ref": "#/definitions/list_or_dict"}, + "links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + + "logging": { + "type": "object", + + "properties": { + "driver": {"type": "string"}, + "options": { + "type": "object", + "patternProperties": { + "^.+$": {"type": ["string", "number", "null"]} + } + } + }, + "additionalProperties": false + }, + + "mac_address": {"type": "string"}, + "network_mode": {"type": "string"}, + + "networks": { + "oneOf": [ + {"$ref": "#/definitions/list_of_strings"}, + { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "oneOf": [ + { + "type": "object", + "properties": { + "aliases": {"$ref": "#/definitions/list_of_strings"}, + "ipv4_address": {"type": "string"}, + "ipv6_address": {"type": "string"} + }, + "additionalProperties": false + }, + {"type": "null"} + ] + } + }, + "additionalProperties": false + } + ] + }, + "pid": {"type": ["string", "null"]}, + + "ports": { + "type": "array", + "items": { + "oneOf": [ + {"type": "number", "format": "ports"}, + {"type": "string", "format": "ports"}, + { + "type": "object", + "properties": { + "mode": {"type": "string"}, + "target": {"type": "integer"}, + "published": {"type": "integer"}, + "protocol": {"type": "string"} + }, + "additionalProperties": false + } + ] + }, + "uniqueItems": true + }, + + "privileged": {"type": "boolean"}, + "read_only": {"type": "boolean"}, + "restart": {"type": "string"}, + "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "shm_size": {"type": ["number", "string"]}, + "secrets": { + "type": "array", + "items": { + "oneOf": [ + {"type": "string"}, + { + "type": "object", + "properties": { + "source": {"type": "string"}, + "target": {"type": "string"}, + "uid": {"type": "string"}, + "gid": {"type": "string"}, + "mode": {"type": "number"} + } + } + ] + } + }, + "sysctls": {"$ref": "#/definitions/list_or_dict"}, + "stdin_open": {"type": "boolean"}, + "stop_grace_period": {"type": "string", "format": "duration"}, + "stop_signal": {"type": "string"}, + "tmpfs": {"$ref": "#/definitions/string_or_list"}, + "tty": {"type": "boolean"}, + "ulimits": { + "type": "object", + "patternProperties": { + "^[a-z]+$": { + "oneOf": [ + {"type": "integer"}, + { + "type":"object", + "properties": { + "hard": {"type": "integer"}, + "soft": {"type": "integer"} + }, + "required": ["soft", "hard"], + "additionalProperties": false + } + ] + } + } + }, + "user": {"type": "string"}, + "userns_mode": {"type": "string"}, + "volumes": { + "type": "array", + "items": { + "oneOf": [ + {"type": "string"}, + { + "type": "object", + "required": ["type"], + "properties": { + "type": {"type": "string"}, + "source": {"type": "string"}, + "target": {"type": "string"}, + "read_only": {"type": "boolean"}, + "bind": { + "type": "object", + "properties": { + "propagation": {"type": "string"} + } + }, + "volume": { + "type": "object", + "properties": { + "nocopy": {"type": "boolean"} + } + } + } + } + ], + "uniqueItems": true + } + }, + "working_dir": {"type": "string"} + }, + "additionalProperties": false + }, + + "healthcheck": { + "id": "#/definitions/healthcheck", + "type": "object", + "additionalProperties": false, + "properties": { + "disable": {"type": "boolean"}, + "interval": {"type": "string"}, + "retries": {"type": "number"}, + "test": { + "oneOf": [ + {"type": "string"}, + {"type": "array", "items": {"type": "string"}} + ] + }, + "timeout": {"type": "string"} + } + }, + "deployment": { + "id": "#/definitions/deployment", + "type": ["object", "null"], + "properties": { + "mode": {"type": "string"}, + "endpoint_mode": {"type": "string"}, + "replicas": {"type": "integer"}, + "labels": {"$ref": "#/definitions/list_or_dict"}, + "update_config": { + "type": "object", + "properties": { + "parallelism": {"type": "integer"}, + "delay": {"type": "string", "format": "duration"}, + "failure_action": {"type": "string"}, + "monitor": {"type": "string", "format": "duration"}, + "max_failure_ratio": {"type": "number"} + }, + "additionalProperties": false + }, + "resources": { + "type": "object", + "properties": { + "limits": {"$ref": "#/definitions/resource"}, + "reservations": {"$ref": "#/definitions/resource"} + } + }, + "restart_policy": { + "type": "object", + "properties": { + "condition": {"type": "string"}, + "delay": {"type": "string", "format": "duration"}, + "max_attempts": {"type": "integer"}, + "window": {"type": "string", "format": "duration"} + }, + "additionalProperties": false + }, + "placement": { + "type": "object", + "properties": { + "constraints": {"type": "array", "items": {"type": "string"}} + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + + "resource": { + "id": "#/definitions/resource", + "type": "object", + "properties": { + "cpus": {"type": "string"}, + "memory": {"type": "string"} + }, + "additionalProperties": false + }, + + "network": { + "id": "#/definitions/network", + "type": ["object", "null"], + "properties": { + "driver": {"type": "string"}, + "driver_opts": { + "type": "object", + "patternProperties": { + "^.+$": {"type": ["string", "number"]} + } + }, + "ipam": { + "type": "object", + "properties": { + "driver": {"type": "string"}, + "config": { + "type": "array", + "items": { + "type": "object", + "properties": { + "subnet": {"type": "string"} + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "external": { + "type": ["boolean", "object"], + "properties": { + "name": {"type": "string"} + }, + "additionalProperties": false + }, + "internal": {"type": "boolean"}, + "attachable": {"type": "boolean"}, + "labels": {"$ref": "#/definitions/list_or_dict"} + }, + "additionalProperties": false + }, + + "volume": { + "id": "#/definitions/volume", + "type": ["object", "null"], + "properties": { + "driver": {"type": "string"}, + "driver_opts": { + "type": "object", + "patternProperties": { + "^.+$": {"type": ["string", "number"]} + } + }, + "external": { + "type": ["boolean", "object"], + "properties": { + "name": {"type": "string"} + }, + "additionalProperties": false + }, + "labels": {"$ref": "#/definitions/list_or_dict"} + }, + "additionalProperties": false + }, + + "secret": { + "id": "#/definitions/secret", + "type": "object", + "properties": { + "file": {"type": "string"}, + "external": { + "type": ["boolean", "object"], + "properties": { + "name": {"type": "string"} + } + }, + "labels": {"$ref": "#/definitions/list_or_dict"} + }, + "additionalProperties": false + }, + + "string_or_list": { + "oneOf": [ + {"type": "string"}, + {"$ref": "#/definitions/list_of_strings"} + ] + }, + + "list_of_strings": { + "type": "array", + "items": {"type": "string"}, + "uniqueItems": true + }, + + "list_or_dict": { + "oneOf": [ + { + "type": "object", + "patternProperties": { + ".+": { + "type": ["string", "number", "null"] + } + }, + "additionalProperties": false + }, + {"type": "array", "items": {"type": "string"}, "uniqueItems": true} + ] + }, + + "constraints": { + "service": { + "id": "#/definitions/constraints/service", + "anyOf": [ + {"required": ["build"]}, + {"required": ["image"]} + ], + "properties": { + "build": { + "required": ["context"] + } + } + } + } + } +} From acc4901d563b52a8128d282a2b3517c8f549ff61 Mon Sep 17 00:00:00 2001 From: Santhosh Manohar Date: Thu, 9 Mar 2017 11:42:10 -0800 Subject: [PATCH 657/978] Enhance network inspect to show all tasks, local & non-local, in swarm mode Signed-off-by: Santhosh Manohar Upstream-commit: 6c7da0ca57b54f8e30b927c26765fb884e6666ce Component: cli --- components/cli/command/network/inspect.go | 8 +++++--- components/cli/command/stack/deploy_composefile.go | 2 +- components/cli/command/system/inspect.go | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/components/cli/command/network/inspect.go b/components/cli/command/network/inspect.go index 1a86855f71..e58d66b77a 100644 --- a/components/cli/command/network/inspect.go +++ b/components/cli/command/network/inspect.go @@ -10,8 +10,9 @@ import ( ) type inspectOptions struct { - format string - names []string + format string + names []string + verbose bool } func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { @@ -28,6 +29,7 @@ func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { } cmd.Flags().StringVarP(&opts.format, "format", "f", "", "Format the output using the given Go template") + cmd.Flags().BoolVarP(&opts.verbose, "verbose", "v", false, "Verbose output for diagnostics") return cmd } @@ -38,7 +40,7 @@ func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { ctx := context.Background() getNetFunc := func(name string) (interface{}, []byte, error) { - return client.NetworkInspectWithRaw(ctx, name) + return client.NetworkInspectWithRaw(ctx, name, opts.verbose) } return inspect.Inspect(dockerCli.Out(), opts.names, opts.format, getNetFunc) diff --git a/components/cli/command/stack/deploy_composefile.go b/components/cli/command/stack/deploy_composefile.go index 72f9b8aac9..3e62494325 100644 --- a/components/cli/command/stack/deploy_composefile.go +++ b/components/cli/command/stack/deploy_composefile.go @@ -140,7 +140,7 @@ func validateExternalNetworks( client := dockerCli.Client() for _, networkName := range externalNetworks { - network, err := client.NetworkInspect(ctx, networkName) + network, err := client.NetworkInspect(ctx, networkName, false) if err != nil { if dockerclient.IsErrNetworkNotFound(err) { return fmt.Errorf("network %q is declared as external, but could not be found. You need to create the network before the stack is deployed (with overlay driver)", networkName) diff --git a/components/cli/command/system/inspect.go b/components/cli/command/system/inspect.go index c86e858a29..6bb9cbe041 100644 --- a/components/cli/command/system/inspect.go +++ b/components/cli/command/system/inspect.go @@ -67,7 +67,7 @@ func inspectImages(ctx context.Context, dockerCli *command.DockerCli) inspect.Ge func inspectNetwork(ctx context.Context, dockerCli *command.DockerCli) inspect.GetRefFunc { return func(ref string) (interface{}, []byte, error) { - return dockerCli.Client().NetworkInspectWithRaw(ctx, ref) + return dockerCli.Client().NetworkInspectWithRaw(ctx, ref, false) } } From ce02b10e887e24bd51742dca64567c8b8d1c5cf0 Mon Sep 17 00:00:00 2001 From: Santhosh Manohar Date: Thu, 9 Mar 2017 11:42:10 -0800 Subject: [PATCH 658/978] Enhance network inspect to show all tasks, local & non-local, in swarm mode Signed-off-by: Santhosh Manohar Upstream-commit: faee4c005bf8832978fcebf6c54d7ab60292c57a Component: cli --- components/cli/interface.go | 4 +-- components/cli/network_inspect.go | 19 +++++++++---- components/cli/network_inspect_test.go | 39 ++++++++++++++++++++++---- 3 files changed, 49 insertions(+), 13 deletions(-) diff --git a/components/cli/interface.go b/components/cli/interface.go index 5823eed883..ae4146bb4a 100644 --- a/components/cli/interface.go +++ b/components/cli/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/components/cli/network_inspect.go b/components/cli/network_inspect.go index 5ad4ea5bf3..7242304025 100644 --- a/components/cli/network_inspect.go +++ b/components/cli/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/components/cli/network_inspect_test.go b/components/cli/network_inspect_test.go index 55f04eca2c..1504289f5d 100644 --- a/components/cli/network_inspect_test.go +++ b/components/cli/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 29417b01592967dd468e1009446bc8489005bb21 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Mon, 13 Mar 2017 18:31:48 -0700 Subject: [PATCH 659/978] bump API to 1.28 Signed-off-by: Victor Vieux Upstream-commit: a972d43e481b1a8b0c2215d8c4e52d9b84300272 Component: cli --- components/cli/command/service/create.go | 2 +- components/cli/command/service/opts.go | 14 +++++++------- components/cli/command/service/update.go | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/components/cli/command/service/create.go b/components/cli/command/service/create.go index c2eb81727a..fc1ecbd9fe 100644 --- a/components/cli/command/service/create.go +++ b/components/cli/command/service/create.go @@ -38,7 +38,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Var(&opts.mounts, flagMount, "Attach a filesystem mount to the service") flags.Var(&opts.constraints, flagConstraint, "Placement constraints") flags.Var(&opts.placementPrefs, flagPlacementPref, "Add a placement preference") - flags.SetAnnotation(flagPlacementPref, "version", []string{"1.27"}) + flags.SetAnnotation(flagPlacementPref, "version", []string{"1.28"}) flags.Var(&opts.networks, flagNetwork, "Network attachments") flags.Var(&opts.secrets, flagSecret, "Specify secrets to expose to the service") flags.SetAnnotation(flagSecret, "version", []string{"1.25"}) diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index baaa58e1f0..46fe919606 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -498,15 +498,15 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { flags.SetAnnotation(flagUpdateMaxFailureRatio, "version", []string{"1.25"}) flags.Uint64Var(&opts.rollback.parallelism, flagRollbackParallelism, 1, "Maximum number of tasks rolled back simultaneously (0 to roll back all at once)") - flags.SetAnnotation(flagRollbackParallelism, "version", []string{"1.27"}) + flags.SetAnnotation(flagRollbackParallelism, "version", []string{"1.28"}) flags.DurationVar(&opts.rollback.delay, flagRollbackDelay, time.Duration(0), "Delay between task rollbacks (ns|us|ms|s|m|h) (default 0s)") - flags.SetAnnotation(flagRollbackDelay, "version", []string{"1.27"}) + flags.SetAnnotation(flagRollbackDelay, "version", []string{"1.28"}) flags.DurationVar(&opts.rollback.monitor, flagRollbackMonitor, time.Duration(0), "Duration after each task rollback to monitor for failure (ns|us|ms|s|m|h) (default 0s)") - flags.SetAnnotation(flagRollbackMonitor, "version", []string{"1.27"}) + flags.SetAnnotation(flagRollbackMonitor, "version", []string{"1.28"}) flags.StringVar(&opts.rollback.onFailure, flagRollbackFailureAction, "pause", `Action on rollback failure ("pause"|"continue")`) - flags.SetAnnotation(flagRollbackFailureAction, "version", []string{"1.27"}) + flags.SetAnnotation(flagRollbackFailureAction, "version", []string{"1.28"}) flags.Var(&opts.rollback.maxFailureRatio, flagRollbackMaxFailureRatio, "Failure rate to tolerate during a rollback") - flags.SetAnnotation(flagRollbackMaxFailureRatio, "version", []string{"1.27"}) + flags.SetAnnotation(flagRollbackMaxFailureRatio, "version", []string{"1.28"}) flags.StringVar(&opts.endpoint.mode, flagEndpointMode, "vip", "Endpoint mode (vip or dnsrr)") @@ -530,10 +530,10 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { flags.SetAnnotation(flagTTY, "version", []string{"1.25"}) flags.BoolVar(&opts.readOnly, flagReadOnly, false, "Mount the container's root filesystem as read only") - flags.SetAnnotation(flagReadOnly, "version", []string{"1.27"}) + flags.SetAnnotation(flagReadOnly, "version", []string{"1.28"}) flags.StringVar(&opts.stopSignal, flagStopSignal, "", "Signal to stop the container") - flags.SetAnnotation(flagStopSignal, "version", []string{"1.27"}) + flags.SetAnnotation(flagStopSignal, "version", []string{"1.28"}) } const ( diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index ab8391e038..fc6a229fa3 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -72,9 +72,9 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.Var(&serviceOpts.mounts, flagMountAdd, "Add or update a mount on a service") flags.Var(&serviceOpts.constraints, flagConstraintAdd, "Add or update a placement constraint") flags.Var(&serviceOpts.placementPrefs, flagPlacementPrefAdd, "Add a placement preference") - flags.SetAnnotation(flagPlacementPrefAdd, "version", []string{"1.27"}) + flags.SetAnnotation(flagPlacementPrefAdd, "version", []string{"1.28"}) flags.Var(&placementPrefOpts{}, flagPlacementPrefRemove, "Remove a placement preference") - flags.SetAnnotation(flagPlacementPrefRemove, "version", []string{"1.27"}) + flags.SetAnnotation(flagPlacementPrefRemove, "version", []string{"1.28"}) flags.Var(&serviceOpts.endpoint.publishPorts, flagPublishAdd, "Add or update a published port") flags.Var(&serviceOpts.groups, flagGroupAdd, "Add an additional supplementary user group to the container") flags.SetAnnotation(flagGroupAdd, "version", []string{"1.25"}) @@ -132,7 +132,7 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID str return errors.New("other flags may not be combined with --rollback") } - if versions.LessThan(dockerCli.Client().ClientVersion(), "1.27") { + if versions.LessThan(dockerCli.Client().ClientVersion(), "1.28") { clientSideRollback = true spec = service.PreviousSpec if spec == nil { From 85cd81c04a5779e872103cdb94508ee6bddbb77e Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Tue, 7 Feb 2017 09:44:47 +0000 Subject: [PATCH 660/978] compose: fix environment interpolation from the client For an environment variable defined in the yaml without value, the value needs to be propagated from the client, as in Docker Compose. Signed-off-by: Akihiro Suda Upstream-commit: b7ffa960bf8650757daa516b9493b22252b314cf Component: cli --- .../cli/command/stack/deploy_composefile.go | 10 +++ components/cli/compose/loader/example2.env | 3 + components/cli/compose/loader/loader.go | 73 +++++++++++------ components/cli/compose/loader/loader_test.go | 82 ++++++++++--------- 4 files changed, 104 insertions(+), 64 deletions(-) diff --git a/components/cli/command/stack/deploy_composefile.go b/components/cli/command/stack/deploy_composefile.go index 3e62494325..b176b47e0b 100644 --- a/components/cli/command/stack/deploy_composefile.go +++ b/components/cli/command/stack/deploy_composefile.go @@ -115,6 +115,16 @@ func getConfigDetails(opts deployOptions) (composetypes.ConfigDetails, error) { } // TODO: support multiple files details.ConfigFiles = []composetypes.ConfigFile{*configFile} + env := os.Environ() + details.Environment = make(map[string]string, len(env)) + for _, s := range env { + // if value is empty, s is like "K=", not "K". + if !strings.Contains(s, "=") { + return details, fmt.Errorf("unexpected environment %q", s) + } + kv := strings.SplitN(s, "=", 2) + details.Environment[kv[0]] = kv[1] + } return details, nil } diff --git a/components/cli/compose/loader/example2.env b/components/cli/compose/loader/example2.env index 0920d5ab05..642334e9fd 100644 --- a/components/cli/compose/loader/example2.env +++ b/components/cli/compose/loader/example2.env @@ -1 +1,4 @@ BAR=2 + +# overridden in configDetails.Environment +QUX=1 diff --git a/components/cli/compose/loader/loader.go b/components/cli/compose/loader/loader.go index 995047e8c9..7c8bfa0a2a 100644 --- a/components/cli/compose/loader/loader.go +++ b/components/cli/compose/loader/loader.go @@ -2,15 +2,16 @@ package loader import ( "fmt" - "os" "path" "reflect" "regexp" "sort" "strings" + "github.com/Sirupsen/logrus" "github.com/docker/docker/cli/compose/interpolation" "github.com/docker/docker/cli/compose/schema" + "github.com/docker/docker/cli/compose/template" "github.com/docker/docker/cli/compose/types" "github.com/docker/docker/opts" runconfigopts "github.com/docker/docker/runconfig/opts" @@ -69,13 +70,17 @@ func Load(configDetails types.ConfigDetails) (*types.Config, error) { } cfg := types.Config{} + lookupEnv := func(k string) (string, bool) { + v, ok := configDetails.Environment[k] + return v, ok + } if services, ok := configDict["services"]; ok { - servicesConfig, err := interpolation.Interpolate(services.(types.Dict), "service", os.LookupEnv) + servicesConfig, err := interpolation.Interpolate(services.(types.Dict), "service", lookupEnv) if err != nil { return nil, err } - servicesList, err := LoadServices(servicesConfig, configDetails.WorkingDir) + servicesList, err := LoadServices(servicesConfig, configDetails.WorkingDir, lookupEnv) if err != nil { return nil, err } @@ -84,7 +89,7 @@ func Load(configDetails types.ConfigDetails) (*types.Config, error) { } if networks, ok := configDict["networks"]; ok { - networksConfig, err := interpolation.Interpolate(networks.(types.Dict), "network", os.LookupEnv) + networksConfig, err := interpolation.Interpolate(networks.(types.Dict), "network", lookupEnv) if err != nil { return nil, err } @@ -98,7 +103,7 @@ func Load(configDetails types.ConfigDetails) (*types.Config, error) { } if volumes, ok := configDict["volumes"]; ok { - volumesConfig, err := interpolation.Interpolate(volumes.(types.Dict), "volume", os.LookupEnv) + volumesConfig, err := interpolation.Interpolate(volumes.(types.Dict), "volume", lookupEnv) if err != nil { return nil, err } @@ -112,7 +117,7 @@ func Load(configDetails types.ConfigDetails) (*types.Config, error) { } if secrets, ok := configDict["secrets"]; ok { - secretsConfig, err := interpolation.Interpolate(secrets.(types.Dict), "secret", os.LookupEnv) + secretsConfig, err := interpolation.Interpolate(secrets.(types.Dict), "secret", lookupEnv) if err != nil { return nil, err } @@ -308,11 +313,11 @@ func formatInvalidKeyError(keyPrefix string, key interface{}) error { // LoadServices produces a ServiceConfig map from a compose file Dict // the servicesDict is not validated if directly used. Use Load() to enable validation -func LoadServices(servicesDict types.Dict, workingDir string) ([]types.ServiceConfig, error) { +func LoadServices(servicesDict types.Dict, workingDir string, lookupEnv template.Mapping) ([]types.ServiceConfig, error) { var services []types.ServiceConfig for name, serviceDef := range servicesDict { - serviceConfig, err := LoadService(name, serviceDef.(types.Dict), workingDir) + serviceConfig, err := loadService(name, serviceDef.(types.Dict), workingDir, lookupEnv) if err != nil { return nil, err } @@ -324,22 +329,39 @@ func LoadServices(servicesDict types.Dict, workingDir string) ([]types.ServiceCo // LoadService produces a single ServiceConfig from a compose file Dict // the serviceDict is not validated if directly used. Use Load() to enable validation -func LoadService(name string, serviceDict types.Dict, workingDir string) (*types.ServiceConfig, error) { +func LoadService(name string, serviceDict types.Dict, workingDir string, lookupEnv template.Mapping) (*types.ServiceConfig, error) { serviceConfig := &types.ServiceConfig{} if err := transform(serviceDict, serviceConfig); err != nil { return nil, err } serviceConfig.Name = name - if err := resolveEnvironment(serviceConfig, workingDir); err != nil { + if err := resolveEnvironment(serviceConfig, workingDir, lookupEnv); err != nil { return nil, err } - resolveVolumePaths(serviceConfig.Volumes, workingDir) + resolveVolumePaths(serviceConfig.Volumes, workingDir, lookupEnv) return serviceConfig, nil } -func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string) error { +func updateEnvironment(environment map[string]string, vars map[string]string, lookupEnv template.Mapping) map[string]string { + result := make(map[string]string, len(environment)) + for k, v := range environment { + result[k]=v + } + for k, v := range vars { + interpolatedV, ok := lookupEnv(k) + if ok { + // lookupEnv is prioritized over vars + result[k] = interpolatedV + } else { + result[k] = v + } + } + return result +} + +func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string, lookupEnv template.Mapping) error { environment := make(map[string]string) if len(serviceConfig.EnvFile) > 0 { @@ -353,36 +375,35 @@ func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string) e } envVars = append(envVars, fileVars...) } - - for k, v := range runconfigopts.ConvertKVStringsToMap(envVars) { - environment[k] = v - } + environment = updateEnvironment(environment, + runconfigopts.ConvertKVStringsToMap(envVars), lookupEnv) } - for k, v := range serviceConfig.Environment { - environment[k] = v - } - - serviceConfig.Environment = environment - + serviceConfig.Environment = updateEnvironment(environment, + serviceConfig.Environment, lookupEnv) return nil } -func resolveVolumePaths(volumes []types.ServiceVolumeConfig, workingDir string) { +func resolveVolumePaths(volumes []types.ServiceVolumeConfig, workingDir string, lookupEnv template.Mapping) { for i, volume := range volumes { if volume.Type != "bind" { continue } - volume.Source = absPath(workingDir, expandUser(volume.Source)) + volume.Source = absPath(workingDir, expandUser(volume.Source, lookupEnv)) volumes[i] = volume } } // TODO: make this more robust -func expandUser(path string) string { +func expandUser(path string, lookupEnv template.Mapping) string { if strings.HasPrefix(path, "~") { - return strings.Replace(path, "~", os.Getenv("HOME"), 1) + home, ok := lookupEnv("HOME") + if !ok { + logrus.Warn("cannot expand '~', because the environment lacks HOME") + return path + } + return strings.Replace(path, "~", home, 1) } return path } diff --git a/components/cli/compose/loader/loader_test.go b/components/cli/compose/loader/loader_test.go index b9fb10f227..4f424d6126 100644 --- a/components/cli/compose/loader/loader_test.go +++ b/components/cli/compose/loader/loader_test.go @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/assert" ) -func buildConfigDetails(source types.Dict) types.ConfigDetails { +func buildConfigDetails(source types.Dict, env map[string]string) types.ConfigDetails { workingDir, err := os.Getwd() if err != nil { panic(err) @@ -23,7 +23,7 @@ func buildConfigDetails(source types.Dict) types.ConfigDetails { ConfigFiles: []types.ConfigFile{ {Filename: "filename.yml", Config: source}, }, - Environment: nil, + Environment: env, } } @@ -154,7 +154,7 @@ func TestParseYAML(t *testing.T) { } func TestLoad(t *testing.T) { - actual, err := Load(buildConfigDetails(sampleDict)) + actual, err := Load(buildConfigDetails(sampleDict, nil)) if !assert.NoError(t, err) { return } @@ -173,7 +173,7 @@ services: secrets: super: external: true -`) +`, nil) if !assert.NoError(t, err) { return } @@ -182,7 +182,7 @@ secrets: } func TestParseAndLoad(t *testing.T) { - actual, err := loadYAML(sampleYAML) + actual, err := loadYAML(sampleYAML, nil) if !assert.NoError(t, err) { return } @@ -192,15 +192,15 @@ func TestParseAndLoad(t *testing.T) { } func TestInvalidTopLevelObjectType(t *testing.T) { - _, err := loadYAML("1") + _, err := loadYAML("1", nil) assert.Error(t, err) assert.Contains(t, err.Error(), "Top-level object must be a mapping") - _, err = loadYAML("\"hello\"") + _, err = loadYAML("\"hello\"", nil) assert.Error(t, err) assert.Contains(t, err.Error(), "Top-level object must be a mapping") - _, err = loadYAML("[\"hello\"]") + _, err = loadYAML("[\"hello\"]", nil) assert.Error(t, err) assert.Contains(t, err.Error(), "Top-level object must be a mapping") } @@ -211,7 +211,7 @@ version: "3" 123: foo: image: busybox -`) +`, nil) assert.Error(t, err) assert.Contains(t, err.Error(), "Non-string key at top level: 123") @@ -222,7 +222,7 @@ services: image: busybox 123: image: busybox -`) +`, nil) assert.Error(t, err) assert.Contains(t, err.Error(), "Non-string key in services: 123") @@ -236,7 +236,7 @@ networks: ipam: config: - 123: oh dear -`) +`, nil) assert.Error(t, err) assert.Contains(t, err.Error(), "Non-string key in networks.default.ipam.config[0]: 123") @@ -247,7 +247,7 @@ services: image: busybox environment: 1: FOO -`) +`, nil) assert.Error(t, err) assert.Contains(t, err.Error(), "Non-string key in services.dict-env.environment: 1") } @@ -258,7 +258,7 @@ version: "3" services: foo: image: busybox -`) +`, nil) assert.NoError(t, err) _, err = loadYAML(` @@ -266,7 +266,7 @@ version: "3.0" services: foo: image: busybox -`) +`, nil) assert.NoError(t, err) } @@ -276,7 +276,7 @@ version: "2" services: foo: image: busybox -`) +`, nil) assert.Error(t, err) assert.Contains(t, err.Error(), "version") @@ -285,7 +285,7 @@ version: "2.0" services: foo: image: busybox -`) +`, nil) assert.Error(t, err) assert.Contains(t, err.Error(), "version") } @@ -296,7 +296,7 @@ version: 3 services: foo: image: busybox -`) +`, nil) assert.Error(t, err) assert.Contains(t, err.Error(), "version must be a string") } @@ -305,7 +305,7 @@ func TestV1Unsupported(t *testing.T) { _, err := loadYAML(` foo: image: busybox -`) +`, nil) assert.Error(t, err) } @@ -315,7 +315,7 @@ version: "3" services: - foo: image: busybox -`) +`, nil) assert.Error(t, err) assert.Contains(t, err.Error(), "services must be a mapping") @@ -323,7 +323,7 @@ services: version: "3" services: foo: busybox -`) +`, nil) assert.Error(t, err) assert.Contains(t, err.Error(), "services.foo must be a mapping") @@ -332,7 +332,7 @@ version: "3" networks: - default: driver: bridge -`) +`, nil) assert.Error(t, err) assert.Contains(t, err.Error(), "networks must be a mapping") @@ -340,7 +340,7 @@ networks: version: "3" networks: default: bridge -`) +`, nil) assert.Error(t, err) assert.Contains(t, err.Error(), "networks.default must be a mapping") @@ -349,7 +349,7 @@ version: "3" volumes: - data: driver: local -`) +`, nil) assert.Error(t, err) assert.Contains(t, err.Error(), "volumes must be a mapping") @@ -357,7 +357,7 @@ volumes: version: "3" volumes: data: local -`) +`, nil) assert.Error(t, err) assert.Contains(t, err.Error(), "volumes.data must be a mapping") } @@ -368,7 +368,7 @@ version: "3" services: foo: image: ["busybox", "latest"] -`) +`, nil) assert.Error(t, err) assert.Contains(t, err.Error(), "services.foo.image must be a string") } @@ -383,6 +383,7 @@ services: FOO: "1" BAR: 2 BAZ: 2.5 + QUX: QUUX: list-env: image: busybox @@ -390,14 +391,16 @@ services: - FOO=1 - BAR=2 - BAZ=2.5 + - QUX - QUUX= -`) +`, map[string]string{"QUX": "qux"}) assert.NoError(t, err) expected := types.MappingWithEquals{ "FOO": "1", "BAR": "2", "BAZ": "2.5", + "QUX": "qux", "QUUX": "", } @@ -416,7 +419,7 @@ services: image: busybox environment: FOO: ["1"] -`) +`, nil) assert.Error(t, err) assert.Contains(t, err.Error(), "services.dict-env.environment.FOO must be a string, number or null") } @@ -428,12 +431,13 @@ services: dict-env: image: busybox environment: "FOO=1" -`) +`, nil) assert.Error(t, err) assert.Contains(t, err.Error(), "services.dict-env.environment must be a mapping") } func TestEnvironmentInterpolation(t *testing.T) { + home := "/home/foo" config, err := loadYAML(` version: "3" services: @@ -450,12 +454,13 @@ networks: volumes: test: driver: $HOME -`) +`, map[string]string{ + "HOME": home, + "FOO": "foo", + }) assert.NoError(t, err) - home := os.Getenv("HOME") - expectedLabels := types.MappingWithEquals{ "home1": home, "home2": home, @@ -483,7 +488,7 @@ services: `)) assert.NoError(t, err) - configDetails := buildConfigDetails(dict) + configDetails := buildConfigDetails(dict, nil) _, err = Load(configDetails) assert.NoError(t, err) @@ -506,7 +511,7 @@ services: `)) assert.NoError(t, err) - configDetails := buildConfigDetails(dict) + configDetails := buildConfigDetails(dict, nil) _, err = Load(configDetails) assert.NoError(t, err) @@ -529,7 +534,7 @@ services: bar: extends: service: foo -`) +`, nil) assert.Error(t, err) assert.IsType(t, &ForbiddenPropertiesError{}, err) @@ -601,7 +606,8 @@ func TestFullExample(t *testing.T) { bytes, err := ioutil.ReadFile("full-example.yml") assert.NoError(t, err) - config, err := loadYAML(string(bytes)) + homeDir := "/home/foo" + config, err := loadYAML(string(bytes), map[string]string{"HOME": homeDir, "QUX": "2"}) if !assert.NoError(t, err) { return } @@ -609,7 +615,6 @@ func TestFullExample(t *testing.T) { workingDir, err := os.Getwd() assert.NoError(t, err) - homeDir := os.Getenv("HOME") stopGracePeriod := time.Duration(20 * time.Second) expectedServiceConfig := types.ServiceConfig{ @@ -664,6 +669,7 @@ func TestFullExample(t *testing.T) { "FOO": "1", "BAR": "2", "BAZ": "3", + "QUX": "2", }, EnvFile: []string{ "./example1.env", @@ -955,13 +961,13 @@ func TestFullExample(t *testing.T) { assert.Equal(t, expectedVolumeConfig, config.Volumes) } -func loadYAML(yaml string) (*types.Config, error) { +func loadYAML(yaml string, env map[string]string) (*types.Config, error) { dict, err := ParseYAML([]byte(yaml)) if err != nil { return nil, err } - return Load(buildConfigDetails(dict)) + return Load(buildConfigDetails(dict, env)) } func serviceSort(services []types.ServiceConfig) []types.ServiceConfig { From 63bca6acc7e10ed11e13b53e172f71610c61cee0 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 14 Mar 2017 12:39:26 -0400 Subject: [PATCH 661/978] Fix environment resolving. Load from env should only happen if the value is unset. Extract a buildEnvironment function and revert some changes to tests. Signed-off-by: Daniel Nephin Upstream-commit: 146d3eb3049d16058d2ae52464ec944d5ebeb18b Component: cli --- .../cli/command/stack/deploy_composefile.go | 18 ++- components/cli/compose/convert/service.go | 9 +- .../cli/compose/convert/service_test.go | 10 +- components/cli/compose/loader/example1.env | 6 +- components/cli/compose/loader/example2.env | 4 +- .../cli/compose/loader/full-example.yml | 6 +- components/cli/compose/loader/loader.go | 79 ++++++------ components/cli/compose/loader/loader_test.go | 118 +++++++++--------- components/cli/compose/types/types.go | 15 ++- 9 files changed, 147 insertions(+), 118 deletions(-) diff --git a/components/cli/command/stack/deploy_composefile.go b/components/cli/command/stack/deploy_composefile.go index b176b47e0b..f415f42f8c 100644 --- a/components/cli/command/stack/deploy_composefile.go +++ b/components/cli/command/stack/deploy_composefile.go @@ -15,6 +15,7 @@ import ( composetypes "github.com/docker/docker/cli/compose/types" apiclient "github.com/docker/docker/client" dockerclient "github.com/docker/docker/client" + "github.com/pkg/errors" "golang.org/x/net/context" ) @@ -115,17 +116,24 @@ func getConfigDetails(opts deployOptions) (composetypes.ConfigDetails, error) { } // TODO: support multiple files details.ConfigFiles = []composetypes.ConfigFile{*configFile} - env := os.Environ() - details.Environment = make(map[string]string, len(env)) + details.Environment, err = buildEnvironment(os.Environ()) + if err != nil { + return details, err + } + return details, nil +} + +func buildEnvironment(env []string) (map[string]string, error) { + result := make(map[string]string, len(env)) for _, s := range env { // if value is empty, s is like "K=", not "K". if !strings.Contains(s, "=") { - return details, fmt.Errorf("unexpected environment %q", s) + return result, errors.Errorf("unexpected environment %q", s) } kv := strings.SplitN(s, "=", 2) - details.Environment[kv[0]] = kv[1] + result[kv[0]] = kv[1] } - return details, nil + return result, nil } func getConfigFile(filename string) (*composetypes.ConfigFile, error) { diff --git a/components/cli/compose/convert/service.go b/components/cli/compose/convert/service.go index ab90d7319a..6b542f7701 100644 --- a/components/cli/compose/convert/service.go +++ b/components/cli/compose/convert/service.go @@ -393,11 +393,16 @@ func convertEndpointSpec(endpointMode string, source []composetypes.ServicePortC }, nil } -func convertEnvironment(source map[string]string) []string { +func convertEnvironment(source map[string]*string) []string { var output []string for name, value := range source { - output = append(output, fmt.Sprintf("%s=%s", name, value)) + switch value { + case nil: + output = append(output, name) + default: + output = append(output, fmt.Sprintf("%s=%s", name, *value)) + } } return output diff --git a/components/cli/compose/convert/service_test.go b/components/cli/compose/convert/service_test.go index 56f495df3f..352e9a61b5 100644 --- a/components/cli/compose/convert/service_test.go +++ b/components/cli/compose/convert/service_test.go @@ -43,10 +43,14 @@ func TestConvertRestartPolicyFromFailure(t *testing.T) { assert.DeepEqual(t, policy, expected) } +func strPtr(val string) *string { + return &val +} + func TestConvertEnvironment(t *testing.T) { - source := map[string]string{ - "foo": "bar", - "key": "value", + source := map[string]*string{ + "foo": strPtr("bar"), + "key": strPtr("value"), } env := convertEnvironment(source) sort.Strings(env) diff --git a/components/cli/compose/loader/example1.env b/components/cli/compose/loader/example1.env index 3e7a059613..f19ec0df4e 100644 --- a/components/cli/compose/loader/example1.env +++ b/components/cli/compose/loader/example1.env @@ -1,8 +1,8 @@ # passed through -FOO=1 +FOO=foo_from_env_file # overridden in example2.env -BAR=1 +BAR=bar_from_env_file # overridden in full-example.yml -BAZ=1 +BAZ=baz_from_env_file diff --git a/components/cli/compose/loader/example2.env b/components/cli/compose/loader/example2.env index 642334e9fd..f47d1e6145 100644 --- a/components/cli/compose/loader/example2.env +++ b/components/cli/compose/loader/example2.env @@ -1,4 +1,4 @@ -BAR=2 +BAR=bar_from_env_file_2 # overridden in configDetails.Environment -QUX=1 +QUX=quz_from_env_file_2 diff --git a/components/cli/compose/loader/full-example.yml b/components/cli/compose/loader/full-example.yml index fb5686a380..e8f3716013 100644 --- a/components/cli/compose/loader/full-example.yml +++ b/components/cli/compose/loader/full-example.yml @@ -77,10 +77,8 @@ services: # Mapping values can be strings, numbers or null # Booleans are not allowed - must be quoted environment: - RACK_ENV: development - SHOW: 'true' - SESSION_SECRET: - BAZ: 3 + BAZ: baz_from_service_def + QUX: # environment: # - RACK_ENV=development # - SHOW=true diff --git a/components/cli/compose/loader/loader.go b/components/cli/compose/loader/loader.go index 7c8bfa0a2a..3edcd81668 100644 --- a/components/cli/compose/loader/loader.go +++ b/components/cli/compose/loader/loader.go @@ -253,9 +253,11 @@ func transformHook( case reflect.TypeOf(map[string]*types.ServiceNetworkConfig{}): return transformServiceNetworkMap(data) case reflect.TypeOf(types.MappingWithEquals{}): - return transformMappingOrList(data, "="), nil + return transformMappingOrList(data, "=", true), nil + case reflect.TypeOf(types.Labels{}): + return transformMappingOrList(data, "=", false), nil case reflect.TypeOf(types.MappingWithColon{}): - return transformMappingOrList(data, ":"), nil + return transformMappingOrList(data, ":", false), nil case reflect.TypeOf(types.ServiceVolumeConfig{}): return transformServiceVolumeConfig(data) } @@ -317,7 +319,7 @@ func LoadServices(servicesDict types.Dict, workingDir string, lookupEnv template var services []types.ServiceConfig for name, serviceDef := range servicesDict { - serviceConfig, err := loadService(name, serviceDef.(types.Dict), workingDir, lookupEnv) + serviceConfig, err := LoadService(name, serviceDef.(types.Dict), workingDir, lookupEnv) if err != nil { return nil, err } @@ -344,25 +346,20 @@ func LoadService(name string, serviceDict types.Dict, workingDir string, lookupE return serviceConfig, nil } -func updateEnvironment(environment map[string]string, vars map[string]string, lookupEnv template.Mapping) map[string]string { - result := make(map[string]string, len(environment)) - for k, v := range environment { - result[k]=v - } +func updateEnvironment(environment map[string]*string, vars map[string]*string, lookupEnv template.Mapping) { for k, v := range vars { interpolatedV, ok := lookupEnv(k) - if ok { + if (v == nil || *v == "") && ok { // lookupEnv is prioritized over vars - result[k] = interpolatedV + environment[k] = &interpolatedV } else { - result[k] = v + environment[k] = v } } - return result } func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string, lookupEnv template.Mapping) error { - environment := make(map[string]string) + environment := make(map[string]*string) if len(serviceConfig.EnvFile) > 0 { var envVars []string @@ -375,12 +372,12 @@ func resolveEnvironment(serviceConfig *types.ServiceConfig, workingDir string, l } envVars = append(envVars, fileVars...) } - environment = updateEnvironment(environment, - runconfigopts.ConvertKVStringsToMap(envVars), lookupEnv) + updateEnvironment(environment, + runconfigopts.ConvertKVStringsToMapWithNil(envVars), lookupEnv) } - serviceConfig.Environment = updateEnvironment(environment, - serviceConfig.Environment, lookupEnv) + updateEnvironment(environment, serviceConfig.Environment, lookupEnv) + serviceConfig.Environment = environment return nil } @@ -497,9 +494,9 @@ func absPath(workingDir string, filepath string) string { func transformMapStringString(data interface{}) (interface{}, error) { switch value := data.(type) { case map[string]interface{}: - return toMapStringString(value), nil + return toMapStringString(value, false), nil case types.Dict: - return toMapStringString(value), nil + return toMapStringString(value, false), nil case map[string]string: return value, nil default: @@ -613,23 +610,27 @@ func transformStringList(data interface{}) (interface{}, error) { } } -func transformMappingOrList(mappingOrList interface{}, sep string) map[string]string { - if mapping, ok := mappingOrList.(types.Dict); ok { - return toMapStringString(mapping) - } - if list, ok := mappingOrList.([]interface{}); ok { - result := make(map[string]string) - for _, value := range list { +func transformMappingOrList(mappingOrList interface{}, sep string, allowNil bool) interface{} { + switch value := mappingOrList.(type) { + case types.Dict: + return toMapStringString(value, allowNil) + case ([]interface{}): + result := make(map[string]interface{}) + for _, value := range value { parts := strings.SplitN(value.(string), sep, 2) - if len(parts) == 1 { - result[parts[0]] = "" - } else { - result[parts[0]] = parts[1] + key := parts[0] + switch { + case len(parts) == 1 && allowNil: + result[key] = nil + case len(parts) == 1 && !allowNil: + result[key] = "" + default: + result[key] = parts[1] } } return result } - panic(fmt.Errorf("expected a map or a slice, got: %#v", mappingOrList)) + panic(fmt.Errorf("expected a map or a list, got %T: %#v", mappingOrList, mappingOrList)) } func transformShellCommand(value interface{}) (interface{}, error) { @@ -693,17 +694,21 @@ func toServicePortConfigs(value string) ([]interface{}, error) { return portConfigs, nil } -func toMapStringString(value map[string]interface{}) map[string]string { - output := make(map[string]string) +func toMapStringString(value map[string]interface{}, allowNil bool) map[string]interface{} { + output := make(map[string]interface{}) for key, value := range value { - output[key] = toString(value) + output[key] = toString(value, allowNil) } return output } -func toString(value interface{}) string { - if value == nil { +func toString(value interface{}, allowNil bool) interface{} { + switch { + case value != nil: + return fmt.Sprint(value) + case allowNil: + return nil + default: return "" } - return fmt.Sprint(value) } diff --git a/components/cli/compose/loader/loader_test.go b/components/cli/compose/loader/loader_test.go index 4f424d6126..661e2c615c 100644 --- a/components/cli/compose/loader/loader_test.go +++ b/components/cli/compose/loader/loader_test.go @@ -27,6 +27,19 @@ func buildConfigDetails(source types.Dict, env map[string]string) types.ConfigDe } } +func loadYAML(yaml string) (*types.Config, error) { + return loadYAMLWithEnv(yaml, nil) +} + +func loadYAMLWithEnv(yaml string, env map[string]string) (*types.Config, error) { + dict, err := ParseYAML([]byte(yaml)) + if err != nil { + return nil, err + } + + return Load(buildConfigDetails(dict, env)) +} + var sampleYAML = ` version: "3" services: @@ -98,12 +111,16 @@ var sampleDict = types.Dict{ }, } +func strPtr(val string) *string { + return &val +} + var sampleConfig = types.Config{ Services: []types.ServiceConfig{ { Name: "foo", Image: "busybox", - Environment: map[string]string{}, + Environment: map[string]*string{}, Networks: map[string]*types.ServiceNetworkConfig{ "with_me": nil, }, @@ -111,7 +128,7 @@ var sampleConfig = types.Config{ { Name: "bar", Image: "busybox", - Environment: map[string]string{"FOO": "1"}, + Environment: map[string]*string{"FOO": strPtr("1")}, Networks: map[string]*types.ServiceNetworkConfig{ "with_ipam": nil, }, @@ -173,7 +190,7 @@ services: secrets: super: external: true -`, nil) +`) if !assert.NoError(t, err) { return } @@ -182,7 +199,7 @@ secrets: } func TestParseAndLoad(t *testing.T) { - actual, err := loadYAML(sampleYAML, nil) + actual, err := loadYAML(sampleYAML) if !assert.NoError(t, err) { return } @@ -192,15 +209,15 @@ func TestParseAndLoad(t *testing.T) { } func TestInvalidTopLevelObjectType(t *testing.T) { - _, err := loadYAML("1", nil) + _, err := loadYAML("1") assert.Error(t, err) assert.Contains(t, err.Error(), "Top-level object must be a mapping") - _, err = loadYAML("\"hello\"", nil) + _, err = loadYAML("\"hello\"") assert.Error(t, err) assert.Contains(t, err.Error(), "Top-level object must be a mapping") - _, err = loadYAML("[\"hello\"]", nil) + _, err = loadYAML("[\"hello\"]") assert.Error(t, err) assert.Contains(t, err.Error(), "Top-level object must be a mapping") } @@ -211,7 +228,7 @@ version: "3" 123: foo: image: busybox -`, nil) +`) assert.Error(t, err) assert.Contains(t, err.Error(), "Non-string key at top level: 123") @@ -222,7 +239,7 @@ services: image: busybox 123: image: busybox -`, nil) +`) assert.Error(t, err) assert.Contains(t, err.Error(), "Non-string key in services: 123") @@ -236,7 +253,7 @@ networks: ipam: config: - 123: oh dear -`, nil) +`) assert.Error(t, err) assert.Contains(t, err.Error(), "Non-string key in networks.default.ipam.config[0]: 123") @@ -247,7 +264,7 @@ services: image: busybox environment: 1: FOO -`, nil) +`) assert.Error(t, err) assert.Contains(t, err.Error(), "Non-string key in services.dict-env.environment: 1") } @@ -258,7 +275,7 @@ version: "3" services: foo: image: busybox -`, nil) +`) assert.NoError(t, err) _, err = loadYAML(` @@ -266,7 +283,7 @@ version: "3.0" services: foo: image: busybox -`, nil) +`) assert.NoError(t, err) } @@ -276,7 +293,7 @@ version: "2" services: foo: image: busybox -`, nil) +`) assert.Error(t, err) assert.Contains(t, err.Error(), "version") @@ -285,7 +302,7 @@ version: "2.0" services: foo: image: busybox -`, nil) +`) assert.Error(t, err) assert.Contains(t, err.Error(), "version") } @@ -296,7 +313,7 @@ version: 3 services: foo: image: busybox -`, nil) +`) assert.Error(t, err) assert.Contains(t, err.Error(), "version must be a string") } @@ -305,7 +322,7 @@ func TestV1Unsupported(t *testing.T) { _, err := loadYAML(` foo: image: busybox -`, nil) +`) assert.Error(t, err) } @@ -315,7 +332,7 @@ version: "3" services: - foo: image: busybox -`, nil) +`) assert.Error(t, err) assert.Contains(t, err.Error(), "services must be a mapping") @@ -323,7 +340,7 @@ services: version: "3" services: foo: busybox -`, nil) +`) assert.Error(t, err) assert.Contains(t, err.Error(), "services.foo must be a mapping") @@ -332,7 +349,7 @@ version: "3" networks: - default: driver: bridge -`, nil) +`) assert.Error(t, err) assert.Contains(t, err.Error(), "networks must be a mapping") @@ -340,7 +357,7 @@ networks: version: "3" networks: default: bridge -`, nil) +`) assert.Error(t, err) assert.Contains(t, err.Error(), "networks.default must be a mapping") @@ -349,7 +366,7 @@ version: "3" volumes: - data: driver: local -`, nil) +`) assert.Error(t, err) assert.Contains(t, err.Error(), "volumes must be a mapping") @@ -357,7 +374,7 @@ volumes: version: "3" volumes: data: local -`, nil) +`) assert.Error(t, err) assert.Contains(t, err.Error(), "volumes.data must be a mapping") } @@ -368,13 +385,13 @@ version: "3" services: foo: image: ["busybox", "latest"] -`, nil) +`) assert.Error(t, err) assert.Contains(t, err.Error(), "services.foo.image must be a string") } -func TestValidEnvironment(t *testing.T) { - config, err := loadYAML(` +func TestLoadWithEnvironment(t *testing.T) { + config, err := loadYAMLWithEnv(` version: "3" services: dict-env: @@ -391,17 +408,17 @@ services: - FOO=1 - BAR=2 - BAZ=2.5 - - QUX - - QUUX= + - QUX= + - QUUX `, map[string]string{"QUX": "qux"}) assert.NoError(t, err) expected := types.MappingWithEquals{ - "FOO": "1", - "BAR": "2", - "BAZ": "2.5", - "QUX": "qux", - "QUUX": "", + "FOO": strPtr("1"), + "BAR": strPtr("2"), + "BAZ": strPtr("2.5"), + "QUX": strPtr("qux"), + "QUUX": nil, } assert.Equal(t, 2, len(config.Services)) @@ -419,7 +436,7 @@ services: image: busybox environment: FOO: ["1"] -`, nil) +`) assert.Error(t, err) assert.Contains(t, err.Error(), "services.dict-env.environment.FOO must be a string, number or null") } @@ -431,14 +448,14 @@ services: dict-env: image: busybox environment: "FOO=1" -`, nil) +`) assert.Error(t, err) assert.Contains(t, err.Error(), "services.dict-env.environment must be a mapping") } func TestEnvironmentInterpolation(t *testing.T) { home := "/home/foo" - config, err := loadYAML(` + config, err := loadYAMLWithEnv(` version: "3" services: test: @@ -461,7 +478,7 @@ volumes: assert.NoError(t, err) - expectedLabels := types.MappingWithEquals{ + expectedLabels := types.Labels{ "home1": home, "home2": home, "nonexistent": "", @@ -534,7 +551,7 @@ services: bar: extends: service: foo -`, nil) +`) assert.Error(t, err) assert.IsType(t, &ForbiddenPropertiesError{}, err) @@ -607,7 +624,8 @@ func TestFullExample(t *testing.T) { assert.NoError(t, err) homeDir := "/home/foo" - config, err := loadYAML(string(bytes), map[string]string{"HOME": homeDir, "QUX": "2"}) + env := map[string]string{"HOME": homeDir, "QUX": "qux_from_environment"} + config, err := loadYAMLWithEnv(string(bytes), env) if !assert.NoError(t, err) { return } @@ -662,14 +680,11 @@ func TestFullExample(t *testing.T) { DNSSearch: []string{"dc1.example.com", "dc2.example.com"}, DomainName: "foo.com", Entrypoint: []string{"/code/entrypoint.sh", "-p", "3000"}, - Environment: map[string]string{ - "RACK_ENV": "development", - "SHOW": "true", - "SESSION_SECRET": "", - "FOO": "1", - "BAR": "2", - "BAZ": "3", - "QUX": "2", + Environment: map[string]*string{ + "FOO": strPtr("foo_from_env_file"), + "BAR": strPtr("bar_from_env_file_2"), + "BAZ": strPtr("baz_from_service_def"), + "QUX": strPtr("qux_from_environment"), }, EnvFile: []string{ "./example1.env", @@ -961,15 +976,6 @@ func TestFullExample(t *testing.T) { assert.Equal(t, expectedVolumeConfig, config.Volumes) } -func loadYAML(yaml string, env map[string]string) (*types.Config, error) { - dict, err := ParseYAML([]byte(yaml)) - if err != nil { - return nil, err - } - - return Load(buildConfigDetails(dict, env)) -} - func serviceSort(services []types.ServiceConfig) []types.ServiceConfig { sort.Sort(servicesByName(services)) return services diff --git a/components/cli/compose/types/types.go b/components/cli/compose/types/types.go index e91b5a7ac8..bb12f8497b 100644 --- a/components/cli/compose/types/types.go +++ b/components/cli/compose/types/types.go @@ -99,7 +99,7 @@ type ServiceConfig struct { HealthCheck *HealthCheckConfig Image string Ipc string - Labels MappingWithEquals + Labels Labels Links []string Logging *LoggingConfig MacAddress string `mapstructure:"mac_address"` @@ -135,7 +135,10 @@ type StringOrNumberList []string // MappingWithEquals is a mapping type that can be converted from a list of // key=value strings -type MappingWithEquals map[string]string +type MappingWithEquals map[string]*string + +// Labels is a mapping type for labels +type Labels map[string]string // MappingWithColon is a mapping type that can be converted from a list of // 'key: value' strings @@ -151,7 +154,7 @@ type LoggingConfig struct { type DeployConfig struct { Mode string Replicas *uint64 - Labels MappingWithEquals + Labels Labels UpdateConfig *UpdateConfig `mapstructure:"update_config"` Resources Resources RestartPolicy *RestartPolicy `mapstructure:"restart_policy"` @@ -268,7 +271,7 @@ type NetworkConfig struct { External External Internal bool Attachable bool - Labels MappingWithEquals + Labels Labels } // IPAMConfig for a network @@ -287,7 +290,7 @@ type VolumeConfig struct { Driver string DriverOpts map[string]string `mapstructure:"driver_opts"` External External - Labels MappingWithEquals + Labels Labels } // External identifies a Volume or Network as a reference to a resource that is @@ -301,5 +304,5 @@ type External struct { type SecretConfig struct { File string External External - Labels MappingWithEquals + Labels Labels } From 0ed0e112c48c4615bd3aa24196cfb3d9c56ae53c Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 22 Feb 2017 15:43:13 -0500 Subject: [PATCH 662/978] Add --prune to stack deploy. Add to command line reference. Signed-off-by: Daniel Nephin Upstream-commit: b1a98b55af8d3840820037d89dac252860918db0 Component: cli --- components/cli/command/stack/deploy.go | 24 +++++++++ .../cli/command/stack/deploy_bundlefile.go | 8 +++ .../cli/command/stack/deploy_composefile.go | 9 +++- components/cli/command/stack/deploy_test.go | 54 +++++++++++++++++++ components/cli/command/stack/remove.go | 6 +-- components/cli/compose/convert/compose.go | 6 +++ components/cli/internal/test/cli.go | 2 +- 7 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 components/cli/command/stack/deploy_test.go diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index 22557fc45b..46af5f63b1 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -3,8 +3,10 @@ package stack import ( "fmt" + "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/compose/convert" "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" @@ -19,6 +21,7 @@ type deployOptions struct { composefile string namespace string sendRegistryAuth bool + prune bool } func newDeployCommand(dockerCli *command.DockerCli) *cobra.Command { @@ -39,6 +42,8 @@ func newDeployCommand(dockerCli *command.DockerCli) *cobra.Command { addBundlefileFlag(&opts.bundlefile, flags) addComposefileFlag(&opts.composefile, flags) addRegistryAuthFlag(&opts.sendRegistryAuth, flags) + flags.BoolVar(&opts.prune, "prune", false, "Prune services that are no longer referenced") + flags.SetAnnotation("prune", "version", []string{"1.27"}) return cmd } @@ -71,3 +76,22 @@ func checkDaemonIsSwarmManager(ctx context.Context, dockerCli *command.DockerCli } return nil } + +// pruneServices removes services that are no longer referenced in the source +func pruneServices(ctx context.Context, dockerCli command.Cli, namespace convert.Namespace, services map[string]struct{}) bool { + client := dockerCli.Client() + + oldServices, err := getServices(ctx, client, namespace.Name()) + if err != nil { + fmt.Fprintf(dockerCli.Err(), "Failed to list services: %s", err) + return true + } + + pruneServices := []swarm.Service{} + for _, service := range oldServices { + if _, exists := services[namespace.Descope(service.Spec.Name)]; !exists { + pruneServices = append(pruneServices, service) + } + } + return removeServices(ctx, dockerCli, pruneServices) +} diff --git a/components/cli/command/stack/deploy_bundlefile.go b/components/cli/command/stack/deploy_bundlefile.go index 5a178c4ab6..14e627cafc 100644 --- a/components/cli/command/stack/deploy_bundlefile.go +++ b/components/cli/command/stack/deploy_bundlefile.go @@ -21,6 +21,14 @@ func deployBundle(ctx context.Context, dockerCli *command.DockerCli, opts deploy namespace := convert.NewNamespace(opts.namespace) + if opts.prune { + services := map[string]struct{}{} + for service := range bundle.Services { + services[service] = struct{}{} + } + pruneServices(ctx, dockerCli, namespace, services) + } + networks := make(map[string]types.NetworkCreate) for _, service := range bundle.Services { for _, networkName := range service.Networks { diff --git a/components/cli/command/stack/deploy_composefile.go b/components/cli/command/stack/deploy_composefile.go index 3e62494325..f8951e06ee 100644 --- a/components/cli/command/stack/deploy_composefile.go +++ b/components/cli/command/stack/deploy_composefile.go @@ -52,8 +52,15 @@ func deployCompose(ctx context.Context, dockerCli *command.DockerCli, opts deplo namespace := convert.NewNamespace(opts.namespace) - serviceNetworks := getServicesDeclaredNetworks(config.Services) + if opts.prune { + services := map[string]struct{}{} + for _, service := range config.Services { + services[service.Name] = struct{}{} + } + pruneServices(ctx, dockerCli, namespace, services) + } + serviceNetworks := getServicesDeclaredNetworks(config.Services) networks, externalNetworks := convert.Networks(namespace, config.Networks, serviceNetworks) if err := validateExternalNetworks(ctx, dockerCli, externalNetworks); err != nil { return err diff --git a/components/cli/command/stack/deploy_test.go b/components/cli/command/stack/deploy_test.go new file mode 100644 index 0000000000..dac1350547 --- /dev/null +++ b/components/cli/command/stack/deploy_test.go @@ -0,0 +1,54 @@ +package stack + +import ( + "bytes" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/compose/convert" + "github.com/docker/docker/cli/internal/test" + "github.com/docker/docker/client" + "github.com/docker/docker/pkg/testutil/assert" + "golang.org/x/net/context" +) + +type fakeClient struct { + client.Client + serviceList []string + removedIDs []string +} + +func (cli *fakeClient) ServiceList(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) { + services := []swarm.Service{} + for _, name := range cli.serviceList { + services = append(services, swarm.Service{ + ID: name, + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{Name: name}, + }, + }) + } + return services, nil +} + +func (cli *fakeClient) ServiceRemove(ctx context.Context, serviceID string) error { + cli.removedIDs = append(cli.removedIDs, serviceID) + return nil +} + +func TestPruneServices(t *testing.T) { + ctx := context.Background() + namespace := convert.NewNamespace("foo") + services := map[string]struct{}{ + "new": {}, + "keep": {}, + } + client := &fakeClient{serviceList: []string{"foo_keep", "foo_remove"}} + dockerCli := test.NewFakeCli(client, &bytes.Buffer{}) + dockerCli.SetErr(&bytes.Buffer{}) + + pruneServices(ctx, dockerCli, namespace, services) + + assert.DeepEqual(t, client.removedIDs, []string{"foo_remove"}) +} diff --git a/components/cli/command/stack/remove.go b/components/cli/command/stack/remove.go index 966c1aa6bf..d466caf2b4 100644 --- a/components/cli/command/stack/remove.go +++ b/components/cli/command/stack/remove.go @@ -68,7 +68,7 @@ func runRemove(dockerCli *command.DockerCli, opts removeOptions) error { func removeServices( ctx context.Context, - dockerCli *command.DockerCli, + dockerCli command.Cli, services []swarm.Service, ) bool { var err error @@ -83,7 +83,7 @@ func removeServices( func removeNetworks( ctx context.Context, - dockerCli *command.DockerCli, + dockerCli command.Cli, networks []types.NetworkResource, ) bool { var err error @@ -98,7 +98,7 @@ func removeNetworks( func removeSecrets( ctx context.Context, - dockerCli *command.DockerCli, + dockerCli command.Cli, secrets []swarm.Secret, ) bool { var err error diff --git a/components/cli/compose/convert/compose.go b/components/cli/compose/convert/compose.go index a4571df02f..d7208bfc5d 100644 --- a/components/cli/compose/convert/compose.go +++ b/components/cli/compose/convert/compose.go @@ -2,6 +2,7 @@ package convert import ( "io/ioutil" + "strings" "github.com/docker/docker/api/types" networktypes "github.com/docker/docker/api/types/network" @@ -24,6 +25,11 @@ func (n Namespace) Scope(name string) string { return n.name + "_" + name } +// Descope returns the name without the namespace prefix +func (n Namespace) Descope(name string) string { + return strings.TrimPrefix(name, n.name+"_") +} + // Name returns the name of the namespace func (n Namespace) Name() string { return n.name diff --git a/components/cli/internal/test/cli.go b/components/cli/internal/test/cli.go index 72de42586d..610918a651 100644 --- a/components/cli/internal/test/cli.go +++ b/components/cli/internal/test/cli.go @@ -35,7 +35,7 @@ func (c *FakeCli) SetIn(in io.ReadCloser) { c.in = in } -// SetErr sets the standard error stream th cli should write on +// SetErr sets the stderr stream for the cli to the specified io.Writer func (c *FakeCli) SetErr(err io.Writer) { c.err = err } From f462549c132d352e5871182714724f2ea33dfe27 Mon Sep 17 00:00:00 2001 From: erxian Date: Mon, 13 Mar 2017 15:28:23 +0800 Subject: [PATCH 663/978] misleading default for --update-monitor duration Signed-off-by: erxian Upstream-commit: 88a99ae70e3cbf3b812302ae6b4f9bca9cd28aeb Component: cli --- components/cli/command/service/opts.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index baaa58e1f0..0c4d41de17 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -491,7 +491,7 @@ func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { flags.Uint64Var(&opts.update.parallelism, flagUpdateParallelism, 1, "Maximum number of tasks updated simultaneously (0 to update all at once)") flags.DurationVar(&opts.update.delay, flagUpdateDelay, time.Duration(0), "Delay between updates (ns|us|ms|s|m|h) (default 0s)") - flags.DurationVar(&opts.update.monitor, flagUpdateMonitor, time.Duration(0), "Duration after each task update to monitor for failure (ns|us|ms|s|m|h) (default 0s)") + flags.DurationVar(&opts.update.monitor, flagUpdateMonitor, time.Duration(0), "Duration after each task update to monitor for failure (ns|us|ms|s|m|h)") flags.SetAnnotation(flagUpdateMonitor, "version", []string{"1.25"}) flags.StringVar(&opts.update.onFailure, flagUpdateFailureAction, "pause", `Action on update failure ("pause"|"continue"|"rollback")`) flags.Var(&opts.update.maxFailureRatio, flagUpdateMaxFailureRatio, "Failure rate to tolerate during an update") From 2524c32d21662528c5df26cb751f059973b9090e Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Wed, 15 Mar 2017 10:25:36 -0700 Subject: [PATCH 664/978] Revert "Planned 1.13 deprecation: email from login" This reverts commit a66efbddb8eaa837cf42aae20b76c08274271dcf. Signed-off-by: Victor Vieux Upstream-commit: bc771127d8cdf3142b48f7bf4bbfd0a391a47796 Component: cli --- components/cli/command/registry/login.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/components/cli/command/registry/login.go b/components/cli/command/registry/login.go index bdcc9a103b..5194c7e8c2 100644 --- a/components/cli/command/registry/login.go +++ b/components/cli/command/registry/login.go @@ -35,9 +35,14 @@ func NewLoginCommand(dockerCli *command.DockerCli) *cobra.Command { } flags := cmd.Flags() + flags.StringVarP(&opts.user, "username", "u", "", "Username") flags.StringVarP(&opts.password, "password", "p", "", "Password") + // Deprecated in 1.11: Should be removed in docker 17.06 + flags.StringVarP(&opts.email, "email", "e", "", "Email") + flags.MarkDeprecated("email", "will be removed in 17.06.") + return cmd } From 559c313548d6bf7c477a62343a08dac9470a6a24 Mon Sep 17 00:00:00 2001 From: Gaetan de Villele Date: Wed, 15 Mar 2017 13:49:52 -0700 Subject: [PATCH 665/978] improve semantics of utility function in cli/command/service Signed-off-by: Gaetan de Villele Upstream-commit: 59c79325bcc6adb2f620cc877b970cbc73f198dd Component: cli --- components/cli/command/service/create.go | 2 +- components/cli/command/service/opts.go | 6 ++---- components/cli/command/service/update.go | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/components/cli/command/service/create.go b/components/cli/command/service/create.go index fc1ecbd9fe..7fd0884930 100644 --- a/components/cli/command/service/create.go +++ b/components/cli/command/service/create.go @@ -29,7 +29,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.StringVar(&opts.mode, flagMode, "replicated", "Service mode (replicated or global)") flags.StringVar(&opts.name, flagName, "", "Service name") - addServiceFlags(cmd, opts) + addServiceFlags(flags, opts) flags.VarP(&opts.labels, flagLabel, "l", "Service labels") flags.Var(&opts.containerLabels, flagContainerLabel, "Container labels") diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index 46fe919606..79126217a4 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -11,7 +11,7 @@ import ( "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/opts" runconfigopts "github.com/docker/docker/runconfig/opts" - "github.com/spf13/cobra" + "github.com/spf13/pflag" ) type int64Value interface { @@ -468,9 +468,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { // addServiceFlags adds all flags that are common to both `create` and `update`. // Any flags that are not common are added separately in the individual command -func addServiceFlags(cmd *cobra.Command, opts *serviceOptions) { - flags := cmd.Flags() - +func addServiceFlags(flags *pflag.FlagSet, opts *serviceOptions) { flags.StringVarP(&opts.workdir, flagWorkdir, "w", "", "Working directory inside the container") flags.StringVarP(&opts.user, flagUser, "u", "", "Username or UID (format: [:])") flags.StringVar(&opts.hostname, flagHostname, "", "Container hostname") diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index fc6a229fa3..7c0ef2a810 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -43,7 +43,7 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.SetAnnotation("rollback", "version", []string{"1.25"}) flags.Bool("force", false, "Force update even if no changes require it") flags.SetAnnotation("force", "version", []string{"1.25"}) - addServiceFlags(cmd, serviceOpts) + addServiceFlags(flags, serviceOpts) flags.Var(newListOptsVar(), flagEnvRemove, "Remove an environment variable") flags.Var(newListOptsVar(), flagGroupRemove, "Remove a previously added supplementary user group from the container") From c74c88f8f767bffa63103c883f0f1431c30d7142 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Fri, 3 Mar 2017 13:42:43 +0100 Subject: [PATCH 666/978] Add missing API version annotations to commands Signed-off-by: Sebastiaan van Stijn Upstream-commit: d5d0d7795bd27cfbb7ca3488ac5e1b8f1d913f0e Component: cli --- components/cli/command/node/cmd.go | 1 + components/cli/command/plugin/cmd.go | 1 + components/cli/command/plugin/upgrade.go | 1 + components/cli/command/secret/cmd.go | 1 + components/cli/command/service/cmd.go | 1 + components/cli/command/swarm/cmd.go | 1 + components/cli/command/volume/cmd.go | 1 + 7 files changed, 7 insertions(+) diff --git a/components/cli/command/node/cmd.go b/components/cli/command/node/cmd.go index e71b9199ad..6bb6c3b28a 100644 --- a/components/cli/command/node/cmd.go +++ b/components/cli/command/node/cmd.go @@ -15,6 +15,7 @@ func NewNodeCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Manage Swarm nodes", Args: cli.NoArgs, RunE: dockerCli.ShowHelp, + Tags: map[string]string{"version": "1.24"}, } cmd.AddCommand( newDemoteCommand(dockerCli), diff --git a/components/cli/command/plugin/cmd.go b/components/cli/command/plugin/cmd.go index 92c990a975..33046d2cb8 100644 --- a/components/cli/command/plugin/cmd.go +++ b/components/cli/command/plugin/cmd.go @@ -13,6 +13,7 @@ func NewPluginCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Manage plugins", Args: cli.NoArgs, RunE: dockerCli.ShowHelp, + Tags: map[string]string{"version": "1.25"}, } cmd.AddCommand( diff --git a/components/cli/command/plugin/upgrade.go b/components/cli/command/plugin/upgrade.go index 07f0c7bb91..46efb096f9 100644 --- a/components/cli/command/plugin/upgrade.go +++ b/components/cli/command/plugin/upgrade.go @@ -26,6 +26,7 @@ func newUpgradeCommand(dockerCli *command.DockerCli) *cobra.Command { } return runUpgrade(dockerCli, options) }, + Tags: map[string]string{"version": "1.26"}, } flags := cmd.Flags() diff --git a/components/cli/command/secret/cmd.go b/components/cli/command/secret/cmd.go index 79e669858c..acaef4dcac 100644 --- a/components/cli/command/secret/cmd.go +++ b/components/cli/command/secret/cmd.go @@ -14,6 +14,7 @@ func NewSecretCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Manage Docker secrets", Args: cli.NoArgs, RunE: dockerCli.ShowHelp, + Tags: map[string]string{"version": "1.25"}, } cmd.AddCommand( newSecretListCommand(dockerCli), diff --git a/components/cli/command/service/cmd.go b/components/cli/command/service/cmd.go index 796fe926c3..51208b80c2 100644 --- a/components/cli/command/service/cmd.go +++ b/components/cli/command/service/cmd.go @@ -14,6 +14,7 @@ func NewServiceCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Manage services", Args: cli.NoArgs, RunE: dockerCli.ShowHelp, + Tags: map[string]string{"version": "1.24"}, } cmd.AddCommand( newCreateCommand(dockerCli), diff --git a/components/cli/command/swarm/cmd.go b/components/cli/command/swarm/cmd.go index 632679c4b6..659dbcdf7b 100644 --- a/components/cli/command/swarm/cmd.go +++ b/components/cli/command/swarm/cmd.go @@ -14,6 +14,7 @@ func NewSwarmCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Manage Swarm", Args: cli.NoArgs, RunE: dockerCli.ShowHelp, + Tags: map[string]string{"version": "1.24"}, } cmd.AddCommand( newInitCommand(dockerCli), diff --git a/components/cli/command/volume/cmd.go b/components/cli/command/volume/cmd.go index 4ef8381333..9086c99248 100644 --- a/components/cli/command/volume/cmd.go +++ b/components/cli/command/volume/cmd.go @@ -13,6 +13,7 @@ func NewVolumeCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Manage volumes", Args: cli.NoArgs, RunE: dockerCli.ShowHelp, + Tags: map[string]string{"version": "1.21"}, } cmd.AddCommand( newCreateCommand(dockerCli), From a85098e5f06779f52bbceb70998da427b707fd16 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Fri, 3 Mar 2017 13:58:50 +0100 Subject: [PATCH 667/978] Improve error handling of commands run against unsupported daemon The current error-handling only checked for version annotations on the subcommand itself, but did not check the top-level command. This patch always traverses the command path (parents), and prints an error if the command is not supported. Before this change: $ docker service Usage: docker service COMMAND Manage services Options: --help Print usage Commands: create Create a new service inspect Display detailed information on one or more services ls List services ps List the tasks of one or more services rm Remove one or more services scale Scale one or multiple replicated services update Update a service Run 'docker service COMMAND --help' for more information on a command. $ docker service ls ID NAME MODE REPLICAS IMAGE After this change: $ DOCKER_API_VERSION=1.12 docker service docker service requires API version 1.24, but the Docker daemon API version is 1.12 $ DOCKER_API_VERSION=1.12 docker service ls docker service ls requires API version 1.24, but the Docker daemon API version is 1.12 $ DOCKER_API_VERSION=1.24 docker plugin --help docker plugin requires API version 1.25, but the Docker daemon API version is 1.24 $ DOCKER_API_VERSION=1.25 docker plugin upgrade --help docker plugin upgrade requires API version 1.26, but the Docker daemon API version is 1.25 Signed-off-by: Sebastiaan van Stijn Upstream-commit: 998950a9f4a4afc10191147b2d108215de9353fe Component: cli --- components/cli/docker.go | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/components/cli/docker.go b/components/cli/docker.go index 570a52a72e..8d589d4416 100644 --- a/components/cli/docker.go +++ b/components/cli/docker.go @@ -229,17 +229,14 @@ func hideUnsupportedFeatures(cmd *cobra.Command, clientVersion, osType string, h } func isSupported(cmd *cobra.Command, clientVersion, osType string, hasExperimental bool) error { - // We check recursively so that, e.g., `docker stack ls` will return the same output as `docker stack` - if !hasExperimental { - for curr := cmd; curr != nil; curr = curr.Parent() { - if _, ok := curr.Tags["experimental"]; ok { - return errors.New("only supported on a Docker daemon with experimental features enabled") - } + // Check recursively so that, e.g., `docker stack ls` returns the same output as `docker stack` + for curr := cmd; curr != nil; curr = curr.Parent() { + if cmdVersion, ok := curr.Tags["version"]; ok && versions.LessThan(clientVersion, cmdVersion) { + return fmt.Errorf("%s requires API version %s, but the Docker daemon API version is %s", cmd.CommandPath(), cmdVersion, clientVersion) + } + if _, ok := curr.Tags["experimental"]; ok && !hasExperimental { + return fmt.Errorf("%s is only supported on a Docker daemon with experimental features enabled", cmd.CommandPath()) } - } - - if cmdVersion, ok := cmd.Tags["version"]; ok && versions.LessThan(clientVersion, cmdVersion) { - return fmt.Errorf("requires API version %s, but the Docker daemon API version is %s", cmdVersion, clientVersion) } errs := []string{} From e28e72773dfdaa903b35a0926ed6ec05be0b940b Mon Sep 17 00:00:00 2001 From: Pure White Date: Thu, 16 Mar 2017 22:33:24 +0800 Subject: [PATCH 668/978] fix a typo when i was using: docker search --automated -s 3 nginx told me: Flag --automated has been deprecated, use --filter=automated=true instead Flag --stars has been deprecated, use --filter=stars=3 instead and when i use: docker search --filter=automated=true --filter=stars=3 nginx told me: Error response from daemon: Invalid filter 'automated' and i found out that the correct command should be: docker search --filter=is-automated=true --filter=stars=3 nginx Signed-off-by: Pure White Upstream-commit: 7fe0d2d64d3234904c8ad964f6d69f6ef055e5d9 Component: cli --- components/cli/command/registry/search.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/registry/search.go b/components/cli/command/registry/search.go index bbcedbdd99..f534082d32 100644 --- a/components/cli/command/registry/search.go +++ b/components/cli/command/registry/search.go @@ -52,7 +52,7 @@ func NewSearchCommand(dockerCli *command.DockerCli) *cobra.Command { flags.BoolVar(&opts.automated, "automated", false, "Only show automated builds") flags.UintVarP(&opts.stars, "stars", "s", 0, "Only displays with at least x stars") - flags.MarkDeprecated("automated", "use --filter=automated=true instead") + flags.MarkDeprecated("automated", "use --filter=is-automated=true instead") flags.MarkDeprecated("stars", "use --filter=stars=3 instead") return cmd From db270254470e048db029e2d1a07a97c86927efa4 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 16 Mar 2017 13:53:49 -0400 Subject: [PATCH 669/978] Fix compose schema id for v3.2 Signed-off-by: Daniel Nephin Upstream-commit: d0fb25319b07fa577d14c320fbf40b2d0d826efc Component: cli --- components/cli/compose/schema/bindata.go | 2 +- components/cli/compose/schema/data/config_schema_v3.2.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/cli/compose/schema/bindata.go b/components/cli/compose/schema/bindata.go index 8857e36a85..e6ce0bfec2 100644 --- a/components/cli/compose/schema/bindata.go +++ b/components/cli/compose/schema/bindata.go @@ -110,7 +110,7 @@ func dataConfig_schema_v31Json() (*asset, error) { return a, nil } -var _dataConfig_schema_v32Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x5b\xcd\x73\xdc\x28\x16\xbf\xf7\x5f\xa1\x52\x72\x8b\x3f\xb2\xb5\xa9\xad\xda\xdc\xf6\xb8\xa7\x99\xf3\xb8\x3a\x2a\x1a\xbd\x56\x13\x4b\x40\x00\xb5\xdd\x49\xf9\x7f\x9f\xd2\x67\x03\x02\x81\xba\xe5\x38\x33\x35\x27\xdb\xe2\xf7\x80\xf7\xfd\x1e\xe0\x1f\x9b\x24\x49\xdf\x4b\x7c\x80\x0a\xa5\x9f\x93\xf4\xa0\x14\xff\x7c\x7f\xff\x55\x32\x7a\xdb\x7d\xbd\x63\xa2\xb8\xcf\x05\xda\xab\xdb\x8f\x9f\xee\xbb\x6f\xef\xd2\x9b\x86\x8e\xe4\x0d\x09\x66\x74\x4f\x8a\xac\x1b\xc9\x8e\xff\xbe\xfb\xd7\x5d\x43\xde\x41\xd4\x89\x43\x03\x62\xbb\xaf\x80\x55\xf7\x4d\xc0\xb7\x9a\x08\x68\x88\x1f\xd2\x23\x08\x49\x18\x4d\xb7\x37\x9b\x66\x8c\x0b\xc6\x41\x28\x02\x32\xfd\x9c\x34\x9b\x4b\x92\x11\x32\x7c\xd0\xa6\x95\x4a\x10\x5a\xa4\xed\xe7\x97\x76\x86\x24\x49\x25\x88\x23\xc1\xda\x0c\xe3\x56\xdf\xdd\x9f\xe7\xbf\x1f\x61\x37\xf6\xac\xda\x66\xdb\xef\x1c\x29\x05\x82\xfe\x3e\xdd\x5b\x3b\xfc\xe5\x01\xdd\x7e\xff\xdf\xed\x1f\x1f\x6f\xff\x7b\x97\xdd\x6e\x3f\xbc\x37\x86\x1b\xf9\x0a\xd8\x77\xcb\xe7\xb0\x27\x94\x28\xc2\xe8\xb8\x7e\x3a\x22\x5f\xfa\xdf\x5e\xc6\x85\x51\x9e\xb7\x60\x54\x1a\x6b\xef\x51\x29\xc1\xe4\x99\x82\x7a\x62\xe2\x31\xc4\xf3\x08\x7b\x23\x9e\xfb\xf5\x1d\x3c\x9b\xec\x1c\x59\x59\x57\x41\x0d\x0e\xa8\x37\x62\xa6\x5b\x7e\x1d\xfd\x49\xc0\x02\x54\xd8\x64\x3b\xd4\x9b\x59\x6c\xb3\xfc\x75\x0c\x6f\x06\xa6\x67\xb1\x1d\x42\x5b\xbb\xdd\xa0\xe1\xde\x2e\x51\xb9\xdc\xcb\x2f\xab\x51\x58\x1e\x29\xe5\xc0\x4b\x76\x6a\xbe\x79\xe4\xd1\x01\x2a\xa0\x2a\x1d\x45\x90\x24\xe9\xae\x26\x65\x6e\x4b\x94\x51\xf8\xad\x99\xe2\x41\xfb\x98\x24\x3f\xec\x48\xa6\xcd\xd3\x8e\x1b\x7f\xf9\x15\x3e\x8e\x7b\x78\x19\xc7\x31\xa3\x0a\x9e\x55\xcb\xd4\xfc\xd2\x9d\x08\x18\x7e\x04\xb1\x27\x25\xc4\x52\x20\x51\xc8\x19\x91\x95\x44\xaa\x8c\x89\x2c\x27\x58\x39\xe9\x31\xc2\x07\xc8\xf6\x82\x55\xc1\x59\xf6\x59\xb7\x0f\x99\xbe\x58\xf3\x4c\x26\x0e\x1b\xe6\x48\xaa\xfd\xb5\xdd\x38\x26\x4c\x31\xe2\x19\xca\x73\x43\x20\x48\x08\x74\x4a\x6f\x92\x94\x28\xa8\xa4\x5b\x56\x49\x5a\x53\xf2\xad\x86\xff\xf7\x10\x25\x6a\xb0\xe7\xcd\x05\xe3\xeb\x4f\x5c\x08\x56\xf3\x8c\x23\xd1\x58\xea\xbc\x1e\x53\xcc\xaa\x0a\xd1\xb5\xcc\x77\x09\x1f\x11\x92\x67\x54\x21\x42\x41\x64\x14\x55\x21\x8b\x6c\xdc\x17\x68\x2e\xb3\xae\x72\x88\xb5\x24\x63\x82\xb1\x8c\x58\x55\x1f\x39\x9d\xf3\x90\x6e\x9a\xc6\x47\x9a\xbd\xa5\x16\x61\x26\x01\x09\x7c\xb8\x90\x9e\x55\x88\xd0\x18\xd9\x01\x55\xe2\xc4\x19\xe9\xec\xe5\x97\x33\x04\xa0\xc7\x6c\x0c\x4a\x8b\xc5\x00\xf4\x48\x04\xa3\xd5\xe0\x0d\x71\x91\x4a\xa3\x7f\xe6\x4c\x82\x2d\x18\x8b\x41\x7d\x68\x64\xd5\x90\xc9\x40\xf1\x30\x30\x7e\x93\xa4\xb4\xae\x76\x20\x9a\x62\xd8\x40\xee\x99\xa8\x50\xb3\xd9\x61\x6d\x6d\xd8\x90\xb4\xc3\xf2\x74\x01\xea\x3c\x34\xf5\x01\x2a\xb3\x92\xd0\xc7\xf5\x4d\x1c\x9e\x95\x40\xd9\x81\x49\x75\x49\x32\x48\x0f\x80\x4a\x75\xc0\x07\xc0\x8f\x33\xe4\x3a\xca\xa0\x66\x52\xc5\x18\x39\xa9\x50\x11\x06\x71\x1c\x82\x94\x68\x07\xe5\x45\x7c\xae\x2a\x7c\x6d\x5a\x56\x14\x0d\xd4\x67\x71\x93\x12\xa8\x1f\x0e\x15\x0f\xb9\x20\x47\x10\xb1\x95\x00\xe3\xe7\xca\xcd\x1e\x0c\x57\x32\x49\xb8\x8c\x35\xa0\x5f\xee\xba\x2a\x76\xc6\xab\xda\xdf\xca\x32\xdd\xda\xe5\x42\x62\xe5\x7d\xd7\x17\x8b\xc3\xb8\x82\xc2\xd0\x4a\x85\x70\x53\x37\x08\x90\x1e\xbd\x9e\xa1\x7d\x9b\x94\x55\x2c\xf7\x19\xe8\x04\x6c\xcb\xc6\x1b\xa9\x17\x27\xc2\xe4\xa2\x42\x34\x4a\x75\xc1\x4e\x24\xc0\x8d\x6f\x7b\xb1\xdb\x3c\x6f\x37\x6c\x62\x2d\x0e\x95\x04\x49\x08\x3b\xbb\x57\x90\xc6\x6c\x84\x1f\x3f\x45\xda\x84\x8b\xf6\x3f\xb3\xb4\x1e\x52\xef\x9c\xf1\x35\x72\x60\xaa\xf3\x56\x5a\x77\x73\x6d\x64\x1b\xf0\xb6\x57\x2e\xe1\x39\xc9\xfd\xb1\xa2\x8d\x10\xba\x83\x71\x26\xd4\xc4\xbb\x96\xa7\x7b\x9f\x05\xeb\xe2\x1a\xe2\xd4\x39\xe1\x77\x8b\x4f\xa4\x31\x51\x77\x14\xd1\xd4\xff\x82\xfe\x11\xf6\x8c\x74\x26\x4a\x39\xd0\x0a\x89\x02\xcc\x36\x84\x50\x05\x05\x08\x0f\x01\xaf\x77\x25\x91\x07\xc8\x97\xd0\x08\xa6\x18\x66\x65\x9c\x63\x38\xfb\xd8\x78\x67\x30\x27\xdc\x5e\x5d\x9b\x71\x41\x8e\xa4\x84\xc2\xe2\x78\xc7\x58\x09\x88\x1a\x89\x42\x00\xca\x33\x46\xcb\x53\x04\x52\x2a\x24\x82\xed\x9f\x04\x5c\x0b\xa2\x4e\x19\xe3\x6a\xf5\xaa\x50\x1e\xaa\x4c\x92\xef\x60\xfa\xde\xd9\xea\xfb\x89\xb6\xd6\x86\xac\x83\xb1\xe4\xb5\xdc\xcf\x67\xb6\xaf\xe4\x36\x92\xd5\x02\x5f\xe7\x38\xb3\xf8\xda\x0c\x72\xf3\xe0\x62\x09\x78\xe2\xf0\xbd\x0a\x43\x35\xd4\xac\xab\x38\x03\xb5\x3c\x49\xac\x2e\xab\xad\xa5\xca\x09\xcd\x18\x07\x1a\xf4\x0d\xa9\x18\xcf\x0a\x81\x30\x64\x1c\x04\x61\x4e\x51\x18\x01\x36\xaf\x05\x6a\xd6\x9f\x4e\x23\x49\x41\x91\x3b\xee\x68\x50\x55\xf1\xfd\x85\x87\x00\x4a\x85\x9d\xbd\x2e\x49\x45\xfc\x4e\xe3\xb0\xda\x88\x7a\xad\xab\xd5\xdc\x25\xda\x4c\x79\x16\x15\xb2\x67\x3a\x84\xf9\x06\x21\xa2\x33\x38\x20\xb1\x20\x75\xb4\x8e\xb9\xf7\xe4\x27\x57\xdf\xe0\xdc\x97\x71\xc5\xd5\xce\x77\xd3\x6f\x64\xeb\xc4\x2f\x2a\xbd\xec\x6d\x6c\xbd\xd5\x8f\xdb\xa9\x6a\x19\x6c\xe2\x5a\x0c\x95\x73\x0d\xc8\x08\x9d\xde\xd5\x24\x7f\x89\x08\x6d\xe8\xa8\x85\x3b\x74\x13\x11\xc7\xfb\x95\x22\x63\xe7\x6b\x47\xfd\xe8\x8a\x40\xa3\xd9\x91\xc9\x81\xef\x12\x49\xc6\xc9\x69\x44\xa1\xa2\x0b\x9d\xd1\x3d\x4b\xbc\xdb\xf5\x37\x72\x3f\x85\x15\xca\x30\xe3\x1e\x29\xc7\xb3\xb1\x34\x63\x5a\xa7\x10\x33\x25\xa5\xcf\xfb\x9f\x98\x78\x6c\x72\x4b\x4e\xdc\x41\x60\x63\x91\x2c\xb8\xc4\xb4\x8e\xed\x86\x09\x5c\xb7\x73\x3a\x34\x78\x9b\x39\x7f\x53\xd8\x83\xbc\xb7\x78\x44\xa2\x9d\x75\x7f\xe5\xca\x99\x4d\x90\x17\xc7\x70\xea\x16\xa0\x04\xb1\x6e\x05\x86\xfa\x47\x4f\xd3\x20\x7f\xcd\xb3\x73\x45\x2a\x60\xb5\x3b\xa2\x6c\x74\xc3\xe9\x89\x52\xed\x96\x33\xa0\x54\x0d\x69\xeb\xf4\x61\x54\xea\xd0\x62\x07\x15\x17\x93\x7b\x80\xe6\xed\x2d\x45\x54\xa2\x12\xc0\x4b\x82\x91\x0c\x15\x03\x57\x1c\xe8\xd6\x3c\x47\x0a\xb2\xee\x49\xcc\xa2\xf2\x6b\xa6\xee\xe2\x48\xa0\xb2\x84\x92\xc8\x2a\xa6\x8e\x49\x73\x28\xd1\xe9\xa2\x12\xb6\x25\xdf\x23\x52\xd6\x02\x32\x84\xbd\x61\xda\xa2\xa8\x18\x25\x8a\x39\xc3\x49\xdc\x92\x15\x7a\xce\x86\x65\x5b\x48\xa8\xbb\x30\x1b\xeb\xd8\xb3\x58\xcd\x12\xba\x34\xbc\xac\x42\x9e\x51\xd1\xb9\xde\xf6\x58\xcc\xb0\xe2\x84\x75\x01\xb2\x09\x3b\xe3\x51\x79\x90\x3e\x18\xe0\xfb\x4e\x3f\xe3\xac\x24\xf8\xb4\x16\x87\x98\xd1\x4e\xc8\x31\x06\x71\xa5\x05\x36\xe6\xd0\xb4\x23\x15\x57\x41\x67\x6d\x09\x9e\x08\xcd\xd9\xd3\x82\x05\xd7\x33\x25\x5e\x22\x0c\x56\x70\xbc\x56\xd0\x52\x09\x44\xa8\x5a\x7c\x33\x74\x2d\x5b\x57\xa4\xfe\xd1\x3e\x03\x29\x62\xc4\x05\x93\xbe\x2f\x2d\x60\x5e\x07\xef\x4f\x2a\xa8\x98\x70\x1a\xe0\x0a\x6f\xec\x42\x2c\x0e\xb0\x15\x52\x60\xd4\x85\x5b\x8f\xca\x18\x5f\xbf\xe3\x0f\x5f\xaa\x6d\xc3\x01\x89\x70\x54\xad\xe5\x1d\xd1\x57\x90\xa9\x33\x07\x27\xf3\x9d\x69\xe2\xef\x4e\x43\xbb\x0e\xef\xbd\x47\xc8\x7a\x47\x3d\x0d\xdd\xb4\x19\x58\xf3\x60\x7a\xc5\xa0\x37\xbc\x1e\xf0\x68\xf5\x61\x2c\xb0\x6f\x46\x59\x6d\xa3\x55\xec\xbd\xba\x5f\x6f\xff\x6d\xad\x6f\x1f\xd3\xb9\x9a\x02\xa4\x14\xc2\x87\xa8\xfe\x61\x61\xd1\x78\x45\x1c\x9a\x74\xb9\xce\x30\xd4\xa3\xfe\x89\x42\x7f\x13\x9b\xfd\x79\xf6\xd5\x3f\xf3\x0d\xbe\xaf\x6d\x51\x17\xe7\xf1\x88\x47\xa5\xbf\x80\xce\xde\x5a\x15\xe6\x3d\x80\xa6\x92\xe9\x59\xc2\x9c\x24\x97\x3e\xa4\xdd\x9a\xdb\xb0\x61\x8e\xff\xc4\x30\x93\xe9\xdc\x2d\xe1\x00\xf1\x9c\x5d\x59\x8b\xf6\x42\x9c\xe7\x7c\xc5\x60\x73\xf7\x61\xa6\x64\x98\x7b\x54\xf4\x4a\xb9\x76\x85\x1b\x58\xb7\x4e\xad\x3e\x63\x90\xee\xf4\x75\xbd\xc7\xff\x35\xfa\xc9\x5b\xfb\x86\x4f\x7a\x9a\x9c\x75\xfd\x30\xcf\xdc\xbb\x77\xf2\x5b\x43\x3e\x16\xa4\x7b\xa2\xa7\x45\xf7\xad\xde\x7a\xf9\xd4\xe8\x7c\x81\x6f\x9f\xf8\x0f\x2f\xe1\x3d\x97\x90\x1b\xfd\x67\xfb\x5f\x0b\x9b\x97\xcd\x9f\x01\x00\x00\xff\xff\x0b\x42\x15\x69\x2e\x35\x00\x00") +var _dataConfig_schema_v32Json = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x5b\xcd\x73\xdc\x28\x16\xbf\xf7\x5f\xa1\x52\x72\x8b\x3f\x52\xbb\xa9\xad\xda\xdc\xf6\xb8\xa7\x99\xf3\xb8\x3a\x2a\x1a\xbd\x56\x13\x4b\x40\x00\xb5\xdd\x49\xf9\x7f\x9f\xd2\x67\x03\x02\x81\xba\xe5\x38\x33\x35\x27\xdb\xe2\xf7\x80\xf7\xfd\x1e\xe0\x1f\x9b\x24\x49\xdf\x4b\x7c\x80\x0a\xa5\x9f\x93\xf4\xa0\x14\xff\x7c\x7f\xff\x55\x32\x7a\xdb\x7d\xbd\x63\xa2\xb8\xcf\x05\xda\xab\xdb\x8f\x9f\xee\xbb\x6f\xef\xd2\x9b\x86\x8e\xe4\x0d\x09\x66\x74\x4f\x8a\xac\x1b\xc9\x8e\xff\xbe\xfb\xd7\x5d\x43\xde\x41\xd4\x89\x43\x03\x62\xbb\xaf\x80\x55\xf7\x4d\xc0\xb7\x9a\x08\x68\x88\x1f\xd2\x23\x08\x49\x18\x4d\xb7\x37\x9b\x66\x8c\x0b\xc6\x41\x28\x02\x32\xfd\x9c\x34\x9b\x4b\x92\x11\x32\x7c\xd0\xa6\x95\x4a\x10\x5a\xa4\xed\xe7\x97\x76\x86\x24\x49\x25\x88\x23\xc1\xda\x0c\xe3\x56\xdf\xdd\x9f\xe7\xbf\x1f\x61\x37\xf6\xac\xda\x66\xdb\xef\x1c\x29\x05\x82\xfe\x3e\xdd\x5b\x3b\xfc\xe5\x01\xdd\x7e\xff\xdf\xed\x1f\x1f\x6f\xff\x7b\x97\xdd\x6e\x3f\xbc\x37\x86\x1b\xf9\x0a\xd8\x77\xcb\xe7\xb0\x27\x94\x28\xc2\xe8\xb8\x7e\x3a\x22\x5f\xfa\xdf\x5e\xc6\x85\x51\x9e\xb7\x60\x54\x1a\x6b\xef\x51\x29\xc1\xe4\x99\x82\x7a\x62\xe2\x31\xc4\xf3\x08\x7b\x23\x9e\xfb\xf5\x1d\x3c\x9b\xec\x1c\x59\x59\x57\x41\x0d\x0e\xa8\x37\x62\xa6\x5b\x7e\x1d\xfd\x49\xc0\x02\x54\xd8\x64\x3b\xd4\x9b\x59\x6c\xb3\xfc\x75\x0c\x6f\x06\xa6\x67\xb1\x1d\x42\x5b\xbb\xdd\xa0\xe1\xde\x2e\x51\xb9\xdc\xcb\x2f\xab\x51\x58\x1e\x29\xe5\xc0\x4b\x76\x6a\xbe\x79\xe4\xd1\x01\x2a\xa0\x2a\x1d\x45\x90\x24\xe9\xae\x26\x65\x6e\x4b\x94\x51\xf8\xad\x99\xe2\x41\xfb\x98\x24\x3f\xec\x48\xa6\xcd\xd3\x8e\x1b\x7f\xf9\x15\x3e\x8e\x7b\x78\x19\xc7\x31\xa3\x0a\x9e\x55\xcb\xd4\xfc\xd2\x9d\x08\x18\x7e\x04\xb1\x27\x25\xc4\x52\x20\x51\xc8\x19\x91\x95\x44\xaa\x8c\x89\x2c\x27\x58\x39\xe9\x31\xc2\x07\xc8\xf6\x82\x55\xc1\x59\xf6\x59\xb7\x0f\x99\xbe\x58\xf3\x4c\x26\x0e\x1b\xe6\x48\xaa\xfd\xb5\xdd\x38\x26\x4c\x31\xe2\x19\xca\x73\x43\x20\x48\x08\x74\x4a\x6f\x92\x94\x28\xa8\xa4\x5b\x56\x49\x5a\x53\xf2\xad\x86\xff\xf7\x10\x25\x6a\xb0\xe7\xcd\x05\xe3\xeb\x4f\x5c\x08\x56\xf3\x8c\x23\xd1\x58\xea\xbc\x1e\x53\xcc\xaa\x0a\xd1\xb5\xcc\x77\x09\x1f\x11\x92\x67\x54\x21\x42\x41\x64\x14\x55\x21\x8b\x6c\xdc\x17\x68\x2e\xb3\xae\x72\x88\xb5\x24\x63\x82\xb1\x8c\x58\x55\x1f\x39\x9d\xf3\x90\x6e\x9a\xc6\x47\x9a\xbd\xa5\x16\x61\x26\x01\x09\x7c\xb8\x90\x9e\x55\x88\xd0\x18\xd9\x01\x55\xe2\xc4\x19\xe9\xec\xe5\x97\x33\x04\xa0\xc7\x6c\x0c\x4a\x8b\xc5\x00\xf4\x48\x04\xa3\xd5\xe0\x0d\x71\x91\x4a\xa3\x7f\xe6\x4c\x82\x2d\x18\x8b\x41\x7d\x68\x64\xd5\x90\xc9\x40\xf1\x30\x30\x7e\x93\xa4\xb4\xae\x76\x20\x9a\x62\xd8\x40\xee\x99\xa8\x50\xb3\xd9\x61\x6d\x6d\xd8\x90\xb4\xc3\xf2\x74\x01\xea\x3c\x34\xf5\x01\x2a\xb3\x92\xd0\xc7\xf5\x4d\x1c\x9e\x95\x40\xd9\x81\x49\x75\x49\x32\x48\x0f\x80\x4a\x75\xc0\x07\xc0\x8f\x33\xe4\x3a\xca\xa0\x66\x52\xc5\x18\x39\xa9\x50\x11\x06\x71\x1c\x82\x94\x68\x07\xe5\x45\x7c\xae\x2a\x7c\x6d\x5a\x56\x14\x0d\xd4\x67\x71\x93\x12\xa8\x1f\x0e\x15\x0f\xb9\x20\x47\x10\xb1\x95\x00\xe3\xe7\xca\xcd\x1e\x0c\x57\x32\x49\xb8\x8c\x35\xa0\x5f\xee\xba\x2a\x76\xc6\xab\xda\xdf\xca\x32\xdd\xda\xe5\x42\x62\xe5\x7d\xd7\x17\x8b\xc3\xb8\x82\xc2\xd0\x4a\x85\x70\x53\x37\x08\x90\x1e\xbd\x9e\xa1\x7d\x9b\x94\x55\x2c\xf7\x19\xe8\x04\x6c\xcb\xc6\x1b\xa9\x17\x27\xc2\xe4\xa2\x42\x34\x4a\x75\xc1\x4e\x24\xc0\x8d\x6f\x7b\xb1\xdb\x3c\x6f\x37\x6c\x62\x2d\x0e\x95\x04\x49\x08\x3b\xbb\x57\x90\xc6\x6c\x84\x1f\x3f\x45\xda\x84\x8b\xf6\x3f\xb3\xb4\x1e\x52\xef\x9c\xf1\x35\x72\x60\xaa\xf3\x56\x5a\x77\x73\x6d\x64\x1b\xf0\xb6\x57\x2e\xe1\x39\xc9\xfd\xb1\xa2\x8d\x10\xba\x83\x71\x26\xd4\xc4\xbb\x96\xa7\x7b\x9f\x05\xeb\xe2\x1a\xe2\xd4\x39\xe1\x77\x8b\x4f\xa4\x31\x51\x77\x14\xd1\xd4\xff\x82\xfe\x11\xf6\x8c\x74\x26\x4a\x39\xd0\x0a\x89\x02\xcc\x36\x84\x50\x05\x05\x08\x0f\x01\xaf\x77\x25\x91\x07\xc8\x97\xd0\x08\xa6\x18\x66\x65\x9c\x63\x38\xfb\xd8\x78\x67\x30\x27\xdc\x5e\x5d\x9b\x71\x41\x8e\xa4\x84\xc2\xe2\x78\xc7\x58\x09\x88\x1a\x89\x42\x00\xca\x33\x46\xcb\x53\x04\x52\x2a\x24\x82\xed\x9f\x04\x5c\x0b\xa2\x4e\x19\xe3\x6a\xf5\xaa\x50\x1e\xaa\x4c\x92\xef\x60\xfa\xde\xd9\xea\xfb\x89\xb6\xd6\x86\xac\x83\xb1\xe4\xb5\xdc\xcf\x67\xb6\xaf\xe4\x36\x92\xd5\x02\x5f\xe7\x38\xb3\xf8\xda\x0c\x72\xf3\xe0\x62\x09\x78\xe2\xf0\xbd\x0a\x43\x35\xd4\xac\xab\x38\x03\xb5\x3c\x49\xac\x2e\xab\xad\xa5\xca\x09\xcd\x18\x07\x1a\xf4\x0d\xa9\x18\xcf\x0a\x81\x30\x64\x1c\x04\x61\x4e\x51\x18\x01\x36\xaf\x05\x6a\xd6\x9f\x4e\x23\x49\x41\x91\x3b\xee\x68\x50\x55\xf1\xfd\x85\x87\x00\x4a\x85\x9d\xbd\x2e\x49\x45\xfc\x4e\xe3\xb0\xda\x88\x7a\xad\xab\xd5\xdc\x25\xda\x4c\x79\x16\x15\xb2\x67\x3a\x84\xf9\x06\x21\xa2\x33\x38\x20\xb1\x20\x75\xb4\x8e\xb9\xf7\xe4\x27\x57\xdf\xe0\xdc\x97\x71\xc5\xd5\xce\x77\xd3\x6f\x64\xeb\xc4\x2f\x2a\xbd\xec\x6d\x6c\xbd\xd5\x8f\xdb\xa9\x6a\x19\x6c\xe2\x5a\x0c\x95\x73\x0d\xc8\x08\x9d\xde\xd5\x24\x7f\x89\x08\x6d\xe8\xa8\x85\x3b\x74\x13\x11\xc7\xfb\x95\x22\x63\xe7\x6b\x47\xfd\xe8\x8a\x40\xa3\xd9\x91\xc9\x81\xef\x12\x49\xc6\xc9\x69\x44\xa1\xa2\x0b\x9d\xd1\x3d\x4b\xbc\xdb\xf5\x37\x72\x3f\x85\x15\xca\x30\xe3\x1e\x29\xc7\xb3\xb1\x34\x63\x5a\xa7\x10\x33\x25\xa5\xcf\xfb\x9f\x98\x78\x6c\x72\x4b\x4e\xdc\x41\x60\x63\x91\x2c\xb8\xc4\xb4\x8e\xed\x86\x09\x5c\xb7\x73\x3a\x34\x78\x9b\x39\x7f\x53\xd8\x83\xbc\xb7\x78\x44\xa2\x9d\x75\x7f\xe5\xca\x99\x4d\x90\x17\xc7\x70\xea\x16\xa0\x04\xb1\x6e\x05\x86\xfa\x47\x4f\xd3\x20\x7f\xcd\xb3\x73\x45\x2a\x60\xb5\x3b\xa2\x6c\x74\xc3\xe9\x89\x52\xed\x96\x33\xa0\x54\x0d\x69\xeb\xf4\x61\x54\xea\xd0\x62\x07\x15\x17\x93\x7b\x80\xe6\xed\x2d\x45\x54\xa2\x12\xc0\x4b\x82\x91\x0c\x15\x03\x57\x1c\xe8\xd6\x3c\x47\x0a\xb2\xee\x49\xcc\xa2\xf2\x6b\xa6\xee\xe2\x48\xa0\xb2\x84\x92\xc8\x2a\xa6\x8e\x49\x73\x28\xd1\xe9\xa2\x12\xb6\x25\xdf\x23\x52\xd6\x02\x32\x84\xbd\x61\xda\xa2\xa8\x18\x25\x8a\x39\xc3\x49\xdc\x92\x15\x7a\xce\x86\x65\x5b\x48\xa8\xbb\x30\x1b\xeb\xd8\xb3\x58\xcd\x12\xba\x34\xbc\xac\x42\x9e\x51\xd1\xb9\xde\xf6\x58\xcc\xb0\xe2\x84\x75\x01\xb2\x09\x3b\xe3\x51\x79\x90\x3e\x18\xe0\xfb\x4e\x3f\xe3\xac\x24\xf8\xb4\x16\x87\x98\xd1\x4e\xc8\x31\x06\x71\xa5\x05\x36\xe6\xd0\xb4\x23\x15\x57\x41\x67\x6d\x09\x9e\x08\xcd\xd9\xd3\x82\x05\xd7\x33\x25\x5e\x22\x0c\x56\x70\xbc\x56\xd0\x52\x09\x44\xa8\x5a\x7c\x33\x74\x2d\x5b\x57\xa4\xfe\xd1\x3e\x03\x29\x62\xc4\x05\x93\xbe\x2f\x2d\x60\x5e\x07\xef\x4f\x2a\xa8\x98\x70\x1a\xe0\x0a\x6f\xec\x42\x2c\x0e\xb0\x15\x52\x60\xd4\x85\x5b\x8f\xca\x18\x5f\xbf\xe3\x0f\x5f\xaa\x6d\xc3\x01\x89\x70\x54\xad\xe5\x1d\xd1\x57\x90\xa9\x33\x07\x27\xf3\x9d\x69\xe2\xef\x4e\x43\xbb\x0e\xef\xbd\x47\xc8\x7a\x47\x3d\x0d\xdd\xb4\x19\x58\xf3\x60\x7a\xc5\xa0\x37\xbc\x1e\xf0\x68\xf5\x61\x2c\xb0\x6f\x46\x59\x6d\xa3\x55\xec\xbd\xba\x5f\x6f\xff\x6d\xad\x6f\x1f\xd3\xb9\x9a\x02\xa4\x14\xc2\x87\xa8\xfe\x61\x61\xd1\x78\x45\x1c\x9a\x74\xb9\xce\x30\xd4\xa3\xfe\x89\x42\x7f\x13\x9b\xfd\x79\xf6\xd5\x3f\xf3\x0d\xbe\xaf\x6d\x51\x17\xe7\xf1\x88\x47\xa5\xbf\x80\xce\xde\x5a\x15\xe6\x3d\x80\xa6\x92\xe9\x59\xc2\x9c\x24\x97\x3e\xa4\xdd\x9a\xdb\xb0\x61\x8e\xff\xc4\x30\x93\xe9\xdc\x2d\xe1\x00\xf1\x9c\x5d\x59\x8b\xf6\x42\x9c\xe7\x7c\xc5\x60\x73\xf7\x61\xa6\x64\x98\x7b\x54\xf4\x4a\xb9\x76\x85\x1b\x58\xb7\x4e\xad\x3e\x63\x90\xee\xf4\x75\xbd\xc7\xff\x35\xfa\xc9\x5b\xfb\x86\x4f\x7a\x9a\x9c\x75\xfd\x30\xcf\xdc\xbb\x77\xf2\x5b\x43\x3e\x16\xa4\x7b\xa2\xa7\x45\xf7\xad\xde\x7a\xf9\xd4\xe8\x7c\x81\x6f\x9f\xf8\x0f\x2f\xe1\x3d\x97\x90\x1b\xfd\x67\xfb\x5f\x0b\x9b\x97\xcd\x9f\x01\x00\x00\xff\xff\x57\x1e\x50\xbb\x2e\x35\x00\x00") func dataConfig_schema_v32JsonBytes() ([]byte, error) { return bindataRead( diff --git a/components/cli/compose/schema/data/config_schema_v3.2.json b/components/cli/compose/schema/data/config_schema_v3.2.json index e47c879a4d..945102f84c 100644 --- a/components/cli/compose/schema/data/config_schema_v3.2.json +++ b/components/cli/compose/schema/data/config_schema_v3.2.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "id": "config_schema_v3.1.json", + "id": "config_schema_v3.2.json", "type": "object", "required": ["version"], From 8698efadf8254b16abd7548d2c166dc9c6ac7383 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Thu, 16 Mar 2017 10:54:18 -0700 Subject: [PATCH 670/978] api: Remove SecretRequestOption type This type is only used by CLI code. It duplicates SecretReference in the types/swarm package. Change the CLI code to use that type instead. Signed-off-by: Aaron Lehmann Upstream-commit: 395081fc6b10bd55add9058fbbcc07407ef927d8 Component: cli --- components/cli/command/service/parse.go | 24 ++++++++--------------- components/cli/compose/convert/service.go | 19 +++++++++--------- 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/components/cli/command/service/parse.go b/components/cli/command/service/parse.go index ce9b454edd..baf5e24547 100644 --- a/components/cli/command/service/parse.go +++ b/components/cli/command/service/parse.go @@ -10,27 +10,19 @@ import ( "golang.org/x/net/context" ) -// ParseSecrets retrieves the secrets from the requested names and converts -// them to secret references to use with the spec -func ParseSecrets(client client.SecretAPIClient, requestedSecrets []*types.SecretRequestOption) ([]*swarmtypes.SecretReference, error) { +// ParseSecrets retrieves the secrets with the requested names and fills +// secret IDs into the secret references. +func ParseSecrets(client client.SecretAPIClient, requestedSecrets []*swarmtypes.SecretReference) ([]*swarmtypes.SecretReference, error) { secretRefs := make(map[string]*swarmtypes.SecretReference) ctx := context.Background() for _, secret := range requestedSecrets { - if _, exists := secretRefs[secret.Target]; exists { - return nil, fmt.Errorf("duplicate secret target for %s not allowed", secret.Source) + if _, exists := secretRefs[secret.File.Name]; exists { + return nil, fmt.Errorf("duplicate secret target for %s not allowed", secret.SecretName) } - secretRef := &swarmtypes.SecretReference{ - File: &swarmtypes.SecretReferenceFileTarget{ - Name: secret.Target, - UID: secret.UID, - GID: secret.GID, - Mode: secret.Mode, - }, - SecretName: secret.Source, - } - - secretRefs[secret.Target] = secretRef + secretRef := new(swarmtypes.SecretReference) + *secretRef = *secret + secretRefs[secret.File.Name] = secretRef } args := filters.NewArgs() diff --git a/components/cli/compose/convert/service.go b/components/cli/compose/convert/service.go index ab90d7319a..f7e539ca62 100644 --- a/components/cli/compose/convert/service.go +++ b/components/cli/compose/convert/service.go @@ -7,7 +7,6 @@ import ( "strings" "time" - "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/swarm" servicecli "github.com/docker/docker/cli/command/service" @@ -196,7 +195,7 @@ func convertServiceSecrets( secrets []composetypes.ServiceSecretConfig, secretSpecs map[string]composetypes.SecretConfig, ) ([]*swarm.SecretReference, error) { - opts := []*types.SecretRequestOption{} + refs := []*swarm.SecretReference{} for _, secret := range secrets { target := secret.Target if target == "" { @@ -222,16 +221,18 @@ func convertServiceSecrets( mode = uint32Ptr(0444) } - opts = append(opts, &types.SecretRequestOption{ - Source: source, - Target: target, - UID: uid, - GID: gid, - Mode: os.FileMode(*mode), + refs = append(refs, &swarm.SecretReference{ + File: &swarm.SecretReferenceFileTarget{ + Name: target, + UID: uid, + GID: gid, + Mode: os.FileMode(*mode), + }, + SecretName: source, }) } - return servicecli.ParseSecrets(client, opts) + return servicecli.ParseSecrets(client, refs) } func uint32Ptr(value uint32) *uint32 { From abd145043bb5477169f062951ec2db38d572d357 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 17 Mar 2017 06:21:55 +0000 Subject: [PATCH 671/978] compose: update the comment about MappingWithEquals Signed-off-by: Akihiro Suda Upstream-commit: 2fc6cd4b71b902a65e56b7e0264356fc0d70f8ed Component: cli --- components/cli/compose/types/types.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/components/cli/compose/types/types.go b/components/cli/compose/types/types.go index bb12f8497b..3e6651fd32 100644 --- a/components/cli/compose/types/types.go +++ b/components/cli/compose/types/types.go @@ -134,7 +134,9 @@ type StringList []string type StringOrNumberList []string // MappingWithEquals is a mapping type that can be converted from a list of -// key=value strings +// key[=value] strings. +// For the key with an empty value (`key=`), the mapped value is set to a pointer to `""`. +// For the key without value (`key`), the mapped value is set to nil. type MappingWithEquals map[string]*string // Labels is a mapping type for labels From bbac6dc261b2df18e2a430a115bfe712673f05b2 Mon Sep 17 00:00:00 2001 From: Boaz Shuster Date: Sun, 5 Mar 2017 13:11:04 +0200 Subject: [PATCH 672/978] Add format to secret ls Signed-off-by: Boaz Shuster Upstream-commit: 1bac314da583ece87b0788e712e95af85dbd6933 Component: cli --- components/cli/command/formatter/secret.go | 101 ++++++++++++++++++ .../cli/command/formatter/secret_test.go | 63 +++++++++++ components/cli/command/secret/ls.go | 40 +++---- components/cli/config/configfile/file.go | 1 + 4 files changed, 180 insertions(+), 25 deletions(-) create mode 100644 components/cli/command/formatter/secret.go create mode 100644 components/cli/command/formatter/secret_test.go diff --git a/components/cli/command/formatter/secret.go b/components/cli/command/formatter/secret.go new file mode 100644 index 0000000000..7ec6f9a62e --- /dev/null +++ b/components/cli/command/formatter/secret.go @@ -0,0 +1,101 @@ +package formatter + +import ( + "fmt" + "strings" + "time" + + "github.com/docker/docker/api/types/swarm" + units "github.com/docker/go-units" +) + +const ( + defaultSecretTableFormat = "table {{.ID}}\t{{.Name}}\t{{.CreatedAt}}\t{{.UpdatedAt}}" + secretIDHeader = "ID" + secretNameHeader = "NAME" + secretCreatedHeader = "CREATED" + secretUpdatedHeader = "UPDATED" +) + +// NewSecretFormat returns a Format for rendering using a network Context +func NewSecretFormat(source string, quiet bool) Format { + switch source { + case TableFormatKey: + if quiet { + return defaultQuietFormat + } + return defaultSecretTableFormat + } + return Format(source) +} + +// SecretWrite writes the context +func SecretWrite(ctx Context, secrets []swarm.Secret) error { + render := func(format func(subContext subContext) error) error { + for _, secret := range secrets { + secretCtx := &secretContext{s: secret} + if err := format(secretCtx); err != nil { + return err + } + } + return nil + } + return ctx.Write(newSecretContext(), render) +} + +func newSecretContext() *secretContext { + sCtx := &secretContext{} + + sCtx.header = map[string]string{ + "ID": secretIDHeader, + "Name": nameHeader, + "CreatedAt": secretCreatedHeader, + "UpdatedAt": secretUpdatedHeader, + "Labels": labelsHeader, + } + return sCtx +} + +type secretContext struct { + HeaderContext + s swarm.Secret +} + +func (c *secretContext) MarshalJSON() ([]byte, error) { + return marshalJSON(c) +} + +func (c *secretContext) ID() string { + return c.s.ID +} + +func (c *secretContext) Name() string { + return c.s.Spec.Annotations.Name +} + +func (c *secretContext) CreatedAt() string { + return units.HumanDuration(time.Now().UTC().Sub(c.s.Meta.CreatedAt)) + " ago" +} + +func (c *secretContext) UpdatedAt() string { + return units.HumanDuration(time.Now().UTC().Sub(c.s.Meta.UpdatedAt)) + " ago" +} + +func (c *secretContext) Labels() string { + mapLabels := c.s.Spec.Annotations.Labels + if mapLabels == nil { + return "" + } + var joinLabels []string + for k, v := range mapLabels { + joinLabels = append(joinLabels, fmt.Sprintf("%s=%s", k, v)) + } + return strings.Join(joinLabels, ",") +} + +func (c *secretContext) Label(name string) string { + if c.s.Spec.Annotations.Labels == nil { + return "" + } + return c.s.Spec.Annotations.Labels[name] +} diff --git a/components/cli/command/formatter/secret_test.go b/components/cli/command/formatter/secret_test.go new file mode 100644 index 0000000000..722b650565 --- /dev/null +++ b/components/cli/command/formatter/secret_test.go @@ -0,0 +1,63 @@ +package formatter + +import ( + "bytes" + "testing" + "time" + + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestSecretContextFormatWrite(t *testing.T) { + // Check default output format (verbose and non-verbose mode) for table headers + cases := []struct { + context Context + expected string + }{ + // Errors + { + Context{Format: "{{InvalidFunction}}"}, + `Template parsing error: template: :1: function "InvalidFunction" not defined +`, + }, + { + Context{Format: "{{nil}}"}, + `Template parsing error: template: :1:2: executing "" at : nil is not a command +`, + }, + // Table format + {Context{Format: NewSecretFormat("table", false)}, + `ID NAME CREATED UPDATED +1 passwords Less than a second ago Less than a second ago +2 id_rsa Less than a second ago Less than a second ago +`}, + {Context{Format: NewSecretFormat("table {{.Name}}", true)}, + `NAME +passwords +id_rsa +`}, + {Context{Format: NewSecretFormat("{{.ID}}-{{.Name}}", false)}, + `1-passwords +2-id_rsa +`}, + } + + secrets := []swarm.Secret{ + {ID: "1", + Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()}, + Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "passwords"}}}, + {ID: "2", + Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()}, + Spec: swarm.SecretSpec{Annotations: swarm.Annotations{Name: "id_rsa"}}}, + } + for _, testcase := range cases { + out := bytes.NewBufferString("") + testcase.context.Output = out + if err := SecretWrite(testcase.context, secrets); err != nil { + assert.Error(t, err, testcase.expected) + } else { + assert.Equal(t, out.String(), testcase.expected) + } + } +} diff --git a/components/cli/command/secret/ls.go b/components/cli/command/secret/ls.go index faeab314b7..211ebceb5f 100644 --- a/components/cli/command/secret/ls.go +++ b/components/cli/command/secret/ls.go @@ -1,20 +1,17 @@ package secret import ( - "fmt" - "text/tabwriter" - "time" - "github.com/docker/docker/api/types" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" - "github.com/docker/go-units" + "github.com/docker/docker/cli/command/formatter" "github.com/spf13/cobra" "golang.org/x/net/context" ) type listOptions struct { - quiet bool + quiet bool + format string } func newSecretListCommand(dockerCli *command.DockerCli) *cobra.Command { @@ -32,6 +29,7 @@ func newSecretListCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs") + flags.StringVarP(&opts.format, "format", "", "", "Pretty-print secrets using a Go template") return cmd } @@ -44,25 +42,17 @@ func runSecretList(dockerCli *command.DockerCli, opts listOptions) error { if err != nil { return err } - - w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0) - if opts.quiet { - for _, s := range secrets { - fmt.Fprintf(w, "%s\n", s.ID) - } - } else { - fmt.Fprintf(w, "ID\tNAME\tCREATED\tUPDATED") - fmt.Fprintf(w, "\n") - - for _, s := range secrets { - created := units.HumanDuration(time.Now().UTC().Sub(s.Meta.CreatedAt)) + " ago" - updated := units.HumanDuration(time.Now().UTC().Sub(s.Meta.UpdatedAt)) + " ago" - - fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", s.ID, s.Spec.Annotations.Name, created, updated) + format := opts.format + if len(format) == 0 { + if len(dockerCli.ConfigFile().SecretFormat) > 0 && !opts.quiet { + format = dockerCli.ConfigFile().SecretFormat + } else { + format = formatter.TableFormatKey } } - - w.Flush() - - return nil + secretCtx := formatter.Context{ + Output: dockerCli.Out(), + Format: formatter.NewSecretFormat(format, opts.quiet), + } + return formatter.SecretWrite(secretCtx, secrets) } diff --git a/components/cli/config/configfile/file.go b/components/cli/config/configfile/file.go index d83434676e..e97fbe47ba 100644 --- a/components/cli/config/configfile/file.go +++ b/components/cli/config/configfile/file.go @@ -37,6 +37,7 @@ type ConfigFile struct { ServiceInspectFormat string `json:"serviceInspectFormat,omitempty"` ServicesFormat string `json:"servicesFormat,omitempty"` TasksFormat string `json:"tasksFormat,omitempty"` + SecretFormat string `json:"secretFormat,omitempty"` } // LegacyLoadFromReader reads the non-nested configuration data given and sets up the From 696316996614578a760ed037cecb64aef79f3240 Mon Sep 17 00:00:00 2001 From: Boaz Shuster Date: Mon, 13 Mar 2017 23:19:46 +0200 Subject: [PATCH 673/978] Use formatter in docker checkpoint ls Signed-off-by: Boaz Shuster Upstream-commit: b8d5b0f675526ee002b7cecd26062692bafaf7a3 Component: cli --- components/cli/command/checkpoint/list.go | 18 ++---- .../cli/command/formatter/checkpoint.go | 52 ++++++++++++++++++ .../cli/command/formatter/checkpoint_test.go | 55 +++++++++++++++++++ 3 files changed, 112 insertions(+), 13 deletions(-) create mode 100644 components/cli/command/formatter/checkpoint.go create mode 100644 components/cli/command/formatter/checkpoint_test.go diff --git a/components/cli/command/checkpoint/list.go b/components/cli/command/checkpoint/list.go index daf8349993..20e7d6d73a 100644 --- a/components/cli/command/checkpoint/list.go +++ b/components/cli/command/checkpoint/list.go @@ -1,14 +1,12 @@ package checkpoint import ( - "fmt" - "text/tabwriter" - "golang.org/x/net/context" "github.com/docker/docker/api/types" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/formatter" "github.com/spf13/cobra" ) @@ -48,15 +46,9 @@ func runList(dockerCli *command.DockerCli, container string, opts listOptions) e return err } - w := tabwriter.NewWriter(dockerCli.Out(), 20, 1, 3, ' ', 0) - fmt.Fprintf(w, "CHECKPOINT NAME") - fmt.Fprintf(w, "\n") - - for _, checkpoint := range checkpoints { - fmt.Fprintf(w, "%s\t", checkpoint.Name) - fmt.Fprint(w, "\n") + cpCtx := formatter.Context{ + Output: dockerCli.Out(), + Format: formatter.NewCheckpointFormat(formatter.TableFormatKey), } - - w.Flush() - return nil + return formatter.CheckpointWrite(cpCtx, checkpoints) } diff --git a/components/cli/command/formatter/checkpoint.go b/components/cli/command/formatter/checkpoint.go new file mode 100644 index 0000000000..041fcafb7d --- /dev/null +++ b/components/cli/command/formatter/checkpoint.go @@ -0,0 +1,52 @@ +package formatter + +import "github.com/docker/docker/api/types" + +const ( + defaultCheckpointFormat = "table {{.Name}}" + + checkpointNameHeader = "CHECKPOINT NAME" +) + +// NewCheckpointFormat returns a format for use with a checkpoint Context +func NewCheckpointFormat(source string) Format { + switch source { + case TableFormatKey: + return defaultCheckpointFormat + } + return Format(source) +} + +// CheckpointWrite writes formatted checkpoints using the Context +func CheckpointWrite(ctx Context, checkpoints []types.Checkpoint) error { + render := func(format func(subContext subContext) error) error { + for _, checkpoint := range checkpoints { + if err := format(&checkpointContext{c: checkpoint}); err != nil { + return err + } + } + return nil + } + return ctx.Write(newCheckpointContext(), render) +} + +type checkpointContext struct { + HeaderContext + c types.Checkpoint +} + +func newCheckpointContext() *checkpointContext { + cpCtx := checkpointContext{} + cpCtx.header = volumeHeaderContext{ + "Name": checkpointNameHeader, + } + return &cpCtx +} + +func (c *checkpointContext) MarshalJSON() ([]byte, error) { + return marshalJSON(c) +} + +func (c *checkpointContext) Name() string { + return c.c.Name +} diff --git a/components/cli/command/formatter/checkpoint_test.go b/components/cli/command/formatter/checkpoint_test.go new file mode 100644 index 0000000000..e88c4d0132 --- /dev/null +++ b/components/cli/command/formatter/checkpoint_test.go @@ -0,0 +1,55 @@ +package formatter + +import ( + "bytes" + "testing" + + "github.com/docker/docker/api/types" + "github.com/stretchr/testify/assert" +) + +func TestCheckpointContextFormatWrite(t *testing.T) { + cases := []struct { + context Context + expected string + }{ + { + Context{Format: NewCheckpointFormat(defaultCheckpointFormat)}, + `CHECKPOINT NAME +checkpoint-1 +checkpoint-2 +checkpoint-3 +`, + }, + { + Context{Format: NewCheckpointFormat("{{.Name}}")}, + `checkpoint-1 +checkpoint-2 +checkpoint-3 +`, + }, + { + Context{Format: NewCheckpointFormat("{{.Name}}:")}, + `checkpoint-1: +checkpoint-2: +checkpoint-3: +`, + }, + } + + checkpoints := []types.Checkpoint{ + {"checkpoint-1"}, + {"checkpoint-2"}, + {"checkpoint-3"}, + } + for _, testcase := range cases { + out := bytes.NewBufferString("") + testcase.context.Output = out + err := CheckpointWrite(testcase.context, checkpoints) + if err != nil { + assert.Error(t, err, testcase.expected) + } else { + assert.Equal(t, out.String(), testcase.expected) + } + } +} From 5d9a5ce7ddd36774b0f74e31be442c1118feb9bd Mon Sep 17 00:00:00 2001 From: liker12134 Date: Mon, 20 Mar 2017 16:27:51 +0800 Subject: [PATCH 674/978] fixed:go vetting warning unkeyed fields Signed-off-by: Aaron.L.Xu Upstream-commit: cc44dec589e45755a7fcccc053aff0267d2b9cf0 Component: cli --- components/cli/command/container/stats_unit_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/container/stats_unit_test.go b/components/cli/command/container/stats_unit_test.go index 828d634c8a..612914c9cd 100644 --- a/components/cli/command/container/stats_unit_test.go +++ b/components/cli/command/container/stats_unit_test.go @@ -8,7 +8,7 @@ import ( func TestCalculateBlockIO(t *testing.T) { blkio := types.BlkioStats{ - IoServiceBytesRecursive: []types.BlkioStatEntry{{8, 0, "read", 1234}, {8, 1, "read", 4567}, {8, 0, "write", 123}, {8, 1, "write", 456}}, + IoServiceBytesRecursive: []types.BlkioStatEntry{{Major: 8, Minor: 0, Op: "read", Value: 1234}, {Major: 8, Minor: 1, Op: "read", Value: 4567}, {Major: 8, Minor: 0, Op: "write", Value: 123}, {Major: 8, Minor: 1, Op: "write", Value: 456}}, } blkRead, blkWrite := calculateBlockIO(blkio) if blkRead != 5801 { From 589b564e68c5c5bafb34a1c98230547528dba3ac Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 20 Mar 2017 15:39:57 +0100 Subject: [PATCH 675/978] Fixing a small typo in compose loader package Signed-off-by: Vincent Demeester Upstream-commit: 4826a5c3af189b6a5d82ee11973e7bf1bbeddb3e Component: cli --- components/cli/compose/loader/loader.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/compose/loader/loader.go b/components/cli/compose/loader/loader.go index 3edcd81668..0653691cd9 100644 --- a/components/cli/compose/loader/loader.go +++ b/components/cli/compose/loader/loader.go @@ -561,7 +561,7 @@ func transformServiceSecret(data interface{}) (interface{}, error) { case map[string]interface{}: return data, nil default: - return data, fmt.Errorf("invalid type %T for external", value) + return data, fmt.Errorf("invalid type %T for secret", value) } } From 6c48ab35feaa2f509387d4675a7d58bf265d8212 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 14 Mar 2017 17:53:29 -0400 Subject: [PATCH 676/978] Create a new ServerType struct for storing details about the server on the client. Signed-off-by: Daniel Nephin Upstream-commit: 4ab8463fed4f97bed60a2bd2902f755ea3a36fa8 Component: cli --- components/cli/command/cli.go | 50 +++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/components/cli/command/cli.go b/components/cli/command/cli.go index 783e516f3d..74d0fa4f76 100644 --- a/components/cli/command/cli.go +++ b/components/cli/command/cli.go @@ -44,28 +44,17 @@ type Cli interface { // DockerCli is an instance the docker command line client. // Instances of the client can be returned from NewDockerCli. type DockerCli struct { - configFile *configfile.ConfigFile - in *InStream - out *OutStream - err io.Writer - keyFile string - client client.APIClient - hasExperimental bool - osType string - defaultVersion string + configFile *configfile.ConfigFile + in *InStream + out *OutStream + err io.Writer + keyFile string + client client.APIClient + defaultVersion string + server ServerInfo } -// HasExperimental returns true if experimental features are accessible. -func (cli *DockerCli) HasExperimental() bool { - return cli.hasExperimental -} - -// OSType returns the operating system the daemon is running on. -func (cli *DockerCli) OSType() string { - return cli.osType -} - -// DefaultVersion returns api.defaultVersion of DOCKER_API_VERSION if specified. +// DefaultVersion returns api.defaultVersion or DOCKER_API_VERSION if specified. func (cli *DockerCli) DefaultVersion() string { return cli.defaultVersion } @@ -102,6 +91,12 @@ func (cli *DockerCli) ConfigFile() *configfile.ConfigFile { return cli.configFile } +// ServerInfo returns the server version details for the host this client is +// connected to +func (cli *DockerCli) ServerInfo() ServerInfo { + return cli.server +} + // GetAllCredentials returns all of the credentials stored in all of the // configured credential stores. func (cli *DockerCli) GetAllCredentials() (map[string]types.AuthConfig, error) { @@ -161,7 +156,6 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error { if err != nil { return err } - cli.defaultVersion = cli.client.ClientVersion() if opts.Common.TrustKey == "" { @@ -171,8 +165,10 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error { } if ping, err := cli.client.Ping(context.Background()); err == nil { - cli.hasExperimental = ping.Experimental - cli.osType = ping.OSType + cli.server = ServerInfo{ + HasExperimental: ping.Experimental, + OSType: ping.OSType, + } // since the new header was added in 1.25, assume server is 1.24 if header is not present. if ping.APIVersion == "" { @@ -184,9 +180,17 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error { cli.client.UpdateClientVersion(ping.APIVersion) } } + return nil } +// ServerInfo stores details about the supported features and platform of the +// server +type ServerInfo struct { + HasExperimental bool + OSType string +} + // NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err. func NewDockerCli(in io.ReadCloser, out, err io.Writer) *DockerCli { return &DockerCli{in: NewInStream(in), out: NewOutStream(out), err: err} From 1373ab09883bdb274f69deaf7f5981c8a7ac5d22 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 14 Mar 2017 17:53:29 -0400 Subject: [PATCH 677/978] Create a new ServerType struct for storing details about the server on the client. Signed-off-by: Daniel Nephin Upstream-commit: 749d8b2bdc87cff53013512245ccdfd9651749b6 Component: cli --- components/cli/docker.go | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/components/cli/docker.go b/components/cli/docker.go index 8d589d4416..96283debc4 100644 --- a/components/cli/docker.go +++ b/components/cli/docker.go @@ -14,6 +14,7 @@ import ( cliconfig "github.com/docker/docker/cli/config" "github.com/docker/docker/cli/debug" cliflags "github.com/docker/docker/cli/flags" + "github.com/docker/docker/client" "github.com/docker/docker/dockerversion" "github.com/docker/docker/pkg/term" "github.com/spf13/cobra" @@ -49,7 +50,7 @@ func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command { if err := dockerCli.Initialize(opts); err != nil { return err } - return isSupported(cmd, dockerCli.Client().ClientVersion(), dockerCli.OSType(), dockerCli.HasExperimental()) + return isSupported(cmd, dockerCli) }, } cli.SetupRootCommand(cmd) @@ -80,7 +81,7 @@ func setFlagErrorFunc(dockerCli *command.DockerCli, cmd *cobra.Command, flags *p flagErrorFunc := cmd.FlagErrorFunc() cmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error { initializeDockerCli(dockerCli, flags, opts) - if err := isSupported(cmd, dockerCli.Client().ClientVersion(), dockerCli.OSType(), dockerCli.HasExperimental()); err != nil { + if err := isSupported(cmd, dockerCli); err != nil { return err } return flagErrorFunc(cmd, err) @@ -90,12 +91,12 @@ func setFlagErrorFunc(dockerCli *command.DockerCli, cmd *cobra.Command, flags *p func setHelpFunc(dockerCli *command.DockerCli, cmd *cobra.Command, flags *pflag.FlagSet, opts *cliflags.ClientOptions) { cmd.SetHelpFunc(func(ccmd *cobra.Command, args []string) { initializeDockerCli(dockerCli, flags, opts) - if err := isSupported(ccmd, dockerCli.Client().ClientVersion(), dockerCli.OSType(), dockerCli.HasExperimental()); err != nil { + if err := isSupported(ccmd, dockerCli); err != nil { ccmd.Println(err) return } - hideUnsupportedFeatures(ccmd, dockerCli.Client().ClientVersion(), dockerCli.OSType(), dockerCli.HasExperimental()) + hideUnsupportedFeatures(ccmd, dockerCli) if err := ccmd.Help(); err != nil { ccmd.Println(err) @@ -122,7 +123,7 @@ func setValidateArgs(dockerCli *command.DockerCli, cmd *cobra.Command, flags *pf cmdArgs := ccmd.Args ccmd.Args = func(cmd *cobra.Command, args []string) error { initializeDockerCli(dockerCli, flags, opts) - if err := isSupported(cmd, dockerCli.Client().ClientVersion(), dockerCli.OSType(), dockerCli.HasExperimental()); err != nil { + if err := isSupported(cmd, dockerCli); err != nil { return err } return cmdArgs(cmd, args) @@ -198,7 +199,16 @@ func dockerPreRun(opts *cliflags.ClientOptions) { } } -func hideUnsupportedFeatures(cmd *cobra.Command, clientVersion, osType string, hasExperimental bool) { +type versionDetails interface { + Client() client.APIClient + ServerInfo() command.ServerInfo +} + +func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) { + clientVersion := details.Client().ClientVersion() + osType := details.ServerInfo().OSType + hasExperimental := details.ServerInfo().HasExperimental + cmd.Flags().VisitAll(func(f *pflag.Flag) { // hide experimental flags if !hasExperimental { @@ -228,7 +238,11 @@ func hideUnsupportedFeatures(cmd *cobra.Command, clientVersion, osType string, h } } -func isSupported(cmd *cobra.Command, clientVersion, osType string, hasExperimental bool) error { +func isSupported(cmd *cobra.Command, details versionDetails) error { + clientVersion := details.Client().ClientVersion() + osType := details.ServerInfo().OSType + hasExperimental := details.ServerInfo().HasExperimental + // Check recursively so that, e.g., `docker stack ls` returns the same output as `docker stack` for curr := cmd; curr != nil; curr = curr.Parent() { if cmdVersion, ok := curr.Tags["version"]; ok && versions.LessThan(clientVersion, cmdVersion) { From 748bae2fe62579c6f768de84c14476ec3c86549a Mon Sep 17 00:00:00 2001 From: "John Howard (VM)" Date: Tue, 21 Mar 2017 15:55:18 -0700 Subject: [PATCH 678/978] Windows: Don't close client stdin handle to avoid hang Signed-off-by: John Howard (VM) Upstream-commit: e8be542957cb23dcb23f428c0fb7e649909a02e5 Component: cli --- components/cli/command/container/hijack.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/components/cli/command/container/hijack.go b/components/cli/command/container/hijack.go index ca136f0e43..11acf114f0 100644 --- a/components/cli/command/container/hijack.go +++ b/components/cli/command/container/hijack.go @@ -105,11 +105,19 @@ func setRawTerminal(streams command.Streams) error { func restoreTerminal(streams command.Streams, in io.Closer) error { streams.In().RestoreTerminal() streams.Out().RestoreTerminal() - // WARNING: DO NOT REMOVE THE OS CHECK !!! + // WARNING: DO NOT REMOVE THE OS CHECKS !!! // For some reason this Close call blocks on darwin.. - // As the client exists right after, simply discard the close + // As the client exits right after, simply discard the close // until we find a better solution. - if in != nil && runtime.GOOS != "darwin" { + // + // This can also cause the client on Windows to get stuck in Win32 CloseHandle() + // in some cases. See https://github.com/docker/docker/issues/28267#issuecomment-288237442 + // Tracked internally at Microsoft by VSO #11352156. In the + // Windows case, you hit this if you are using the native/v2 console, + // not the "legacy" console, and you start the client in a new window. eg + // `start docker run --rm -it microsoft/nanoserver cmd /s /c echo foobar` + // will hang. Remove start, and it won't repro. + if in != nil && runtime.GOOS != "darwin" && runtime.GOOS != "windows" { return in.Close() } return nil From f91e9b960badf5065374724161d1259436977d13 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Wed, 22 Mar 2017 00:21:15 +0100 Subject: [PATCH 679/978] update "docker daemon" deprecation message for new version scheme Signed-off-by: Sebastiaan van Stijn Upstream-commit: b83bf0a4fd0a1cb42cecf5361eb0f8d393310f11 Component: cli --- components/cli/daemon_unix.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/daemon_unix.go b/components/cli/daemon_unix.go index f68d220c2f..6ec6b625a1 100644 --- a/components/cli/daemon_unix.go +++ b/components/cli/daemon_unix.go @@ -24,7 +24,7 @@ func newDaemonCommand() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { return runDaemon() }, - Deprecated: "and will be removed in Docker 1.16. Please run `dockerd` directly.", + Deprecated: "and will be removed in Docker 17.12. Please run `dockerd` directly.", } cmd.SetHelpFunc(helpFunc) return cmd From eb1ed29f05c59fecd70d03d3c604d9d8915c857c Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Tue, 21 Mar 2017 16:19:59 -0700 Subject: [PATCH 680/978] cli: Wrong error message from "node ps" outside swarm mode "docker node ps" behaves strangely outside swarm mode: $ docker node ps ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS Error: No such node: It should explain that the node is not a swarm manager. The reason this happens is that the argument to "docker node ps" defaults to "self". The first thing the command does is try to resolve "self" to a node ID using the /info endpoint. If there is no node ID, it tries to use the empty string as an ID, and tries to GET /nodes/, which is not a valid endpoint. Change the command to check if the node ID is present in the /info response. If it isn't, a swarm API endpoint can supply a useful error message. Also, avoid printing the column headers if the only following text is an error. Signed-off-by: Aaron Lehmann Upstream-commit: d59f6d09339edfe4295767a435df68de700bd5c7 Component: cli --- components/cli/command/node/cmd.go | 13 +++++++++++++ components/cli/command/node/inspect_test.go | 2 +- components/cli/command/node/ps.go | 6 ++++-- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/components/cli/command/node/cmd.go b/components/cli/command/node/cmd.go index 6bb6c3b28a..ea8b40a9a6 100644 --- a/components/cli/command/node/cmd.go +++ b/components/cli/command/node/cmd.go @@ -1,6 +1,9 @@ package node import ( + "errors" + + "github.com/docker/docker/api/types" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" apiclient "github.com/docker/docker/client" @@ -38,6 +41,16 @@ func Reference(ctx context.Context, client apiclient.APIClient, ref string) (str if err != nil { return "", err } + if info.Swarm.NodeID == "" { + // If there's no node ID in /info, the node probably + // isn't a manager. Call a swarm-specific endpoint to + // get a more specific error message. + _, err = client.NodeList(ctx, types.NodeListOptions{}) + if err != nil { + return "", err + } + return "", errors.New("node ID not found in /info") + } return info.Swarm.NodeID, nil } return ref, nil diff --git a/components/cli/command/node/inspect_test.go b/components/cli/command/node/inspect_test.go index 91bd41e165..59f7049bdc 100644 --- a/components/cli/command/node/inspect_test.go +++ b/components/cli/command/node/inspect_test.go @@ -49,7 +49,7 @@ func TestNodeInspectErrors(t *testing.T) { return swarm.Node{}, []byte{}, fmt.Errorf("error inspecting the node") }, infoFunc: func() (types.Info, error) { - return types.Info{}, nil + return types.Info{Swarm: swarm.Info{NodeID: "abc"}}, nil }, expectedError: "error inspecting the node", }, diff --git a/components/cli/command/node/ps.go b/components/cli/command/node/ps.go index cb0f3efdfc..da57255761 100644 --- a/components/cli/command/node/ps.go +++ b/components/cli/command/node/ps.go @@ -95,8 +95,10 @@ func runPs(dockerCli command.Cli, opts psOptions) error { } } - if err := task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), !opts.noTrunc, opts.quiet, format); err != nil { - errs = append(errs, err.Error()) + if len(errs) == 0 || len(tasks) != 0 { + if err := task.Print(dockerCli, ctx, tasks, idresolver.New(client, opts.noResolve), !opts.noTrunc, opts.quiet, format); err != nil { + errs = append(errs, err.Error()) + } } if len(errs) > 0 { From 1d07f176cbc3555d2bbf8fc13430cbd5b55e9693 Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Tue, 21 Mar 2017 19:09:02 -0700 Subject: [PATCH 681/978] Return proper exit code on builder panic Signed-off-by: Tonis Tiigi Upstream-commit: 82b04969b74054f53c078112f452024e389c58a7 Component: cli --- components/cli/command/image/build.go | 1 + 1 file changed, 1 insertion(+) diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go index 5f7d5d07a8..040a2c2293 100644 --- a/components/cli/command/image/build.go +++ b/components/cli/command/image/build.go @@ -305,6 +305,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { } return cli.StatusError{Status: jerr.Message, StatusCode: jerr.Code} } + return err } // Windows: show error message about modified file permissions if the From 0c575acb3194661cd70543edcefde60d87a36a99 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Wed, 22 Mar 2017 15:42:03 +0100 Subject: [PATCH 682/978] Remove compose types.Dict alias It is just an alias type and make the code a little bit more complex and hard to use from outside `compose` package. Signed-off-by: Vincent Demeester Upstream-commit: a1e1ab78d0cda366e3dfda9099b8c5d74a2a3f10 Component: cli --- .../compose/interpolation/interpolation.go | 17 +++--- .../interpolation/interpolation_test.go | 22 ++++--- components/cli/compose/loader/loader.go | 57 ++++++++----------- components/cli/compose/loader/loader_test.go | 30 +++++----- components/cli/compose/types/types.go | 5 +- 5 files changed, 57 insertions(+), 74 deletions(-) diff --git a/components/cli/compose/interpolation/interpolation.go b/components/cli/compose/interpolation/interpolation.go index 29c2e0e279..2a89d57482 100644 --- a/components/cli/compose/interpolation/interpolation.go +++ b/components/cli/compose/interpolation/interpolation.go @@ -4,19 +4,18 @@ import ( "fmt" "github.com/docker/docker/cli/compose/template" - "github.com/docker/docker/cli/compose/types" ) // Interpolate replaces variables in a string with the values from a mapping -func Interpolate(config types.Dict, section string, mapping template.Mapping) (types.Dict, error) { - out := types.Dict{} +func Interpolate(config map[string]interface{}, section string, mapping template.Mapping) (map[string]interface{}, error) { + out := map[string]interface{}{} for name, item := range config { if item == nil { out[name] = nil continue } - interpolatedItem, err := interpolateSectionItem(name, item.(types.Dict), section, mapping) + interpolatedItem, err := interpolateSectionItem(name, item.(map[string]interface{}), section, mapping) if err != nil { return nil, err } @@ -28,12 +27,12 @@ func Interpolate(config types.Dict, section string, mapping template.Mapping) (t func interpolateSectionItem( name string, - item types.Dict, + item map[string]interface{}, section string, mapping template.Mapping, -) (types.Dict, error) { +) (map[string]interface{}, error) { - out := types.Dict{} + out := map[string]interface{}{} for key, value := range item { interpolatedValue, err := recursiveInterpolate(value, mapping) @@ -60,8 +59,8 @@ func recursiveInterpolate( case string: return template.Substitute(value, mapping) - case types.Dict: - out := types.Dict{} + case map[string]interface{}: + out := map[string]interface{}{} for key, elem := range value { interpolatedElem, err := recursiveInterpolate(elem, mapping) if err != nil { diff --git a/components/cli/compose/interpolation/interpolation_test.go b/components/cli/compose/interpolation/interpolation_test.go index 1852b9eb44..9b055f4703 100644 --- a/components/cli/compose/interpolation/interpolation_test.go +++ b/components/cli/compose/interpolation/interpolation_test.go @@ -4,8 +4,6 @@ import ( "testing" "github.com/stretchr/testify/assert" - - "github.com/docker/docker/cli/compose/types" ) var defaults = map[string]string{ @@ -19,25 +17,25 @@ func defaultMapping(name string) (string, bool) { } func TestInterpolate(t *testing.T) { - services := types.Dict{ - "servicea": types.Dict{ + services := map[string]interface{}{ + "servicea": map[string]interface{}{ "image": "example:${USER}", "volumes": []interface{}{"$FOO:/target"}, - "logging": types.Dict{ + "logging": map[string]interface{}{ "driver": "${FOO}", - "options": types.Dict{ + "options": map[string]interface{}{ "user": "$USER", }, }, }, } - expected := types.Dict{ - "servicea": types.Dict{ + expected := map[string]interface{}{ + "servicea": map[string]interface{}{ "image": "example:jenny", "volumes": []interface{}{"bar:/target"}, - "logging": types.Dict{ + "logging": map[string]interface{}{ "driver": "bar", - "options": types.Dict{ + "options": map[string]interface{}{ "user": "jenny", }, }, @@ -49,8 +47,8 @@ func TestInterpolate(t *testing.T) { } func TestInvalidInterpolation(t *testing.T) { - services := types.Dict{ - "servicea": types.Dict{ + services := map[string]interface{}{ + "servicea": map[string]interface{}{ "image": "${", }, } diff --git a/components/cli/compose/loader/loader.go b/components/cli/compose/loader/loader.go index 0653691cd9..9085cf65cf 100644 --- a/components/cli/compose/loader/loader.go +++ b/components/cli/compose/loader/loader.go @@ -28,7 +28,7 @@ var ( // ParseYAML reads the bytes from a file, parses the bytes into a mapping // structure, and returns it. -func ParseYAML(source []byte) (types.Dict, error) { +func ParseYAML(source []byte) (map[string]interface{}, error) { var cfg interface{} if err := yaml.Unmarshal(source, &cfg); err != nil { return nil, err @@ -41,7 +41,7 @@ func ParseYAML(source []byte) (types.Dict, error) { if err != nil { return nil, err } - return converted.(types.Dict), nil + return converted.(map[string]interface{}), nil } // Load reads a ConfigDetails and returns a fully loaded configuration @@ -56,7 +56,7 @@ func Load(configDetails types.ConfigDetails) (*types.Config, error) { configDict := getConfigDict(configDetails) if services, ok := configDict["services"]; ok { - if servicesDict, ok := services.(types.Dict); ok { + if servicesDict, ok := services.(map[string]interface{}); ok { forbidden := getProperties(servicesDict, types.ForbiddenProperties) if len(forbidden) > 0 { @@ -75,7 +75,7 @@ func Load(configDetails types.ConfigDetails) (*types.Config, error) { return v, ok } if services, ok := configDict["services"]; ok { - servicesConfig, err := interpolation.Interpolate(services.(types.Dict), "service", lookupEnv) + servicesConfig, err := interpolation.Interpolate(services.(map[string]interface{}), "service", lookupEnv) if err != nil { return nil, err } @@ -89,7 +89,7 @@ func Load(configDetails types.ConfigDetails) (*types.Config, error) { } if networks, ok := configDict["networks"]; ok { - networksConfig, err := interpolation.Interpolate(networks.(types.Dict), "network", lookupEnv) + networksConfig, err := interpolation.Interpolate(networks.(map[string]interface{}), "network", lookupEnv) if err != nil { return nil, err } @@ -103,7 +103,7 @@ func Load(configDetails types.ConfigDetails) (*types.Config, error) { } if volumes, ok := configDict["volumes"]; ok { - volumesConfig, err := interpolation.Interpolate(volumes.(types.Dict), "volume", lookupEnv) + volumesConfig, err := interpolation.Interpolate(volumes.(map[string]interface{}), "volume", lookupEnv) if err != nil { return nil, err } @@ -117,7 +117,7 @@ func Load(configDetails types.ConfigDetails) (*types.Config, error) { } if secrets, ok := configDict["secrets"]; ok { - secretsConfig, err := interpolation.Interpolate(secrets.(types.Dict), "secret", lookupEnv) + secretsConfig, err := interpolation.Interpolate(secrets.(map[string]interface{}), "secret", lookupEnv) if err != nil { return nil, err } @@ -139,7 +139,7 @@ func GetUnsupportedProperties(configDetails types.ConfigDetails) []string { unsupported := map[string]bool{} for _, service := range getServices(getConfigDict(configDetails)) { - serviceDict := service.(types.Dict) + serviceDict := service.(map[string]interface{}) for _, property := range types.UnsupportedProperties { if _, isSet := serviceDict[property]; isSet { unsupported[property] = true @@ -165,11 +165,11 @@ func GetDeprecatedProperties(configDetails types.ConfigDetails) map[string]strin return getProperties(getServices(getConfigDict(configDetails)), types.DeprecatedProperties) } -func getProperties(services types.Dict, propertyMap map[string]string) map[string]string { +func getProperties(services map[string]interface{}, propertyMap map[string]string) map[string]string { output := map[string]string{} for _, service := range services { - if serviceDict, ok := service.(types.Dict); ok { + if serviceDict, ok := service.(map[string]interface{}); ok { for property, description := range propertyMap { if _, isSet := serviceDict[property]; isSet { output[property] = description @@ -192,18 +192,18 @@ func (e *ForbiddenPropertiesError) Error() string { } // TODO: resolve multiple files into a single config -func getConfigDict(configDetails types.ConfigDetails) types.Dict { +func getConfigDict(configDetails types.ConfigDetails) map[string]interface{} { return configDetails.ConfigFiles[0].Config } -func getServices(configDict types.Dict) types.Dict { +func getServices(configDict map[string]interface{}) map[string]interface{} { if services, ok := configDict["services"]; ok { - if servicesDict, ok := services.(types.Dict); ok { + if servicesDict, ok := services.(map[string]interface{}); ok { return servicesDict } } - return types.Dict{} + return map[string]interface{}{} } func transform(source map[string]interface{}, target interface{}) error { @@ -265,10 +265,9 @@ func transformHook( } // keys needs to be converted to strings for jsonschema -// TODO: don't use types.Dict func convertToStringKeysRecursive(value interface{}, keyPrefix string) (interface{}, error) { if mapping, ok := value.(map[interface{}]interface{}); ok { - dict := make(types.Dict) + dict := make(map[string]interface{}) for key, entry := range mapping { str, ok := key.(string) if !ok { @@ -315,11 +314,11 @@ func formatInvalidKeyError(keyPrefix string, key interface{}) error { // LoadServices produces a ServiceConfig map from a compose file Dict // the servicesDict is not validated if directly used. Use Load() to enable validation -func LoadServices(servicesDict types.Dict, workingDir string, lookupEnv template.Mapping) ([]types.ServiceConfig, error) { +func LoadServices(servicesDict map[string]interface{}, workingDir string, lookupEnv template.Mapping) ([]types.ServiceConfig, error) { var services []types.ServiceConfig for name, serviceDef := range servicesDict { - serviceConfig, err := LoadService(name, serviceDef.(types.Dict), workingDir, lookupEnv) + serviceConfig, err := LoadService(name, serviceDef.(map[string]interface{}), workingDir, lookupEnv) if err != nil { return nil, err } @@ -331,7 +330,7 @@ func LoadServices(servicesDict types.Dict, workingDir string, lookupEnv template // LoadService produces a single ServiceConfig from a compose file Dict // the serviceDict is not validated if directly used. Use Load() to enable validation -func LoadService(name string, serviceDict types.Dict, workingDir string, lookupEnv template.Mapping) (*types.ServiceConfig, error) { +func LoadService(name string, serviceDict map[string]interface{}, workingDir string, lookupEnv template.Mapping) (*types.ServiceConfig, error) { serviceConfig := &types.ServiceConfig{} if err := transform(serviceDict, serviceConfig); err != nil { return nil, err @@ -409,7 +408,7 @@ func transformUlimits(data interface{}) (interface{}, error) { switch value := data.(type) { case int: return types.UlimitsConfig{Single: value}, nil - case types.Dict: + case map[string]interface{}: ulimit := types.UlimitsConfig{} ulimit.Soft = value["soft"].(int) ulimit.Hard = value["hard"].(int) @@ -421,7 +420,7 @@ func transformUlimits(data interface{}) (interface{}, error) { // LoadNetworks produces a NetworkConfig map from a compose file Dict // the source Dict is not validated if directly used. Use Load() to enable validation -func LoadNetworks(source types.Dict) (map[string]types.NetworkConfig, error) { +func LoadNetworks(source map[string]interface{}) (map[string]types.NetworkConfig, error) { networks := make(map[string]types.NetworkConfig) err := transform(source, &networks) if err != nil { @@ -438,7 +437,7 @@ func LoadNetworks(source types.Dict) (map[string]types.NetworkConfig, error) { // LoadVolumes produces a VolumeConfig map from a compose file Dict // the source Dict is not validated if directly used. Use Load() to enable validation -func LoadVolumes(source types.Dict) (map[string]types.VolumeConfig, error) { +func LoadVolumes(source map[string]interface{}) (map[string]types.VolumeConfig, error) { volumes := make(map[string]types.VolumeConfig) err := transform(source, &volumes) if err != nil { @@ -467,7 +466,7 @@ func LoadVolumes(source types.Dict) (map[string]types.VolumeConfig, error) { // LoadSecrets produces a SecretConfig map from a compose file Dict // the source Dict is not validated if directly used. Use Load() to enable validation -func LoadSecrets(source types.Dict, workingDir string) (map[string]types.SecretConfig, error) { +func LoadSecrets(source map[string]interface{}, workingDir string) (map[string]types.SecretConfig, error) { secrets := make(map[string]types.SecretConfig) if err := transform(source, &secrets); err != nil { return secrets, err @@ -495,8 +494,6 @@ func transformMapStringString(data interface{}) (interface{}, error) { switch value := data.(type) { case map[string]interface{}: return toMapStringString(value, false), nil - case types.Dict: - return toMapStringString(value, false), nil case map[string]string: return value, nil default: @@ -508,8 +505,6 @@ func transformExternal(data interface{}) (interface{}, error) { switch value := data.(type) { case bool: return map[string]interface{}{"external": value}, nil - case types.Dict: - return map[string]interface{}{"external": true, "name": value["name"]}, nil case map[string]interface{}: return map[string]interface{}{"external": true, "name": value["name"]}, nil default: @@ -538,8 +533,6 @@ func transformServicePort(data interface{}) (interface{}, error) { return data, err } ports = append(ports, v...) - case types.Dict: - ports = append(ports, value) case map[string]interface{}: ports = append(ports, value) default: @@ -556,8 +549,6 @@ func transformServiceSecret(data interface{}) (interface{}, error) { switch value := data.(type) { case string: return map[string]interface{}{"source": value}, nil - case types.Dict: - return data, nil case map[string]interface{}: return data, nil default: @@ -569,8 +560,6 @@ func transformServiceVolumeConfig(data interface{}) (interface{}, error) { switch value := data.(type) { case string: return parseVolume(value) - case types.Dict: - return data, nil case map[string]interface{}: return data, nil default: @@ -612,7 +601,7 @@ func transformStringList(data interface{}) (interface{}, error) { func transformMappingOrList(mappingOrList interface{}, sep string, allowNil bool) interface{} { switch value := mappingOrList.(type) { - case types.Dict: + case map[string]interface{}: return toMapStringString(value, allowNil) case ([]interface{}): result := make(map[string]interface{}) diff --git a/components/cli/compose/loader/loader_test.go b/components/cli/compose/loader/loader_test.go index 661e2c615c..e7e2992ada 100644 --- a/components/cli/compose/loader/loader_test.go +++ b/components/cli/compose/loader/loader_test.go @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/assert" ) -func buildConfigDetails(source types.Dict, env map[string]string) types.ConfigDetails { +func buildConfigDetails(source map[string]interface{}, env map[string]string) types.ConfigDetails { workingDir, err := os.Getwd() if err != nil { panic(err) @@ -70,39 +70,39 @@ networks: - subnet: 172.28.0.0/16 ` -var sampleDict = types.Dict{ +var sampleDict = map[string]interface{}{ "version": "3", - "services": types.Dict{ - "foo": types.Dict{ + "services": map[string]interface{}{ + "foo": map[string]interface{}{ "image": "busybox", - "networks": types.Dict{"with_me": nil}, + "networks": map[string]interface{}{"with_me": nil}, }, - "bar": types.Dict{ + "bar": map[string]interface{}{ "image": "busybox", "environment": []interface{}{"FOO=1"}, "networks": []interface{}{"with_ipam"}, }, }, - "volumes": types.Dict{ - "hello": types.Dict{ + "volumes": map[string]interface{}{ + "hello": map[string]interface{}{ "driver": "default", - "driver_opts": types.Dict{ + "driver_opts": map[string]interface{}{ "beep": "boop", }, }, }, - "networks": types.Dict{ - "default": types.Dict{ + "networks": map[string]interface{}{ + "default": map[string]interface{}{ "driver": "bridge", - "driver_opts": types.Dict{ + "driver_opts": map[string]interface{}{ "beep": "boop", }, }, - "with_ipam": types.Dict{ - "ipam": types.Dict{ + "with_ipam": map[string]interface{}{ + "ipam": map[string]interface{}{ "driver": "default", "config": []interface{}{ - types.Dict{ + map[string]interface{}{ "subnet": "172.28.0.0/16", }, }, diff --git a/components/cli/compose/types/types.go b/components/cli/compose/types/types.go index 3e6651fd32..19500e195d 100644 --- a/components/cli/compose/types/types.go +++ b/components/cli/compose/types/types.go @@ -50,13 +50,10 @@ var ForbiddenProperties = map[string]string{ "memswap_limit": "Set resource limits using deploy.resources", } -// Dict is a mapping of strings to interface{} -type Dict map[string]interface{} - // ConfigFile is a filename and the contents of the file as a Dict type ConfigFile struct { Filename string - Config Dict + Config map[string]interface{} } // ConfigDetails are the details about a group of ConfigFiles From ff2a4eeb5b95af928cfd92b4eabcb255901b77cb Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Thu, 23 Mar 2017 16:05:24 +0100 Subject: [PATCH 683/978] Make sure we error out instead of panic during interpolation Use type assertion to error out if the type isn't the right one instead of panic as before this change. Signed-off-by: Vincent Demeester Upstream-commit: fe19bc6891936fc5bb79e9e774c3d6f5e8880c84 Component: cli --- components/cli/compose/interpolation/interpolation.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/components/cli/compose/interpolation/interpolation.go b/components/cli/compose/interpolation/interpolation.go index 2a89d57482..c8e962b490 100644 --- a/components/cli/compose/interpolation/interpolation.go +++ b/components/cli/compose/interpolation/interpolation.go @@ -1,9 +1,8 @@ package interpolation import ( - "fmt" - "github.com/docker/docker/cli/compose/template" + "github.com/pkg/errors" ) // Interpolate replaces variables in a string with the values from a mapping @@ -15,7 +14,11 @@ func Interpolate(config map[string]interface{}, section string, mapping template out[name] = nil continue } - interpolatedItem, err := interpolateSectionItem(name, item.(map[string]interface{}), section, mapping) + mapItem, ok := item.(map[string]interface{}) + if !ok { + return nil, errors.Errorf("Invalid type for %s : %T instead of %T", name, item, out) + } + interpolatedItem, err := interpolateSectionItem(name, mapItem, section, mapping) if err != nil { return nil, err } @@ -37,7 +40,7 @@ func interpolateSectionItem( for key, value := range item { interpolatedValue, err := recursiveInterpolate(value, mapping) if err != nil { - return nil, fmt.Errorf( + return nil, errors.Errorf( "Invalid interpolation format for %#v option in %s %#v: %#v. You may need to escape any $ with another $.", key, section, name, err.Template, ) From d7600dd8413619e32d2fe3246078b788184c196c Mon Sep 17 00:00:00 2001 From: "John Howard (VM)" Date: Tue, 21 Mar 2017 10:02:16 -0700 Subject: [PATCH 684/978] Windows: Remove --credentialspec flag Signed-off-by: John Howard (VM) Upstream-commit: c8f2ef1b1e0c7a609b51b4c98b7c27f5ec601697 Component: cli --- components/cli/command/container/opts.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/components/cli/command/container/opts.go b/components/cli/command/container/opts.go index febddbc5d1..73cde873b4 100644 --- a/components/cli/command/container/opts.go +++ b/components/cli/command/container/opts.go @@ -115,7 +115,6 @@ type containerOptions struct { autoRemove bool init bool initPath string - credentialSpec string Image string Args []string @@ -188,8 +187,6 @@ func addFlags(flags *pflag.FlagSet) *containerOptions { flags.BoolVar(&copts.privileged, "privileged", false, "Give extended privileges to this container") flags.Var(&copts.securityOpt, "security-opt", "Security Options") flags.StringVar(&copts.usernsMode, "userns", "", "User namespace to use") - flags.StringVar(&copts.credentialSpec, "credentialspec", "", "Credential spec for managed service account (Windows only)") - flags.SetAnnotation("credentialspec", "ostype", []string{"windows"}) // Network and port publishing flag flags.Var(&copts.extraHosts, "add-host", "Add a custom host-to-IP mapping (host:ip)") From 8cc3b14857d2f5c2faff291010d11b5960657fe1 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 24 Mar 2017 10:43:28 -0400 Subject: [PATCH 685/978] Cleanup compose convert error messages. Signed-off-by: Daniel Nephin Upstream-commit: c70387aebc116fba8092cb383b0cc2eb36356460 Component: cli --- components/cli/compose/convert/service.go | 15 +++++++++------ components/cli/compose/convert/volume.go | 2 +- components/cli/compose/convert/volume_test.go | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/components/cli/compose/convert/service.go b/components/cli/compose/convert/service.go index 497dbe004f..8e31cbe8fb 100644 --- a/components/cli/compose/convert/service.go +++ b/components/cli/compose/convert/service.go @@ -14,6 +14,7 @@ import ( "github.com/docker/docker/client" "github.com/docker/docker/opts" runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/pkg/errors" ) const defaultNetwork = "default" @@ -35,11 +36,11 @@ func Services( secrets, err := convertServiceSecrets(client, namespace, service.Secrets, config.Secrets) if err != nil { - return nil, err + return nil, errors.Wrapf(err, "service %s", service.Name) } serviceSpec, err := convertService(namespace, service, networks, volumes, secrets) if err != nil { - return nil, err + return nil, errors.Wrapf(err, "service %s", service.Name) } result[service.Name] = serviceSpec } @@ -68,7 +69,6 @@ func convertService( mounts, err := Volumes(service.Volumes, volumes, namespace) if err != nil { - // TODO: better error message (include service name) return swarm.ServiceSpec{}, err } @@ -167,8 +167,7 @@ func convertServiceNetworks( for networkName, network := range networks { networkConfig, ok := networkConfigs[networkName] if !ok && networkName != defaultNetwork { - return []swarm.NetworkAttachmentConfig{}, fmt.Errorf( - "service %q references network %q, which is not declared", name, networkName) + return nil, errors.Errorf("undefined network %q", networkName) } var aliases []string if network != nil { @@ -202,8 +201,12 @@ func convertServiceSecrets( target = secret.Source } + secretSpec, exists := secretSpecs[secret.Source] + if !exists { + return nil, errors.Errorf("undefined secret %q", secret.Source) + } + source := namespace.Scope(secret.Source) - secretSpec := secretSpecs[secret.Source] if secretSpec.External.External { source = secretSpec.External.Name } diff --git a/components/cli/compose/convert/volume.go b/components/cli/compose/convert/volume.go index 682b44377a..d6b14283ad 100644 --- a/components/cli/compose/convert/volume.go +++ b/components/cli/compose/convert/volume.go @@ -57,7 +57,7 @@ func convertVolumeToMount( stackVolume, exists := stackVolumes[volume.Source] if !exists { - return result, errors.Errorf("undefined volume: %s", volume.Source) + return result, errors.Errorf("undefined volume %q", volume.Source) } result.Source = namespace.Scope(volume.Source) diff --git a/components/cli/compose/convert/volume_test.go b/components/cli/compose/convert/volume_test.go index 705f03f404..73d642e5fe 100644 --- a/components/cli/compose/convert/volume_test.go +++ b/components/cli/compose/convert/volume_test.go @@ -188,5 +188,5 @@ func TestConvertVolumeToMountVolumeDoesNotExist(t *testing.T) { ReadOnly: true, } _, err := convertVolumeToMount(config, volumes{}, namespace) - assert.Error(t, err, "undefined volume: unknown") + assert.Error(t, err, "undefined volume \"unknown\"") } From 7827d424b6c0533386334b7897623351155c7135 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 24 Mar 2017 12:24:58 -0400 Subject: [PATCH 686/978] Fix external volume error to pass validation. Signed-off-by: Daniel Nephin Upstream-commit: c1b2fad9aa791f8f1f80d2871f64e99fdec1db97 Component: cli --- components/cli/compose/loader/loader.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/components/cli/compose/loader/loader.go b/components/cli/compose/loader/loader.go index 9085cf65cf..821097bbf6 100644 --- a/components/cli/compose/loader/loader.go +++ b/components/cli/compose/loader/loader.go @@ -19,6 +19,7 @@ import ( units "github.com/docker/go-units" shellwords "github.com/mattn/go-shellwords" "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" yaml "gopkg.in/yaml.v2" ) @@ -435,6 +436,12 @@ func LoadNetworks(source map[string]interface{}) (map[string]types.NetworkConfig return networks, nil } +func externalVolumeError(volume, key string) error { + return errors.Errorf( + "conflicting parameters \"external\" and %q specified for volume %q", + key, volume) +} + // LoadVolumes produces a VolumeConfig map from a compose file Dict // the source Dict is not validated if directly used. Use Load() to enable validation func LoadVolumes(source map[string]interface{}) (map[string]types.VolumeConfig, error) { @@ -445,15 +452,14 @@ func LoadVolumes(source map[string]interface{}) (map[string]types.VolumeConfig, } for name, volume := range volumes { if volume.External.External { - template := "conflicting parameters \"external\" and %q specified for volume %q" if volume.Driver != "" { - return nil, fmt.Errorf(template, "driver", name) + return nil, externalVolumeError(name, "driver") } if len(volume.DriverOpts) > 0 { - return nil, fmt.Errorf(template, "driver_opts", name) + return nil, externalVolumeError(name, "driver_opts") } if len(volume.Labels) > 0 { - return nil, fmt.Errorf(template, "labels", name) + return nil, externalVolumeError(name, "labels") } if volume.External.Name == "" { volume.External.Name = name From 25b1b9c3c289632855daed340a69344bf348e187 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 9 Mar 2017 13:23:45 -0500 Subject: [PATCH 687/978] Replace fmt.Errorf() with errors.Errorf() in the cli Signed-off-by: Daniel Nephin Upstream-commit: e9d6193dfd90d994ac9ddc84d8cd40d0b80389b1 Component: cli --- components/cli/cobra.go | 3 +- .../cli/command/bundlefile/bundlefile.go | 7 +-- components/cli/command/cli.go | 2 +- components/cli/command/container/attach.go | 2 +- components/cli/command/container/cp.go | 5 +- components/cli/command/container/create.go | 9 ++-- components/cli/command/container/diff.go | 2 +- components/cli/command/container/export.go | 2 +- components/cli/command/container/kill.go | 2 +- components/cli/command/container/opts.go | 51 ++++++++++--------- components/cli/command/container/opts_test.go | 9 ++-- components/cli/command/container/pause.go | 2 +- components/cli/command/container/port.go | 3 +- components/cli/command/container/rename.go | 4 +- components/cli/command/container/restart.go | 2 +- components/cli/command/container/rm.go | 2 +- components/cli/command/container/run.go | 2 +- components/cli/command/container/start.go | 4 +- components/cli/command/container/stats.go | 2 +- .../cli/command/container/stats_helpers.go | 2 +- components/cli/command/container/stop.go | 2 +- components/cli/command/container/unpause.go | 2 +- components/cli/command/container/update.go | 2 +- components/cli/command/container/wait.go | 2 +- components/cli/command/formatter/formatter.go | 6 +-- components/cli/command/formatter/reflect.go | 11 ++-- components/cli/command/formatter/service.go | 4 +- .../cli/command/idresolver/idresolver.go | 5 +- components/cli/command/image/build.go | 9 ++-- components/cli/command/image/build/context.go | 35 ++++++------- components/cli/command/image/load.go | 4 +- components/cli/command/image/pull.go | 2 +- components/cli/command/image/remove.go | 3 +- components/cli/command/image/save.go | 2 +- components/cli/command/image/trust.go | 12 ++--- components/cli/command/in.go | 2 +- components/cli/command/inspect/inspector.go | 10 ++-- components/cli/command/network/create.go | 19 +++---- components/cli/command/node/demote_test.go | 10 ++-- components/cli/command/node/inspect_test.go | 11 ++-- components/cli/command/node/list_test.go | 6 +-- components/cli/command/node/promote_test.go | 10 ++-- components/cli/command/node/ps.go | 4 +- components/cli/command/node/ps_test.go | 7 +-- components/cli/command/node/remove.go | 3 +- components/cli/command/node/remove_test.go | 4 +- components/cli/command/node/update.go | 4 +- components/cli/command/node/update_test.go | 16 +++--- components/cli/command/plugin/create.go | 3 +- components/cli/command/plugin/enable.go | 3 +- components/cli/command/plugin/install.go | 6 +-- components/cli/command/plugin/push.go | 5 +- components/cli/command/plugin/upgrade.go | 4 +- components/cli/command/registry.go | 7 +-- components/cli/command/registry/login.go | 3 +- components/cli/command/secret/create.go | 3 +- components/cli/command/secret/remove.go | 3 +- components/cli/command/service/inspect.go | 8 +-- components/cli/command/service/logs.go | 11 ++-- components/cli/command/service/opts.go | 12 ++--- components/cli/command/service/parse.go | 7 ++- components/cli/command/service/ps.go | 4 +- components/cli/command/service/remove.go | 3 +- components/cli/command/service/scale.go | 15 +++--- components/cli/command/service/trust.go | 3 +- components/cli/command/service/update.go | 10 ++-- components/cli/command/stack/deploy.go | 4 +- .../cli/command/stack/deploy_composefile.go | 6 +-- components/cli/command/stack/list.go | 3 +- components/cli/command/stack/opts.go | 5 +- components/cli/command/stack/remove.go | 3 +- components/cli/command/swarm/init.go | 2 +- components/cli/command/swarm/init_test.go | 11 ++-- components/cli/command/swarm/join.go | 3 +- components/cli/command/swarm/join_test.go | 6 +-- components/cli/command/swarm/join_token.go | 2 +- .../cli/command/swarm/join_token_test.go | 11 ++-- components/cli/command/swarm/leave_test.go | 4 +- components/cli/command/swarm/opts.go | 6 +-- components/cli/command/swarm/unlock.go | 2 +- .../cli/command/swarm/unlock_key_test.go | 7 +-- components/cli/command/swarm/unlock_test.go | 6 +-- components/cli/command/swarm/update_test.go | 23 +++++---- components/cli/command/system/inspect.go | 5 +- components/cli/command/volume/create.go | 3 +- components/cli/command/volume/create_test.go | 14 ++--- components/cli/command/volume/inspect_test.go | 7 +-- components/cli/command/volume/list_test.go | 4 +- components/cli/command/volume/prune_test.go | 3 +- components/cli/command/volume/remove.go | 3 +- components/cli/command/volume/remove_test.go | 4 +- components/cli/compose/convert/service.go | 8 +-- components/cli/compose/loader/loader.go | 30 +++++------ components/cli/compose/schema/bindata.go | 21 ++++---- components/cli/config/config.go | 12 ++--- components/cli/config/configfile/file.go | 12 ++--- .../config/credentials/native_store_test.go | 3 +- components/cli/required.go | 14 ++--- components/cli/trust/trust.go | 26 +++++----- 99 files changed, 370 insertions(+), 337 deletions(-) diff --git a/components/cli/cobra.go b/components/cli/cobra.go index b01774f04a..c7bb39c43d 100644 --- a/components/cli/cobra.go +++ b/components/cli/cobra.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/docker/docker/pkg/term" + "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -51,7 +52,7 @@ var helpCommand = &cobra.Command{ RunE: func(c *cobra.Command, args []string) error { cmd, args, e := c.Root().Find(args) if cmd == nil || e != nil || len(args) > 0 { - return fmt.Errorf("unknown help topic: %v", strings.Join(args, " ")) + return errors.Errorf("unknown help topic: %v", strings.Join(args, " ")) } helpFunc := cmd.HelpFunc() diff --git a/components/cli/command/bundlefile/bundlefile.go b/components/cli/command/bundlefile/bundlefile.go index 7fd1e4f6c4..07e2c8b081 100644 --- a/components/cli/command/bundlefile/bundlefile.go +++ b/components/cli/command/bundlefile/bundlefile.go @@ -2,8 +2,9 @@ package bundlefile import ( "encoding/json" - "fmt" "io" + + "github.com/pkg/errors" ) // Bundlefile stores the contents of a bundlefile @@ -39,12 +40,12 @@ func LoadFile(reader io.Reader) (*Bundlefile, error) { if err := decoder.Decode(bundlefile); err != nil { switch jsonErr := err.(type) { case *json.SyntaxError: - return nil, fmt.Errorf( + return nil, errors.Errorf( "JSON syntax error at byte %v: %s", jsonErr.Offset, jsonErr.Error()) case *json.UnmarshalTypeError: - return nil, fmt.Errorf( + return nil, errors.Errorf( "Unexpected type at byte %v. Expected %s but received %s.", jsonErr.Offset, jsonErr.Type, diff --git a/components/cli/command/cli.go b/components/cli/command/cli.go index 77b05d5832..9db5d8d0f5 100644 --- a/components/cli/command/cli.go +++ b/components/cli/command/cli.go @@ -1,8 +1,8 @@ package command import ( - "errors" "fmt" + "github.com/pkg/errors" "io" "net/http" "os" diff --git a/components/cli/command/container/attach.go b/components/cli/command/container/attach.go index 073914dc35..7d2869f76c 100644 --- a/components/cli/command/container/attach.go +++ b/components/cli/command/container/attach.go @@ -1,7 +1,7 @@ package container import ( - "errors" + "github.com/pkg/errors" "io" "net/http/httputil" diff --git a/components/cli/command/container/cp.go b/components/cli/command/container/cp.go index 8df850b360..a1d7110a61 100644 --- a/components/cli/command/container/cp.go +++ b/components/cli/command/container/cp.go @@ -1,8 +1,6 @@ package container import ( - "errors" - "fmt" "io" "os" "path/filepath" @@ -13,6 +11,7 @@ import ( "github.com/docker/docker/cli/command" "github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/system" + "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" ) @@ -227,7 +226,7 @@ func copyToContainer(ctx context.Context, dockerCli *command.DockerCli, srcPath, content = os.Stdin resolvedDstPath = dstInfo.Path if !dstInfo.IsDir { - return fmt.Errorf("destination \"%s:%s\" must be a directory", dstContainer, dstPath) + return errors.Errorf("destination \"%s:%s\" must be a directory", dstContainer, dstPath) } } else { // Prepare source copy info. diff --git a/components/cli/command/container/create.go b/components/cli/command/container/create.go index ef894bad5a..9222b4060b 100644 --- a/components/cli/command/container/create.go +++ b/components/cli/command/container/create.go @@ -14,6 +14,7 @@ import ( apiclient "github.com/docker/docker/client" "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/registry" + "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" "golang.org/x/net/context" @@ -118,7 +119,7 @@ func (cid *cidFile) Close() error { return nil } if err := os.Remove(cid.path); err != nil { - return fmt.Errorf("failed to remove the CID file '%s': %s \n", cid.path, err) + return errors.Errorf("failed to remove the CID file '%s': %s \n", cid.path, err) } return nil @@ -126,7 +127,7 @@ func (cid *cidFile) Close() error { func (cid *cidFile) Write(id string) error { if _, err := cid.file.Write([]byte(id)); err != nil { - return fmt.Errorf("Failed to write the container ID to the file: %s", err) + return errors.Errorf("Failed to write the container ID to the file: %s", err) } cid.written = true return nil @@ -134,12 +135,12 @@ func (cid *cidFile) Write(id string) error { func newCIDFile(path string) (*cidFile, error) { if _, err := os.Stat(path); err == nil { - return nil, fmt.Errorf("Container ID file found, make sure the other container isn't running or delete %s", path) + return nil, errors.Errorf("Container ID file found, make sure the other container isn't running or delete %s", path) } f, err := os.Create(path) if err != nil { - return nil, fmt.Errorf("Failed to create the container ID file: %s", err) + return nil, errors.Errorf("Failed to create the container ID file: %s", err) } return &cidFile{path: path, file: f}, nil diff --git a/components/cli/command/container/diff.go b/components/cli/command/container/diff.go index 81260b05be..c279c4849c 100644 --- a/components/cli/command/container/diff.go +++ b/components/cli/command/container/diff.go @@ -1,8 +1,8 @@ package container import ( - "errors" "fmt" + "github.com/pkg/errors" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" diff --git a/components/cli/command/container/export.go b/components/cli/command/container/export.go index 42f90bbaaa..dfb514440e 100644 --- a/components/cli/command/container/export.go +++ b/components/cli/command/container/export.go @@ -1,7 +1,7 @@ package container import ( - "errors" + "github.com/pkg/errors" "io" "github.com/docker/docker/cli" diff --git a/components/cli/command/container/kill.go b/components/cli/command/container/kill.go index 5c7f7ba14b..32eea6c0b6 100644 --- a/components/cli/command/container/kill.go +++ b/components/cli/command/container/kill.go @@ -1,8 +1,8 @@ package container import ( - "errors" "fmt" + "github.com/pkg/errors" "strings" "github.com/docker/docker/cli" diff --git a/components/cli/command/container/opts.go b/components/cli/command/container/opts.go index febddbc5d1..f7472a3987 100644 --- a/components/cli/command/container/opts.go +++ b/components/cli/command/container/opts.go @@ -18,6 +18,7 @@ import ( "github.com/docker/docker/pkg/signal" runconfigopts "github.com/docker/docker/runconfig/opts" "github.com/docker/go-connections/nat" + "github.com/pkg/errors" "github.com/spf13/pflag" ) @@ -304,7 +305,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err // Validate the input mac address if copts.macAddress != "" { if _, err := opts.ValidateMACAddress(copts.macAddress); err != nil { - return nil, fmt.Errorf("%s is not a valid mac address", copts.macAddress) + return nil, errors.Errorf("%s is not a valid mac address", copts.macAddress) } } if copts.stdin { @@ -320,7 +321,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err swappiness := copts.swappiness if swappiness != -1 && (swappiness < 0 || swappiness > 100) { - return nil, fmt.Errorf("invalid value: %d. Valid memory swappiness range is 0-100", swappiness) + return nil, errors.Errorf("invalid value: %d. Valid memory swappiness range is 0-100", swappiness) } var binds []string @@ -371,7 +372,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err // Merge in exposed ports to the map of published ports for _, e := range copts.expose.GetAll() { if strings.Contains(e, ":") { - return nil, fmt.Errorf("invalid port format for --expose: %s", e) + return nil, errors.Errorf("invalid port format for --expose: %s", e) } //support two formats for expose, original format /[] or /[] proto, port := nat.SplitProtoPort(e) @@ -379,7 +380,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err //if expose a port, the start and end port are the same start, end, err := nat.ParsePortRange(port) if err != nil { - return nil, fmt.Errorf("invalid range format for --expose: %s, error: %s", e, err) + return nil, errors.Errorf("invalid range format for --expose: %s, error: %s", e, err) } for i := start; i <= end; i++ { p, err := nat.NewPort(proto, strconv.FormatUint(i, 10)) @@ -416,22 +417,22 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err ipcMode := container.IpcMode(copts.ipcMode) if !ipcMode.Valid() { - return nil, fmt.Errorf("--ipc: invalid IPC mode") + return nil, errors.Errorf("--ipc: invalid IPC mode") } pidMode := container.PidMode(copts.pidMode) if !pidMode.Valid() { - return nil, fmt.Errorf("--pid: invalid PID mode") + return nil, errors.Errorf("--pid: invalid PID mode") } utsMode := container.UTSMode(copts.utsMode) if !utsMode.Valid() { - return nil, fmt.Errorf("--uts: invalid UTS mode") + return nil, errors.Errorf("--uts: invalid UTS mode") } usernsMode := container.UsernsMode(copts.usernsMode) if !usernsMode.Valid() { - return nil, fmt.Errorf("--userns: invalid USER mode") + return nil, errors.Errorf("--userns: invalid USER mode") } restartPolicy, err := runconfigopts.ParseRestartPolicy(copts.restartPolicy) @@ -462,7 +463,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err copts.healthRetries != 0 if copts.noHealthcheck { if haveHealthSettings { - return nil, fmt.Errorf("--no-healthcheck conflicts with --health-* options") + return nil, errors.Errorf("--no-healthcheck conflicts with --health-* options") } test := strslice.StrSlice{"NONE"} healthConfig = &container.HealthConfig{Test: test} @@ -473,13 +474,13 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err probe = strslice.StrSlice(args) } if copts.healthInterval < 0 { - return nil, fmt.Errorf("--health-interval cannot be negative") + return nil, errors.Errorf("--health-interval cannot be negative") } if copts.healthTimeout < 0 { - return nil, fmt.Errorf("--health-timeout cannot be negative") + return nil, errors.Errorf("--health-timeout cannot be negative") } if copts.healthRetries < 0 { - return nil, fmt.Errorf("--health-retries cannot be negative") + return nil, errors.Errorf("--health-retries cannot be negative") } healthConfig = &container.HealthConfig{ @@ -594,7 +595,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err } if copts.autoRemove && !hostConfig.RestartPolicy.IsNone() { - return nil, fmt.Errorf("Conflicting options: --restart and --rm") + return nil, errors.Errorf("Conflicting options: --restart and --rm") } // only set this value if the user provided the flag, else it should default to nil @@ -656,7 +657,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err func parseLoggingOpts(loggingDriver string, loggingOpts []string) (map[string]string, error) { loggingOptsMap := runconfigopts.ConvertKVStringsToMap(loggingOpts) if loggingDriver == "none" && len(loggingOpts) > 0 { - return map[string]string{}, fmt.Errorf("invalid logging opts for driver %s", loggingDriver) + return map[string]string{}, errors.Errorf("invalid logging opts for driver %s", loggingDriver) } return loggingOptsMap, nil } @@ -669,17 +670,17 @@ func parseSecurityOpts(securityOpts []string) ([]string, error) { if strings.Contains(opt, ":") { con = strings.SplitN(opt, ":", 2) } else { - return securityOpts, fmt.Errorf("Invalid --security-opt: %q", opt) + return securityOpts, errors.Errorf("Invalid --security-opt: %q", opt) } } if con[0] == "seccomp" && con[1] != "unconfined" { f, err := ioutil.ReadFile(con[1]) if err != nil { - return securityOpts, fmt.Errorf("opening seccomp profile (%s) failed: %v", con[1], err) + return securityOpts, errors.Errorf("opening seccomp profile (%s) failed: %v", con[1], err) } b := bytes.NewBuffer(nil) if err := json.Compact(b, f); err != nil { - return securityOpts, fmt.Errorf("compacting json for seccomp profile (%s) failed: %v", con[1], err) + return securityOpts, errors.Errorf("compacting json for seccomp profile (%s) failed: %v", con[1], err) } securityOpts[key] = fmt.Sprintf("seccomp=%s", b.Bytes()) } @@ -696,7 +697,7 @@ func parseStorageOpts(storageOpts []string) (map[string]string, error) { opt := strings.SplitN(option, "=", 2) m[opt[0]] = opt[1] } else { - return nil, fmt.Errorf("invalid storage option") + return nil, errors.Errorf("invalid storage option") } } return m, nil @@ -722,7 +723,7 @@ func parseDevice(device string) (container.DeviceMapping, error) { case 1: src = arr[0] default: - return container.DeviceMapping{}, fmt.Errorf("invalid device specification: %s", device) + return container.DeviceMapping{}, errors.Errorf("invalid device specification: %s", device) } if dst == "" { @@ -745,7 +746,7 @@ func validateDeviceCgroupRule(val string) (string, error) { return val, nil } - return val, fmt.Errorf("invalid device cgroup format '%s'", val) + return val, errors.Errorf("invalid device cgroup format '%s'", val) } // validDeviceMode checks if the mode for device is valid or not. @@ -781,12 +782,12 @@ func validatePath(val string, validator func(string) bool) (string, error) { var mode string if strings.Count(val, ":") > 2 { - return val, fmt.Errorf("bad format for path: %s", val) + return val, errors.Errorf("bad format for path: %s", val) } split := strings.SplitN(val, ":", 3) if split[0] == "" { - return val, fmt.Errorf("bad format for path: %s", val) + return val, errors.Errorf("bad format for path: %s", val) } switch len(split) { case 1: @@ -805,13 +806,13 @@ func validatePath(val string, validator func(string) bool) (string, error) { containerPath = split[1] mode = split[2] if isValid := validator(split[2]); !isValid { - return val, fmt.Errorf("bad mode specified: %s", mode) + return val, errors.Errorf("bad mode specified: %s", mode) } val = fmt.Sprintf("%s:%s:%s", split[0], containerPath, mode) } if !path.IsAbs(containerPath) { - return val, fmt.Errorf("%s is not an absolute path", containerPath) + return val, errors.Errorf("%s is not an absolute path", containerPath) } return val, nil } @@ -885,5 +886,5 @@ func validateAttach(val string) (string, error) { return s, nil } } - return val, fmt.Errorf("valid streams are STDIN, STDOUT and STDERR") + return val, errors.Errorf("valid streams are STDIN, STDOUT and STDERR") } diff --git a/components/cli/command/container/opts_test.go b/components/cli/command/container/opts_test.go index 3c7753cd00..b628c0b625 100644 --- a/components/cli/command/container/opts_test.go +++ b/components/cli/command/container/opts_test.go @@ -16,6 +16,7 @@ import ( "github.com/docker/docker/pkg/testutil/assert" "github.com/docker/docker/runconfig" "github.com/docker/go-connections/nat" + "github.com/pkg/errors" "github.com/spf13/pflag" ) @@ -224,7 +225,7 @@ func compareRandomizedStrings(a, b, c, d string) error { if a == d && b == c { return nil } - return fmt.Errorf("strings don't match") + return errors.Errorf("strings don't match") } // Simple parse with MacAddress validation @@ -751,14 +752,14 @@ func callDecodeContainerConfig(volumes []string, binds []string) (*container.Con w.Config.Volumes[v] = struct{}{} } if b, err = json.Marshal(w); err != nil { - return nil, nil, fmt.Errorf("Error on marshal %s", err.Error()) + return nil, nil, errors.Errorf("Error on marshal %s", err.Error()) } c, h, _, err = runconfig.DecodeContainerConfig(bytes.NewReader(b)) if err != nil { - return nil, nil, fmt.Errorf("Error parsing %s: %v", string(b), err) + return nil, nil, errors.Errorf("Error parsing %s: %v", string(b), err) } if c == nil || h == nil { - return nil, nil, fmt.Errorf("Empty config or hostconfig") + return nil, nil, errors.Errorf("Empty config or hostconfig") } return c, h, err diff --git a/components/cli/command/container/pause.go b/components/cli/command/container/pause.go index 7d42ca571e..742d6d5560 100644 --- a/components/cli/command/container/pause.go +++ b/components/cli/command/container/pause.go @@ -1,8 +1,8 @@ package container import ( - "errors" "fmt" + "github.com/pkg/errors" "strings" "github.com/docker/docker/cli" diff --git a/components/cli/command/container/port.go b/components/cli/command/container/port.go index dd1a6b245f..2793f6bc6b 100644 --- a/components/cli/command/container/port.go +++ b/components/cli/command/container/port.go @@ -7,6 +7,7 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/go-connections/nat" + "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" ) @@ -64,7 +65,7 @@ func runPort(dockerCli *command.DockerCli, opts *portOptions) error { } return nil } - return fmt.Errorf("Error: No public port '%s' published for %s", natPort, opts.container) + return errors.Errorf("Error: No public port '%s' published for %s", natPort, opts.container) } for from, frontends := range c.NetworkSettings.Ports { diff --git a/components/cli/command/container/rename.go b/components/cli/command/container/rename.go index a24711ad3f..07b4852f47 100644 --- a/components/cli/command/container/rename.go +++ b/components/cli/command/container/rename.go @@ -1,12 +1,12 @@ package container import ( - "errors" "fmt" "strings" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" ) @@ -45,7 +45,7 @@ func runRename(dockerCli *command.DockerCli, opts *renameOptions) error { if err := dockerCli.Client().ContainerRename(ctx, oldName, newName); err != nil { fmt.Fprintln(dockerCli.Err(), err) - return fmt.Errorf("Error: failed to rename container named %s", oldName) + return errors.Errorf("Error: failed to rename container named %s", oldName) } return nil } diff --git a/components/cli/command/container/restart.go b/components/cli/command/container/restart.go index 0a3dd9218d..7cfc9c0eab 100644 --- a/components/cli/command/container/restart.go +++ b/components/cli/command/container/restart.go @@ -1,8 +1,8 @@ package container import ( - "errors" "fmt" + "github.com/pkg/errors" "strings" "time" diff --git a/components/cli/command/container/rm.go b/components/cli/command/container/rm.go index c02533d787..7e6fd4588b 100644 --- a/components/cli/command/container/rm.go +++ b/components/cli/command/container/rm.go @@ -1,8 +1,8 @@ package container import ( - "errors" "fmt" + "github.com/pkg/errors" "strings" "github.com/docker/docker/api/types" diff --git a/components/cli/command/container/run.go b/components/cli/command/container/run.go index fe869f7958..4fd05c74bb 100644 --- a/components/cli/command/container/run.go +++ b/components/cli/command/container/run.go @@ -1,8 +1,8 @@ package container import ( - "errors" "fmt" + "github.com/pkg/errors" "io" "net/http/httputil" "os" diff --git a/components/cli/command/container/start.go b/components/cli/command/container/start.go index f5d8ca0bc4..7702cd4a75 100644 --- a/components/cli/command/container/start.go +++ b/components/cli/command/container/start.go @@ -1,7 +1,6 @@ package container import ( - "errors" "fmt" "io" "net/http/httputil" @@ -12,6 +11,7 @@ import ( "github.com/docker/docker/cli/command" "github.com/docker/docker/pkg/promise" "github.com/docker/docker/pkg/signal" + "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" ) @@ -173,7 +173,7 @@ func startContainersWithoutAttachments(ctx context.Context, dockerCli *command.D } if len(failedContainers) > 0 { - return fmt.Errorf("Error: failed to start containers: %s", strings.Join(failedContainers, ", ")) + return errors.Errorf("Error: failed to start containers: %s", strings.Join(failedContainers, ", ")) } return nil } diff --git a/components/cli/command/container/stats.go b/components/cli/command/container/stats.go index 940a039143..9d2d59a5b4 100644 --- a/components/cli/command/container/stats.go +++ b/components/cli/command/container/stats.go @@ -1,8 +1,8 @@ package container import ( - "errors" "fmt" + "github.com/pkg/errors" "io" "strings" "sync" diff --git a/components/cli/command/container/stats_helpers.go b/components/cli/command/container/stats_helpers.go index 3dc939a137..8f7a924f2d 100644 --- a/components/cli/command/container/stats_helpers.go +++ b/components/cli/command/container/stats_helpers.go @@ -2,7 +2,7 @@ package container import ( "encoding/json" - "errors" + "github.com/pkg/errors" "io" "strings" "sync" diff --git a/components/cli/command/container/stop.go b/components/cli/command/container/stop.go index 48fd63a9f0..cba20c77ae 100644 --- a/components/cli/command/container/stop.go +++ b/components/cli/command/container/stop.go @@ -1,8 +1,8 @@ package container import ( - "errors" "fmt" + "github.com/pkg/errors" "strings" "time" diff --git a/components/cli/command/container/unpause.go b/components/cli/command/container/unpause.go index 5f342da0d7..1842991540 100644 --- a/components/cli/command/container/unpause.go +++ b/components/cli/command/container/unpause.go @@ -1,8 +1,8 @@ package container import ( - "errors" "fmt" + "github.com/pkg/errors" "strings" "github.com/docker/docker/cli" diff --git a/components/cli/command/container/update.go b/components/cli/command/container/update.go index b2a44975b3..22b2863974 100644 --- a/components/cli/command/container/update.go +++ b/components/cli/command/container/update.go @@ -1,8 +1,8 @@ package container import ( - "errors" "fmt" + "github.com/pkg/errors" "strings" containertypes "github.com/docker/docker/api/types/container" diff --git a/components/cli/command/container/wait.go b/components/cli/command/container/wait.go index d8dce6ef1a..9b46318f5b 100644 --- a/components/cli/command/container/wait.go +++ b/components/cli/command/container/wait.go @@ -1,8 +1,8 @@ package container import ( - "errors" "fmt" + "github.com/pkg/errors" "strings" "github.com/docker/docker/cli" diff --git a/components/cli/command/formatter/formatter.go b/components/cli/command/formatter/formatter.go index a151e9c283..3f07aee963 100644 --- a/components/cli/command/formatter/formatter.go +++ b/components/cli/command/formatter/formatter.go @@ -2,13 +2,13 @@ package formatter import ( "bytes" - "fmt" "io" "strings" "text/tabwriter" "text/template" "github.com/docker/docker/pkg/templates" + "github.com/pkg/errors" ) // Format keys used to specify certain kinds of output formats @@ -64,7 +64,7 @@ func (c *Context) preFormat() { func (c *Context) parseFormat() (*template.Template, error) { tmpl, err := templates.Parse(c.finalFormat) if err != nil { - return tmpl, fmt.Errorf("Template parsing error: %v\n", err) + return tmpl, errors.Errorf("Template parsing error: %v\n", err) } return tmpl, err } @@ -85,7 +85,7 @@ func (c *Context) postFormat(tmpl *template.Template, subContext subContext) { func (c *Context) contextFormat(tmpl *template.Template, subContext subContext) error { if err := tmpl.Execute(c.buffer, subContext); err != nil { - return fmt.Errorf("Template parsing error: %v\n", err) + return errors.Errorf("Template parsing error: %v\n", err) } if c.Format.IsTable() && c.header != nil { c.header = subContext.FullHeader() diff --git a/components/cli/command/formatter/reflect.go b/components/cli/command/formatter/reflect.go index 9692bbce7d..fd59404d05 100644 --- a/components/cli/command/formatter/reflect.go +++ b/components/cli/command/formatter/reflect.go @@ -2,9 +2,10 @@ package formatter import ( "encoding/json" - "fmt" "reflect" "unicode" + + "github.com/pkg/errors" ) func marshalJSON(x interface{}) ([]byte, error) { @@ -19,14 +20,14 @@ func marshalJSON(x interface{}) ([]byte, error) { func marshalMap(x interface{}) (map[string]interface{}, error) { val := reflect.ValueOf(x) if val.Kind() != reflect.Ptr { - return nil, fmt.Errorf("expected a pointer to a struct, got %v", val.Kind()) + return nil, errors.Errorf("expected a pointer to a struct, got %v", val.Kind()) } if val.IsNil() { - return nil, fmt.Errorf("expected a pointer to a struct, got nil pointer") + return nil, errors.Errorf("expected a pointer to a struct, got nil pointer") } valElem := val.Elem() if valElem.Kind() != reflect.Struct { - return nil, fmt.Errorf("expected a pointer to a struct, got a pointer to %v", valElem.Kind()) + return nil, errors.Errorf("expected a pointer to a struct, got a pointer to %v", valElem.Kind()) } typ := val.Type() m := make(map[string]interface{}) @@ -48,7 +49,7 @@ var unmarshallableNames = map[string]struct{}{"FullHeader": {}} // It returns ("", nil, nil) for valid but non-marshallable parameter. (e.g. "unexportedFunc()") func marshalForMethod(typ reflect.Method, val reflect.Value) (string, interface{}, error) { if val.Kind() != reflect.Func { - return "", nil, fmt.Errorf("expected func, got %v", val.Kind()) + return "", nil, errors.Errorf("expected func, got %v", val.Kind()) } name, numIn, numOut := typ.Name, val.Type().NumIn(), val.Type().NumOut() _, blackListed := unmarshallableNames[name] diff --git a/components/cli/command/formatter/service.go b/components/cli/command/formatter/service.go index 98c760ed7f..4a4bae2cff 100644 --- a/components/cli/command/formatter/service.go +++ b/components/cli/command/formatter/service.go @@ -1,7 +1,6 @@ package formatter import ( - "fmt" "strings" "time" @@ -11,6 +10,7 @@ import ( "github.com/docker/docker/cli/command/inspect" "github.com/docker/docker/pkg/stringid" units "github.com/docker/go-units" + "github.com/pkg/errors" ) const serviceInspectPrettyTemplate Format = ` @@ -147,7 +147,7 @@ func ServiceInspectWrite(ctx Context, refs []string, getRef inspect.GetRefFunc) } service, ok := serviceI.(swarm.Service) if !ok { - return fmt.Errorf("got wrong object to inspect") + return errors.Errorf("got wrong object to inspect") } if err := format(&serviceInspectContext{Service: service}); err != nil { return err diff --git a/components/cli/command/idresolver/idresolver.go b/components/cli/command/idresolver/idresolver.go index ad0d96735d..25c51a27eb 100644 --- a/components/cli/command/idresolver/idresolver.go +++ b/components/cli/command/idresolver/idresolver.go @@ -1,12 +1,11 @@ package idresolver import ( - "fmt" - "golang.org/x/net/context" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/client" + "github.com/pkg/errors" ) // IDResolver provides ID to Name resolution. @@ -46,7 +45,7 @@ func (r *IDResolver) get(ctx context.Context, t interface{}, id string) (string, } return service.Spec.Annotations.Name, nil default: - return "", fmt.Errorf("unsupported type") + return "", errors.Errorf("unsupported type") } } diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go index 040a2c2293..b14b0356ca 100644 --- a/components/cli/command/image/build.go +++ b/components/cli/command/image/build.go @@ -28,6 +28,7 @@ import ( "github.com/docker/docker/pkg/urlutil" runconfigopts "github.com/docker/docker/runconfig/opts" units "github.com/docker/go-units" + "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" ) @@ -166,14 +167,14 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { case urlutil.IsURL(specifiedContext): buildCtx, relDockerfile, err = build.GetContextFromURL(progBuff, specifiedContext, options.dockerfileName) default: - return fmt.Errorf("unable to prepare context: path %q not found", specifiedContext) + return errors.Errorf("unable to prepare context: path %q not found", specifiedContext) } if err != nil { if options.quiet && urlutil.IsURL(specifiedContext) { fmt.Fprintln(dockerCli.Err(), progBuff) } - return fmt.Errorf("unable to prepare context: %s", err) + return errors.Errorf("unable to prepare context: %s", err) } if tempDir != "" { @@ -185,7 +186,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { // And canonicalize dockerfile name to a platform-independent one relDockerfile, err = archive.CanonicalTarNameForPath(relDockerfile) if err != nil { - return fmt.Errorf("cannot canonicalize dockerfile path %s: %v", relDockerfile, err) + return errors.Errorf("cannot canonicalize dockerfile path %s: %v", relDockerfile, err) } f, err := os.Open(filepath.Join(contextDir, ".dockerignore")) @@ -203,7 +204,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { } if err := build.ValidateContextDirectory(contextDir, excludes); err != nil { - return fmt.Errorf("Error checking context: '%s'.", err) + return errors.Errorf("Error checking context: '%s'.", err) } // If .dockerignore mentions .dockerignore or the Dockerfile diff --git a/components/cli/command/image/build/context.go b/components/cli/command/image/build/context.go index 9ea065adf8..85d319e0b7 100644 --- a/components/cli/command/image/build/context.go +++ b/components/cli/command/image/build/context.go @@ -18,6 +18,7 @@ import ( "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/progress" "github.com/docker/docker/pkg/streamformatter" + "github.com/pkg/errors" ) const ( @@ -36,7 +37,7 @@ func ValidateContextDirectory(srcPath string, excludes []string) error { return filepath.Walk(contextRoot, func(filePath string, f os.FileInfo, err error) error { if err != nil { if os.IsPermission(err) { - return fmt.Errorf("can't stat '%s'", filePath) + return errors.Errorf("can't stat '%s'", filePath) } if os.IsNotExist(err) { return nil @@ -65,7 +66,7 @@ func ValidateContextDirectory(srcPath string, excludes []string) error { if !f.IsDir() { currentFile, err := os.Open(filePath) if err != nil && os.IsPermission(err) { - return fmt.Errorf("no permission to read from '%s'", filePath) + return errors.Errorf("no permission to read from '%s'", filePath) } currentFile.Close() } @@ -81,7 +82,7 @@ func GetContextFromReader(r io.ReadCloser, dockerfileName string) (out io.ReadCl magic, err := buf.Peek(archive.HeaderSize) if err != nil && err != io.EOF { - return nil, "", fmt.Errorf("failed to peek context header from STDIN: %v", err) + return nil, "", errors.Errorf("failed to peek context header from STDIN: %v", err) } if archive.IsArchive(magic) { @@ -91,7 +92,7 @@ func GetContextFromReader(r io.ReadCloser, dockerfileName string) (out io.ReadCl // Input should be read as a Dockerfile. tmpDir, err := ioutil.TempDir("", "docker-build-context-") if err != nil { - return nil, "", fmt.Errorf("unable to create temporary context directory: %v", err) + return nil, "", errors.Errorf("unable to create temporary context directory: %v", err) } f, err := os.Create(filepath.Join(tmpDir, DefaultDockerfileName)) @@ -131,10 +132,10 @@ func GetContextFromReader(r io.ReadCloser, dockerfileName string) (out io.ReadCl // success. func GetContextFromGitURL(gitURL, dockerfileName string) (absContextDir, relDockerfile string, err error) { if _, err := exec.LookPath("git"); err != nil { - return "", "", fmt.Errorf("unable to find 'git': %v", err) + return "", "", errors.Errorf("unable to find 'git': %v", err) } if absContextDir, err = gitutils.Clone(gitURL); err != nil { - return "", "", fmt.Errorf("unable to 'git clone' to temporary context directory: %v", err) + return "", "", errors.Errorf("unable to 'git clone' to temporary context directory: %v", err) } return getDockerfileRelPath(absContextDir, dockerfileName) @@ -147,7 +148,7 @@ func GetContextFromGitURL(gitURL, dockerfileName string) (absContextDir, relDock func GetContextFromURL(out io.Writer, remoteURL, dockerfileName string) (io.ReadCloser, string, error) { response, err := httputils.Download(remoteURL) if err != nil { - return nil, "", fmt.Errorf("unable to download remote context %s: %v", remoteURL, err) + return nil, "", errors.Errorf("unable to download remote context %s: %v", remoteURL, err) } progressOutput := streamformatter.NewStreamFormatter().NewProgressOutput(out, true) @@ -167,7 +168,7 @@ func GetContextFromLocalDir(localDir, dockerfileName string) (absContextDir, rel // current directory and not the context directory. if dockerfileName != "" { if dockerfileName, err = filepath.Abs(dockerfileName); err != nil { - return "", "", fmt.Errorf("unable to get absolute path to Dockerfile: %v", err) + return "", "", errors.Errorf("unable to get absolute path to Dockerfile: %v", err) } } @@ -179,7 +180,7 @@ func GetContextFromLocalDir(localDir, dockerfileName string) (absContextDir, rel // the dockerfile in that context directory, and a non-nil error on success. func getDockerfileRelPath(givenContextDir, givenDockerfile string) (absContextDir, relDockerfile string, err error) { if absContextDir, err = filepath.Abs(givenContextDir); err != nil { - return "", "", fmt.Errorf("unable to get absolute context directory of given context directory %q: %v", givenContextDir, err) + return "", "", errors.Errorf("unable to get absolute context directory of given context directory %q: %v", givenContextDir, err) } // The context dir might be a symbolic link, so follow it to the actual @@ -192,17 +193,17 @@ func getDockerfileRelPath(givenContextDir, givenDockerfile string) (absContextDi if !isUNC(absContextDir) { absContextDir, err = filepath.EvalSymlinks(absContextDir) if err != nil { - return "", "", fmt.Errorf("unable to evaluate symlinks in context path: %v", err) + return "", "", errors.Errorf("unable to evaluate symlinks in context path: %v", err) } } stat, err := os.Lstat(absContextDir) if err != nil { - return "", "", fmt.Errorf("unable to stat context directory %q: %v", absContextDir, err) + return "", "", errors.Errorf("unable to stat context directory %q: %v", absContextDir, err) } if !stat.IsDir() { - return "", "", fmt.Errorf("context must be a directory: %s", absContextDir) + return "", "", errors.Errorf("context must be a directory: %s", absContextDir) } absDockerfile := givenDockerfile @@ -236,23 +237,23 @@ func getDockerfileRelPath(givenContextDir, givenDockerfile string) (absContextDi if !isUNC(absDockerfile) { absDockerfile, err = filepath.EvalSymlinks(absDockerfile) if err != nil { - return "", "", fmt.Errorf("unable to evaluate symlinks in Dockerfile path: %v", err) + return "", "", errors.Errorf("unable to evaluate symlinks in Dockerfile path: %v", err) } } if _, err := os.Lstat(absDockerfile); err != nil { if os.IsNotExist(err) { - return "", "", fmt.Errorf("Cannot locate Dockerfile: %q", absDockerfile) + return "", "", errors.Errorf("Cannot locate Dockerfile: %q", absDockerfile) } - return "", "", fmt.Errorf("unable to stat Dockerfile: %v", err) + return "", "", errors.Errorf("unable to stat Dockerfile: %v", err) } if relDockerfile, err = filepath.Rel(absContextDir, absDockerfile); err != nil { - return "", "", fmt.Errorf("unable to get relative Dockerfile path: %v", err) + return "", "", errors.Errorf("unable to get relative Dockerfile path: %v", err) } if strings.HasPrefix(relDockerfile, ".."+string(filepath.Separator)) { - return "", "", fmt.Errorf("The Dockerfile (%s) must be within the build context (%s)", givenDockerfile, givenContextDir) + return "", "", errors.Errorf("The Dockerfile (%s) must be within the build context (%s)", givenDockerfile, givenContextDir) } return absContextDir, relDockerfile, nil diff --git a/components/cli/command/image/load.go b/components/cli/command/image/load.go index 988f5106e2..24346f126b 100644 --- a/components/cli/command/image/load.go +++ b/components/cli/command/image/load.go @@ -1,7 +1,6 @@ package image import ( - "fmt" "io" "golang.org/x/net/context" @@ -10,6 +9,7 @@ import ( "github.com/docker/docker/cli/command" "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/pkg/system" + "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -56,7 +56,7 @@ func runLoad(dockerCli *command.DockerCli, opts loadOptions) error { // To avoid getting stuck, verify that a tar file is given either in // the input flag or through stdin and if not display an error message and exit. if opts.input == "" && dockerCli.In().IsTerminal() { - return fmt.Errorf("requested load from stdin, but stdin is empty") + return errors.Errorf("requested load from stdin, but stdin is empty") } if !dockerCli.Out().IsTerminal() { diff --git a/components/cli/command/image/pull.go b/components/cli/command/image/pull.go index 7152fdc526..2c702e898c 100644 --- a/components/cli/command/image/pull.go +++ b/components/cli/command/image/pull.go @@ -1,8 +1,8 @@ package image import ( - "errors" "fmt" + "github.com/pkg/errors" "strings" "golang.org/x/net/context" diff --git a/components/cli/command/image/remove.go b/components/cli/command/image/remove.go index c79ceba7a8..48e8d2c2ac 100644 --- a/components/cli/command/image/remove.go +++ b/components/cli/command/image/remove.go @@ -9,6 +9,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -71,7 +72,7 @@ func runRemove(dockerCli *command.DockerCli, opts removeOptions, images []string } if len(errs) > 0 { - return fmt.Errorf("%s", strings.Join(errs, "\n")) + return errors.Errorf("%s", strings.Join(errs, "\n")) } return nil } diff --git a/components/cli/command/image/save.go b/components/cli/command/image/save.go index bbe82d2a05..f475f17ff2 100644 --- a/components/cli/command/image/save.go +++ b/components/cli/command/image/save.go @@ -1,7 +1,7 @@ package image import ( - "errors" + "github.com/pkg/errors" "io" "golang.org/x/net/context" diff --git a/components/cli/command/image/trust.go b/components/cli/command/image/trust.go index 8332dd7deb..75bae2eb53 100644 --- a/components/cli/command/image/trust.go +++ b/components/cli/command/image/trust.go @@ -3,7 +3,6 @@ package image import ( "encoding/hex" "encoding/json" - "errors" "fmt" "io" "path" @@ -19,6 +18,7 @@ import ( "github.com/docker/notary/client" "github.com/docker/notary/tuf/data" "github.com/opencontainers/go-digest" + "github.com/pkg/errors" "golang.org/x/net/context" ) @@ -92,7 +92,7 @@ func PushTrustedReference(cli *command.DockerCli, repoInfo *registry.RepositoryI } if cnt > 1 { - return fmt.Errorf("internal error: only one call to handleTarget expected") + return errors.Errorf("internal error: only one call to handleTarget expected") } if target == nil { @@ -195,7 +195,7 @@ func addTargetToAllSignableRoles(repo *client.NotaryRepository, target *client.T } if len(signableRoles) == 0 { - return fmt.Errorf("no valid signing keys for delegation roles") + return errors.Errorf("no valid signing keys for delegation roles") } return repo.AddTarget(target, signableRoles...) @@ -245,7 +245,7 @@ func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry refs = append(refs, t) } if len(refs) == 0 { - return trust.NotaryError(ref.Name(), fmt.Errorf("No trusted tags for %s", ref.Name())) + return trust.NotaryError(ref.Name(), errors.Errorf("No trusted tags for %s", ref.Name())) } } else { t, err := notaryRepo.GetTargetByName(tagged.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole) @@ -255,7 +255,7 @@ func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry // Only get the tag if it's in the top level targets role or the releases delegation role // ignore it if it's in any other delegation roles if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole { - return trust.NotaryError(ref.Name(), fmt.Errorf("No trust data for %s", tagged.Tag())) + return trust.NotaryError(ref.Name(), errors.Errorf("No trust data for %s", tagged.Tag())) } logrus.Debugf("retrieving target for %s role\n", t.Role) @@ -347,7 +347,7 @@ func TrustedReference(ctx context.Context, cli *command.DockerCli, ref reference // Only list tags in the top level targets role or the releases delegation role - ignore // all other delegation roles if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole { - return nil, trust.NotaryError(repoInfo.Name.Name(), fmt.Errorf("No trust data for %s", ref.Tag())) + return nil, trust.NotaryError(repoInfo.Name.Name(), errors.Errorf("No trust data for %s", ref.Tag())) } r, err := convertTarget(t.Target) if err != nil { diff --git a/components/cli/command/in.go b/components/cli/command/in.go index 7204b7ad04..d12af6fd96 100644 --- a/components/cli/command/in.go +++ b/components/cli/command/in.go @@ -1,7 +1,7 @@ package command import ( - "errors" + "github.com/pkg/errors" "io" "os" "runtime" diff --git a/components/cli/command/inspect/inspector.go b/components/cli/command/inspect/inspector.go index a899da065b..13e584ab49 100644 --- a/components/cli/command/inspect/inspector.go +++ b/components/cli/command/inspect/inspector.go @@ -3,7 +3,6 @@ package inspect import ( "bytes" "encoding/json" - "fmt" "io" "strings" "text/template" @@ -11,6 +10,7 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/docker/cli" "github.com/docker/docker/pkg/templates" + "github.com/pkg/errors" ) // Inspector defines an interface to implement to process elements @@ -44,7 +44,7 @@ func NewTemplateInspectorFromString(out io.Writer, tmplStr string) (Inspector, e tmpl, err := templates.Parse(tmplStr) if err != nil { - return nil, fmt.Errorf("Template parsing error: %s", err) + return nil, errors.Errorf("Template parsing error: %s", err) } return NewTemplateInspector(out, tmpl), nil } @@ -94,7 +94,7 @@ func (i *TemplateInspector) Inspect(typedElement interface{}, rawElement []byte) buffer := new(bytes.Buffer) if err := i.tmpl.Execute(buffer, typedElement); err != nil { if rawElement == nil { - return fmt.Errorf("Template parsing error: %v", err) + return errors.Errorf("Template parsing error: %v", err) } return i.tryRawInspectFallback(rawElement) } @@ -112,12 +112,12 @@ func (i *TemplateInspector) tryRawInspectFallback(rawElement []byte) error { dec := json.NewDecoder(rdr) if rawErr := dec.Decode(&raw); rawErr != nil { - return fmt.Errorf("unable to read inspect data: %v", rawErr) + return errors.Errorf("unable to read inspect data: %v", rawErr) } tmplMissingKey := i.tmpl.Option("missingkey=error") if rawErr := tmplMissingKey.Execute(buffer, raw); rawErr != nil { - return fmt.Errorf("Template parsing error: %v", rawErr) + return errors.Errorf("Template parsing error: %v", rawErr) } i.buffer.Write(buffer.Bytes()) diff --git a/components/cli/command/network/create.go b/components/cli/command/network/create.go index 21300d7839..b2916f6a00 100644 --- a/components/cli/command/network/create.go +++ b/components/cli/command/network/create.go @@ -13,6 +13,7 @@ import ( "github.com/docker/docker/cli/command" "github.com/docker/docker/opts" runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -110,7 +111,7 @@ func runCreate(dockerCli *command.DockerCli, opts createOptions) error { // structured ipam data. func consolidateIpam(subnets, ranges, gateways []string, auxaddrs map[string]string) ([]network.IPAMConfig, error) { if len(subnets) < len(ranges) || len(subnets) < len(gateways) { - return nil, fmt.Errorf("every ip-range or gateway must have a corresponding subnet") + return nil, errors.Errorf("every ip-range or gateway must have a corresponding subnet") } iData := map[string]*network.IPAMConfig{} @@ -126,7 +127,7 @@ func consolidateIpam(subnets, ranges, gateways []string, auxaddrs map[string]str return nil, err } if ok1 || ok2 { - return nil, fmt.Errorf("multiple overlapping subnet configuration is not supported") + return nil, errors.Errorf("multiple overlapping subnet configuration is not supported") } } iData[s] = &network.IPAMConfig{Subnet: s, AuxAddress: map[string]string{}} @@ -144,14 +145,14 @@ func consolidateIpam(subnets, ranges, gateways []string, auxaddrs map[string]str continue } if iData[s].IPRange != "" { - return nil, fmt.Errorf("cannot configure multiple ranges (%s, %s) on the same subnet (%s)", r, iData[s].IPRange, s) + return nil, errors.Errorf("cannot configure multiple ranges (%s, %s) on the same subnet (%s)", r, iData[s].IPRange, s) } d := iData[s] d.IPRange = r match = true } if !match { - return nil, fmt.Errorf("no matching subnet for range %s", r) + return nil, errors.Errorf("no matching subnet for range %s", r) } } @@ -167,14 +168,14 @@ func consolidateIpam(subnets, ranges, gateways []string, auxaddrs map[string]str continue } if iData[s].Gateway != "" { - return nil, fmt.Errorf("cannot configure multiple gateways (%s, %s) for the same subnet (%s)", g, iData[s].Gateway, s) + return nil, errors.Errorf("cannot configure multiple gateways (%s, %s) for the same subnet (%s)", g, iData[s].Gateway, s) } d := iData[s] d.Gateway = g match = true } if !match { - return nil, fmt.Errorf("no matching subnet for gateway %s", g) + return nil, errors.Errorf("no matching subnet for gateway %s", g) } } @@ -193,7 +194,7 @@ func consolidateIpam(subnets, ranges, gateways []string, auxaddrs map[string]str match = true } if !match { - return nil, fmt.Errorf("no matching subnet for aux-address %s", aa) + return nil, errors.Errorf("no matching subnet for aux-address %s", aa) } } @@ -211,13 +212,13 @@ func subnetMatches(subnet, data string) (bool, error) { _, s, err := net.ParseCIDR(subnet) if err != nil { - return false, fmt.Errorf("Invalid subnet %s : %v", s, err) + return false, errors.Errorf("Invalid subnet %s : %v", s, err) } if strings.Contains(data, "/") { ip, _, err = net.ParseCIDR(data) if err != nil { - return false, fmt.Errorf("Invalid cidr %s : %v", data, err) + return false, errors.Errorf("Invalid cidr %s : %v", data, err) } } else { ip = net.ParseIP(data) diff --git a/components/cli/command/node/demote_test.go b/components/cli/command/node/demote_test.go index 3ba88f41c8..710455ff56 100644 --- a/components/cli/command/node/demote_test.go +++ b/components/cli/command/node/demote_test.go @@ -2,12 +2,12 @@ package node import ( "bytes" - "fmt" "io/ioutil" "testing" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli/internal/test" + "github.com/pkg/errors" // Import builders to get the builder function as package function . "github.com/docker/docker/cli/internal/test/builders" "github.com/docker/docker/pkg/testutil/assert" @@ -26,14 +26,14 @@ func TestNodeDemoteErrors(t *testing.T) { { args: []string{"nodeID"}, nodeInspectFunc: func() (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, fmt.Errorf("error inspecting the node") + return swarm.Node{}, []byte{}, errors.Errorf("error inspecting the node") }, expectedError: "error inspecting the node", }, { args: []string{"nodeID"}, nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - return fmt.Errorf("error updating the node") + return errors.Errorf("error updating the node") }, expectedError: "error updating the node", }, @@ -60,7 +60,7 @@ func TestNodeDemoteNoChange(t *testing.T) { }, nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { if node.Role != swarm.NodeRoleWorker { - return fmt.Errorf("expected role worker, got %s", node.Role) + return errors.Errorf("expected role worker, got %s", node.Role) } return nil }, @@ -78,7 +78,7 @@ func TestNodeDemoteMultipleNode(t *testing.T) { }, nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { if node.Role != swarm.NodeRoleWorker { - return fmt.Errorf("expected role worker, got %s", node.Role) + return errors.Errorf("expected role worker, got %s", node.Role) } return nil }, diff --git a/components/cli/command/node/inspect_test.go b/components/cli/command/node/inspect_test.go index 91bd41e165..004cc0e82e 100644 --- a/components/cli/command/node/inspect_test.go +++ b/components/cli/command/node/inspect_test.go @@ -9,6 +9,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli/internal/test" + "github.com/pkg/errors" // Import builders to get the builder function as package function . "github.com/docker/docker/cli/internal/test/builders" "github.com/docker/docker/pkg/testutil/assert" @@ -29,24 +30,24 @@ func TestNodeInspectErrors(t *testing.T) { { args: []string{"self"}, infoFunc: func() (types.Info, error) { - return types.Info{}, fmt.Errorf("error asking for node info") + return types.Info{}, errors.Errorf("error asking for node info") }, expectedError: "error asking for node info", }, { args: []string{"nodeID"}, nodeInspectFunc: func() (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, fmt.Errorf("error inspecting the node") + return swarm.Node{}, []byte{}, errors.Errorf("error inspecting the node") }, infoFunc: func() (types.Info, error) { - return types.Info{}, fmt.Errorf("error asking for node info") + return types.Info{}, errors.Errorf("error asking for node info") }, expectedError: "error inspecting the node", }, { args: []string{"self"}, nodeInspectFunc: func() (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, fmt.Errorf("error inspecting the node") + return swarm.Node{}, []byte{}, errors.Errorf("error inspecting the node") }, infoFunc: func() (types.Info, error) { return types.Info{}, nil @@ -59,7 +60,7 @@ func TestNodeInspectErrors(t *testing.T) { "pretty": "true", }, infoFunc: func() (types.Info, error) { - return types.Info{}, fmt.Errorf("error asking for node info") + return types.Info{}, errors.Errorf("error asking for node info") }, expectedError: "error asking for node info", }, diff --git a/components/cli/command/node/list_test.go b/components/cli/command/node/list_test.go index 237c4be9ca..7b657cd73c 100644 --- a/components/cli/command/node/list_test.go +++ b/components/cli/command/node/list_test.go @@ -2,13 +2,13 @@ package node import ( "bytes" - "fmt" "io/ioutil" "testing" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli/internal/test" + "github.com/pkg/errors" // Import builders to get the builder function as package function . "github.com/docker/docker/cli/internal/test/builders" "github.com/docker/docker/pkg/testutil/assert" @@ -22,7 +22,7 @@ func TestNodeListErrorOnAPIFailure(t *testing.T) { }{ { nodeListFunc: func() ([]swarm.Node, error) { - return []swarm.Node{}, fmt.Errorf("error listing nodes") + return []swarm.Node{}, errors.Errorf("error listing nodes") }, expectedError: "error listing nodes", }, @@ -35,7 +35,7 @@ func TestNodeListErrorOnAPIFailure(t *testing.T) { }, nil }, infoFunc: func() (types.Info, error) { - return types.Info{}, fmt.Errorf("error asking for node info") + return types.Info{}, errors.Errorf("error asking for node info") }, expectedError: "error asking for node info", }, diff --git a/components/cli/command/node/promote_test.go b/components/cli/command/node/promote_test.go index ef4666321d..9b646724d1 100644 --- a/components/cli/command/node/promote_test.go +++ b/components/cli/command/node/promote_test.go @@ -2,12 +2,12 @@ package node import ( "bytes" - "fmt" "io/ioutil" "testing" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli/internal/test" + "github.com/pkg/errors" // Import builders to get the builder function as package function . "github.com/docker/docker/cli/internal/test/builders" "github.com/docker/docker/pkg/testutil/assert" @@ -26,14 +26,14 @@ func TestNodePromoteErrors(t *testing.T) { { args: []string{"nodeID"}, nodeInspectFunc: func() (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, fmt.Errorf("error inspecting the node") + return swarm.Node{}, []byte{}, errors.Errorf("error inspecting the node") }, expectedError: "error inspecting the node", }, { args: []string{"nodeID"}, nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - return fmt.Errorf("error updating the node") + return errors.Errorf("error updating the node") }, expectedError: "error updating the node", }, @@ -60,7 +60,7 @@ func TestNodePromoteNoChange(t *testing.T) { }, nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { if node.Role != swarm.NodeRoleManager { - return fmt.Errorf("expected role manager, got %s", node.Role) + return errors.Errorf("expected role manager, got %s", node.Role) } return nil }, @@ -78,7 +78,7 @@ func TestNodePromoteMultipleNode(t *testing.T) { }, nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { if node.Role != swarm.NodeRoleManager { - return fmt.Errorf("expected role manager, got %s", node.Role) + return errors.Errorf("expected role manager, got %s", node.Role) } return nil }, diff --git a/components/cli/command/node/ps.go b/components/cli/command/node/ps.go index cb0f3efdfc..b12f34a3a3 100644 --- a/components/cli/command/node/ps.go +++ b/components/cli/command/node/ps.go @@ -1,7 +1,6 @@ package node import ( - "fmt" "strings" "github.com/docker/docker/api/types" @@ -12,6 +11,7 @@ import ( "github.com/docker/docker/cli/command/idresolver" "github.com/docker/docker/cli/command/task" "github.com/docker/docker/opts" + "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" ) @@ -100,7 +100,7 @@ func runPs(dockerCli command.Cli, opts psOptions) error { } if len(errs) > 0 { - return fmt.Errorf("%s", strings.Join(errs, "\n")) + return errors.Errorf("%s", strings.Join(errs, "\n")) } return nil diff --git a/components/cli/command/node/ps_test.go b/components/cli/command/node/ps_test.go index 1a1022d213..de6ff7d578 100644 --- a/components/cli/command/node/ps_test.go +++ b/components/cli/command/node/ps_test.go @@ -10,6 +10,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli/internal/test" + "github.com/pkg/errors" // Import builders to get the builder function as package function . "github.com/docker/docker/cli/internal/test/builders" "github.com/docker/docker/pkg/testutil/assert" @@ -28,21 +29,21 @@ func TestNodePsErrors(t *testing.T) { }{ { infoFunc: func() (types.Info, error) { - return types.Info{}, fmt.Errorf("error asking for node info") + return types.Info{}, errors.Errorf("error asking for node info") }, expectedError: "error asking for node info", }, { args: []string{"nodeID"}, nodeInspectFunc: func() (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, fmt.Errorf("error inspecting the node") + return swarm.Node{}, []byte{}, errors.Errorf("error inspecting the node") }, expectedError: "error inspecting the node", }, { args: []string{"nodeID"}, taskListFunc: func(options types.TaskListOptions) ([]swarm.Task, error) { - return []swarm.Task{}, fmt.Errorf("error returning the task list") + return []swarm.Task{}, errors.Errorf("error returning the task list") }, expectedError: "error returning the task list", }, diff --git a/components/cli/command/node/remove.go b/components/cli/command/node/remove.go index 0e4963aca4..bd429ee45f 100644 --- a/components/cli/command/node/remove.go +++ b/components/cli/command/node/remove.go @@ -9,6 +9,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -49,7 +50,7 @@ func runRemove(dockerCli command.Cli, args []string, opts removeOptions) error { } if len(errs) > 0 { - return fmt.Errorf("%s", strings.Join(errs, "\n")) + return errors.Errorf("%s", strings.Join(errs, "\n")) } return nil diff --git a/components/cli/command/node/remove_test.go b/components/cli/command/node/remove_test.go index 54930a276c..d7e742aa4c 100644 --- a/components/cli/command/node/remove_test.go +++ b/components/cli/command/node/remove_test.go @@ -2,12 +2,12 @@ package node import ( "bytes" - "fmt" "io/ioutil" "testing" "github.com/docker/docker/cli/internal/test" "github.com/docker/docker/pkg/testutil/assert" + "github.com/pkg/errors" ) func TestNodeRemoveErrors(t *testing.T) { @@ -22,7 +22,7 @@ func TestNodeRemoveErrors(t *testing.T) { { args: []string{"nodeID"}, nodeRemoveFunc: func() error { - return fmt.Errorf("error removing the node") + return errors.Errorf("error removing the node") }, expectedError: "error removing the node", }, diff --git a/components/cli/command/node/update.go b/components/cli/command/node/update.go index aecb88c4ab..82668595a7 100644 --- a/components/cli/command/node/update.go +++ b/components/cli/command/node/update.go @@ -1,7 +1,6 @@ package node import ( - "errors" "fmt" "github.com/docker/docker/api/types/swarm" @@ -9,6 +8,7 @@ import ( "github.com/docker/docker/cli/command" "github.com/docker/docker/opts" runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" "golang.org/x/net/context" @@ -104,7 +104,7 @@ func mergeNodeUpdate(flags *pflag.FlagSet) func(*swarm.Node) error { for _, k := range keys { // if a key doesn't exist, fail the command explicitly if _, exists := spec.Annotations.Labels[k]; !exists { - return fmt.Errorf("key %s doesn't exist in node's labels", k) + return errors.Errorf("key %s doesn't exist in node's labels", k) } delete(spec.Annotations.Labels, k) } diff --git a/components/cli/command/node/update_test.go b/components/cli/command/node/update_test.go index 439ba94436..493a386270 100644 --- a/components/cli/command/node/update_test.go +++ b/components/cli/command/node/update_test.go @@ -2,12 +2,12 @@ package node import ( "bytes" - "fmt" "io/ioutil" "testing" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli/internal/test" + "github.com/pkg/errors" // Import builders to get the builder function as package function . "github.com/docker/docker/cli/internal/test/builders" "github.com/docker/docker/pkg/testutil/assert" @@ -31,14 +31,14 @@ func TestNodeUpdateErrors(t *testing.T) { { args: []string{"nodeID"}, nodeInspectFunc: func() (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, fmt.Errorf("error inspecting the node") + return swarm.Node{}, []byte{}, errors.Errorf("error inspecting the node") }, expectedError: "error inspecting the node", }, { args: []string{"nodeID"}, nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { - return fmt.Errorf("error updating the node") + return errors.Errorf("error updating the node") }, expectedError: "error updating the node", }, @@ -88,7 +88,7 @@ func TestNodeUpdate(t *testing.T) { }, nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { if node.Role != swarm.NodeRoleManager { - return fmt.Errorf("expected role manager, got %s", node.Role) + return errors.Errorf("expected role manager, got %s", node.Role) } return nil }, @@ -103,7 +103,7 @@ func TestNodeUpdate(t *testing.T) { }, nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { if node.Availability != swarm.NodeAvailabilityDrain { - return fmt.Errorf("expected drain availability, got %s", node.Availability) + return errors.Errorf("expected drain availability, got %s", node.Availability) } return nil }, @@ -118,7 +118,7 @@ func TestNodeUpdate(t *testing.T) { }, nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { if _, present := node.Annotations.Labels["lbl"]; !present { - return fmt.Errorf("expected 'lbl' label, got %v", node.Annotations.Labels) + return errors.Errorf("expected 'lbl' label, got %v", node.Annotations.Labels) } return nil }, @@ -133,7 +133,7 @@ func TestNodeUpdate(t *testing.T) { }, nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { if value, present := node.Annotations.Labels["key"]; !present || value != "value" { - return fmt.Errorf("expected 'key' label to be 'value', got %v", node.Annotations.Labels) + return errors.Errorf("expected 'key' label to be 'value', got %v", node.Annotations.Labels) } return nil }, @@ -150,7 +150,7 @@ func TestNodeUpdate(t *testing.T) { }, nodeUpdateFunc: func(nodeID string, version swarm.Version, node swarm.NodeSpec) error { if len(node.Annotations.Labels) > 0 { - return fmt.Errorf("expected no labels, got %v", node.Annotations.Labels) + return errors.Errorf("expected no labels, got %v", node.Annotations.Labels) } return nil }, diff --git a/components/cli/command/plugin/create.go b/components/cli/command/plugin/create.go index e1e6f74ee3..b51f1933db 100644 --- a/components/cli/command/plugin/create.go +++ b/components/cli/command/plugin/create.go @@ -13,6 +13,7 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/pkg/archive" + "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" ) @@ -50,7 +51,7 @@ func validateContextDir(contextDir string) (string, error) { } if !stat.IsDir() { - return "", fmt.Errorf("context must be a directory") + return "", errors.Errorf("context must be a directory") } return absContextDir, nil diff --git a/components/cli/command/plugin/enable.go b/components/cli/command/plugin/enable.go index 77762f4024..b1ca48f8f1 100644 --- a/components/cli/command/plugin/enable.go +++ b/components/cli/command/plugin/enable.go @@ -6,6 +6,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" ) @@ -36,7 +37,7 @@ func newEnableCommand(dockerCli *command.DockerCli) *cobra.Command { func runEnable(dockerCli *command.DockerCli, opts *enableOpts) error { name := opts.name if opts.timeout < 0 { - return fmt.Errorf("negative timeout %d is invalid", opts.timeout) + return errors.Errorf("negative timeout %d is invalid", opts.timeout) } if err := dockerCli.Client().PluginEnable(context.Background(), name, types.PluginEnableOptions{Timeout: opts.timeout}); err != nil { diff --git a/components/cli/command/plugin/install.go b/components/cli/command/plugin/install.go index ed874e17b9..18b3fa3739 100644 --- a/components/cli/command/plugin/install.go +++ b/components/cli/command/plugin/install.go @@ -1,7 +1,6 @@ package plugin import ( - "errors" "fmt" "strings" @@ -12,6 +11,7 @@ import ( "github.com/docker/docker/cli/command/image" "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/registry" + "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" "golang.org/x/net/context" @@ -92,7 +92,7 @@ func buildPullConfig(ctx context.Context, dockerCli *command.DockerCli, opts plu ref = reference.TagNameOnly(ref) nt, ok := ref.(reference.NamedTagged) if !ok { - return types.PluginInstallOptions{}, fmt.Errorf("invalid name: %s", ref.String()) + return types.PluginInstallOptions{}, errors.Errorf("invalid name: %s", ref.String()) } ctx := context.Background() @@ -132,7 +132,7 @@ func runInstall(dockerCli *command.DockerCli, opts pluginOptions) error { return err } if _, ok := aref.(reference.Canonical); ok { - return fmt.Errorf("invalid name: %s", opts.localName) + return errors.Errorf("invalid name: %s", opts.localName) } localName = reference.FamiliarString(reference.TagNameOnly(aref)) } diff --git a/components/cli/command/plugin/push.go b/components/cli/command/plugin/push.go index f3643b7f1b..de4f95cce8 100644 --- a/components/cli/command/plugin/push.go +++ b/components/cli/command/plugin/push.go @@ -1,8 +1,6 @@ package plugin import ( - "fmt" - "golang.org/x/net/context" "github.com/docker/distribution/reference" @@ -11,6 +9,7 @@ import ( "github.com/docker/docker/cli/command/image" "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/registry" + "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -37,7 +36,7 @@ func runPush(dockerCli *command.DockerCli, name string) error { return err } if _, ok := named.(reference.Canonical); ok { - return fmt.Errorf("invalid name: %s", name) + return errors.Errorf("invalid name: %s", name) } named = reference.TagNameOnly(named) diff --git a/components/cli/command/plugin/upgrade.go b/components/cli/command/plugin/upgrade.go index 46efb096f9..cbcbe17ece 100644 --- a/components/cli/command/plugin/upgrade.go +++ b/components/cli/command/plugin/upgrade.go @@ -39,11 +39,11 @@ func runUpgrade(dockerCli *command.DockerCli, opts pluginOptions) error { ctx := context.Background() p, _, err := dockerCli.Client().PluginInspectWithRaw(ctx, opts.localName) if err != nil { - return fmt.Errorf("error reading plugin data: %v", err) + return errors.Errorf("error reading plugin data: %v", err) } if p.Enabled { - return fmt.Errorf("the plugin must be disabled before upgrading") + return errors.Errorf("the plugin must be disabled before upgrading") } opts.localName = p.Name diff --git a/components/cli/command/registry.go b/components/cli/command/registry.go index 411310fa34..e13bba775d 100644 --- a/components/cli/command/registry.go +++ b/components/cli/command/registry.go @@ -17,6 +17,7 @@ import ( registrytypes "github.com/docker/docker/api/types/registry" "github.com/docker/docker/pkg/term" "github.com/docker/docker/registry" + "github.com/pkg/errors" ) // ElectAuthServer returns the default registry to use (by asking the daemon) @@ -95,7 +96,7 @@ func ConfigureAuth(cli *DockerCli, flUser, flPassword, serverAddress string, isD // will hit this if you attempt docker login from mintty where stdin // is a pipe, not a character based console. if flPassword == "" && !cli.In().IsTerminal() { - return authconfig, fmt.Errorf("Error: Cannot perform an interactive login from a non TTY device") + return authconfig, errors.Errorf("Error: Cannot perform an interactive login from a non TTY device") } authconfig.Username = strings.TrimSpace(authconfig.Username) @@ -113,7 +114,7 @@ func ConfigureAuth(cli *DockerCli, flUser, flPassword, serverAddress string, isD } } if flUser == "" { - return authconfig, fmt.Errorf("Error: Non-null Username Required") + return authconfig, errors.Errorf("Error: Non-null Username Required") } if flPassword == "" { oldState, err := term.SaveState(cli.In().FD()) @@ -128,7 +129,7 @@ func ConfigureAuth(cli *DockerCli, flUser, flPassword, serverAddress string, isD term.RestoreTerminal(cli.In().FD(), oldState) if flPassword == "" { - return authconfig, fmt.Errorf("Error: Password Required") + return authconfig, errors.Errorf("Error: Password Required") } } diff --git a/components/cli/command/registry/login.go b/components/cli/command/registry/login.go index f7c7f05da5..343d107dc2 100644 --- a/components/cli/command/registry/login.go +++ b/components/cli/command/registry/login.go @@ -8,6 +8,7 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/registry" + "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -76,7 +77,7 @@ func runLogin(dockerCli *command.DockerCli, opts loginOptions) error { authConfig.IdentityToken = response.IdentityToken } if err := dockerCli.CredentialsStore(serverAddress).Store(authConfig); err != nil { - return fmt.Errorf("Error saving credentials: %v", err) + return errors.Errorf("Error saving credentials: %v", err) } if response.Status != "" { diff --git a/components/cli/command/secret/create.go b/components/cli/command/secret/create.go index a3248e5dfe..11a85a22ca 100644 --- a/components/cli/command/secret/create.go +++ b/components/cli/command/secret/create.go @@ -11,6 +11,7 @@ import ( "github.com/docker/docker/opts" "github.com/docker/docker/pkg/system" runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" ) @@ -58,7 +59,7 @@ func runSecretCreate(dockerCli *command.DockerCli, options createOptions) error secretData, err := ioutil.ReadAll(in) if err != nil { - return fmt.Errorf("Error reading content from %q: %v", options.file, err) + return errors.Errorf("Error reading content from %q: %v", options.file, err) } spec := swarm.SecretSpec{ diff --git a/components/cli/command/secret/remove.go b/components/cli/command/secret/remove.go index 91ca4388f0..9115550d4d 100644 --- a/components/cli/command/secret/remove.go +++ b/components/cli/command/secret/remove.go @@ -6,6 +6,7 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" ) @@ -45,7 +46,7 @@ func runSecretRemove(dockerCli *command.DockerCli, opts removeOptions) error { } if len(errs) > 0 { - return fmt.Errorf("%s", strings.Join(errs, "\n")) + return errors.Errorf("%s", strings.Join(errs, "\n")) } return nil diff --git a/components/cli/command/service/inspect.go b/components/cli/command/service/inspect.go index 7af9b98c3c..8247d45afa 100644 --- a/components/cli/command/service/inspect.go +++ b/components/cli/command/service/inspect.go @@ -1,7 +1,6 @@ package service import ( - "fmt" "strings" "golang.org/x/net/context" @@ -10,6 +9,7 @@ import ( "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/formatter" apiclient "github.com/docker/docker/client" + "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -30,7 +30,7 @@ func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command { opts.refs = args if opts.pretty && len(opts.format) > 0 { - return fmt.Errorf("--format is incompatible with human friendly format") + return errors.Errorf("--format is incompatible with human friendly format") } return runInspect(dockerCli, opts) }, @@ -55,7 +55,7 @@ func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { if err == nil || !apiclient.IsErrServiceNotFound(err) { return service, nil, err } - return nil, nil, fmt.Errorf("Error: no such service: %s", ref) + return nil, nil, errors.Errorf("Error: no such service: %s", ref) } f := opts.format @@ -69,7 +69,7 @@ func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { // check if the user is trying to apply a template to the pretty format, which // is not supported if strings.HasPrefix(f, "pretty") && f != "pretty" { - return fmt.Errorf("Cannot supply extra formatting options to the pretty template") + return errors.Errorf("Cannot supply extra formatting options to the pretty template") } serviceCtx := formatter.Context{ diff --git a/components/cli/command/service/logs.go b/components/cli/command/service/logs.go index 5f5090585e..1bf5723ae0 100644 --- a/components/cli/command/service/logs.go +++ b/components/cli/command/service/logs.go @@ -17,6 +17,7 @@ import ( "github.com/docker/docker/client" "github.com/docker/docker/pkg/stdcopy" "github.com/docker/docker/pkg/stringid" + "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -170,7 +171,7 @@ func (lw *logWriter) Write(buf []byte) (int, error) { parts := bytes.SplitN(buf, []byte(" "), numParts) if len(parts) != numParts { - return 0, fmt.Errorf("invalid context in log message: %v", string(buf)) + return 0, errors.Errorf("invalid context in log message: %v", string(buf)) } logCtx, err := lw.parseContext(string(parts[contextIndex])) @@ -210,24 +211,24 @@ func (lw *logWriter) parseContext(input string) (logContext, error) { for _, component := range components { parts := strings.SplitN(component, "=", 2) if len(parts) != 2 { - return logContext{}, fmt.Errorf("invalid context: %s", input) + return logContext{}, errors.Errorf("invalid context: %s", input) } context[parts[0]] = parts[1] } nodeID, ok := context["com.docker.swarm.node.id"] if !ok { - return logContext{}, fmt.Errorf("missing node id in context: %s", input) + return logContext{}, errors.Errorf("missing node id in context: %s", input) } serviceID, ok := context["com.docker.swarm.service.id"] if !ok { - return logContext{}, fmt.Errorf("missing service id in context: %s", input) + return logContext{}, errors.Errorf("missing service id in context: %s", input) } taskID, ok := context["com.docker.swarm.task.id"] if !ok { - return logContext{}, fmt.Errorf("missing task id in context: %s", input) + return logContext{}, errors.Errorf("missing task id in context: %s", input) } return logContext{ diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index 68981bec37..2afae80c52 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -1,7 +1,6 @@ package service import ( - "errors" "fmt" "strconv" "strings" @@ -11,6 +10,7 @@ import ( "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/opts" runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/pkg/errors" "github.com/spf13/pflag" ) @@ -32,7 +32,7 @@ func (d *PositiveDurationOpt) Set(s string) error { return err } if *d.DurationOpt.value < 0 { - return fmt.Errorf("duration cannot be negative") + return errors.Errorf("duration cannot be negative") } return nil } @@ -140,7 +140,7 @@ func (opts *placementPrefOpts) Set(value string) error { return errors.New(`placement preference must be of the format "="`) } if fields[0] != "spread" { - return fmt.Errorf("unsupported placement preference %s (only spread is supported)", fields[0]) + return errors.Errorf("unsupported placement preference %s (only spread is supported)", fields[0]) } opts.prefs = append(opts.prefs, swarm.PlacementPreference{ @@ -268,7 +268,7 @@ func (opts *healthCheckOptions) toHealthConfig() (*container.HealthConfig, error opts.retries != 0 if opts.noHealthcheck { if haveHealthSettings { - return nil, fmt.Errorf("--%s conflicts with --health-* options", flagNoHealthcheck) + return nil, errors.Errorf("--%s conflicts with --health-* options", flagNoHealthcheck) } healthConfig = &container.HealthConfig{Test: []string{"NONE"}} } else if haveHealthSettings { @@ -372,7 +372,7 @@ func (opts *serviceOptions) ToServiceMode() (swarm.ServiceMode, error) { switch opts.mode { case "global": if opts.replicas.Value() != nil { - return serviceMode, fmt.Errorf("replicas can only be used with replicated mode") + return serviceMode, errors.Errorf("replicas can only be used with replicated mode") } serviceMode.Global = &swarm.GlobalService{} @@ -381,7 +381,7 @@ func (opts *serviceOptions) ToServiceMode() (swarm.ServiceMode, error) { Replicas: opts.replicas.Value(), } default: - return serviceMode, fmt.Errorf("Unknown mode: %s, only replicated and global supported", opts.mode) + return serviceMode, errors.Errorf("Unknown mode: %s, only replicated and global supported", opts.mode) } return serviceMode, nil } diff --git a/components/cli/command/service/parse.go b/components/cli/command/service/parse.go index baf5e24547..f86bebe87c 100644 --- a/components/cli/command/service/parse.go +++ b/components/cli/command/service/parse.go @@ -1,12 +1,11 @@ package service import ( - "fmt" - "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" swarmtypes "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/client" + "github.com/pkg/errors" "golang.org/x/net/context" ) @@ -18,7 +17,7 @@ func ParseSecrets(client client.SecretAPIClient, requestedSecrets []*swarmtypes. for _, secret := range requestedSecrets { if _, exists := secretRefs[secret.File.Name]; exists { - return nil, fmt.Errorf("duplicate secret target for %s not allowed", secret.SecretName) + return nil, errors.Errorf("duplicate secret target for %s not allowed", secret.SecretName) } secretRef := new(swarmtypes.SecretReference) *secretRef = *secret @@ -47,7 +46,7 @@ func ParseSecrets(client client.SecretAPIClient, requestedSecrets []*swarmtypes. for _, ref := range secretRefs { id, ok := foundSecrets[ref.SecretName] if !ok { - return nil, fmt.Errorf("secret not found: %s", ref.SecretName) + return nil, errors.Errorf("secret not found: %s", ref.SecretName) } // set the id for the ref to properly assign in swarm diff --git a/components/cli/command/service/ps.go b/components/cli/command/service/ps.go index c4ff1b9e3f..3a53a545d0 100644 --- a/components/cli/command/service/ps.go +++ b/components/cli/command/service/ps.go @@ -1,7 +1,6 @@ package service import ( - "fmt" "strings" "golang.org/x/net/context" @@ -15,6 +14,7 @@ import ( "github.com/docker/docker/cli/command/node" "github.com/docker/docker/cli/command/task" "github.com/docker/docker/opts" + "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -89,7 +89,7 @@ func runPS(dockerCli *command.DockerCli, opts psOptions) error { } // If nothing has been found, return immediately. if serviceCount == 0 { - return fmt.Errorf("no such services: %s", service) + return errors.Errorf("no such services: %s", service) } } diff --git a/components/cli/command/service/remove.go b/components/cli/command/service/remove.go index c3fbbabbca..a7b0107089 100644 --- a/components/cli/command/service/remove.go +++ b/components/cli/command/service/remove.go @@ -6,6 +6,7 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" ) @@ -41,7 +42,7 @@ func runRemove(dockerCli *command.DockerCli, sids []string) error { fmt.Fprintf(dockerCli.Out(), "%s\n", sid) } if len(errs) > 0 { - return fmt.Errorf(strings.Join(errs, "\n")) + return errors.Errorf(strings.Join(errs, "\n")) } return nil } diff --git a/components/cli/command/service/scale.go b/components/cli/command/service/scale.go index cf89e90273..ed76c862fe 100644 --- a/components/cli/command/service/scale.go +++ b/components/cli/command/service/scale.go @@ -10,6 +10,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -30,7 +31,7 @@ func scaleArgs(cmd *cobra.Command, args []string) error { } for _, arg := range args { if parts := strings.SplitN(arg, "=", 2); len(parts) != 2 { - return fmt.Errorf( + return errors.Errorf( "Invalid scale specifier '%s'.\nSee '%s --help'.\n\nUsage: %s\n\n%s", arg, cmd.CommandPath(), @@ -43,7 +44,7 @@ func scaleArgs(cmd *cobra.Command, args []string) error { } func runScale(dockerCli *command.DockerCli, args []string) error { - var errors []string + var errs []string for _, arg := range args { parts := strings.SplitN(arg, "=", 2) serviceID, scaleStr := parts[0], parts[1] @@ -51,19 +52,19 @@ func runScale(dockerCli *command.DockerCli, args []string) error { // validate input arg scale number scale, err := strconv.ParseUint(scaleStr, 10, 64) if err != nil { - errors = append(errors, fmt.Sprintf("%s: invalid replicas value %s: %v", serviceID, scaleStr, err)) + errs = append(errs, fmt.Sprintf("%s: invalid replicas value %s: %v", serviceID, scaleStr, err)) continue } if err := runServiceScale(dockerCli, serviceID, scale); err != nil { - errors = append(errors, fmt.Sprintf("%s: %v", serviceID, err)) + errs = append(errs, fmt.Sprintf("%s: %v", serviceID, err)) } } - if len(errors) == 0 { + if len(errs) == 0 { return nil } - return fmt.Errorf(strings.Join(errors, "\n")) + return errors.Errorf(strings.Join(errs, "\n")) } func runServiceScale(dockerCli *command.DockerCli, serviceID string, scale uint64) error { @@ -77,7 +78,7 @@ func runServiceScale(dockerCli *command.DockerCli, serviceID string, scale uint6 serviceMode := &service.Spec.Mode if serviceMode.Replicated == nil { - return fmt.Errorf("scale can only be used with replicated mode") + return errors.Errorf("scale can only be used with replicated mode") } serviceMode.Replicated.Replicas = &scale diff --git a/components/cli/command/service/trust.go b/components/cli/command/service/trust.go index 3fd80ae879..eba52a9dd1 100644 --- a/components/cli/command/service/trust.go +++ b/components/cli/command/service/trust.go @@ -2,7 +2,6 @@ package service import ( "encoding/hex" - "fmt" "github.com/Sirupsen/logrus" "github.com/docker/distribution/reference" @@ -72,7 +71,7 @@ func trustedResolveDigest(ctx context.Context, cli *command.DockerCli, ref refer // Only get the tag if it's in the top level targets role or the releases delegation role // ignore it if it's in any other delegation roles if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole { - return nil, trust.NotaryError(repoInfo.Name.Name(), fmt.Errorf("No trust data for %s", reference.FamiliarString(ref))) + return nil, trust.NotaryError(repoInfo.Name.Name(), errors.Errorf("No trust data for %s", reference.FamiliarString(ref))) } logrus.Debugf("retrieving target for %s role\n", t.Role) diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index 7c0ef2a810..6470d25989 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -1,7 +1,6 @@ package service import ( - "errors" "fmt" "sort" "strings" @@ -19,6 +18,7 @@ import ( runconfigopts "github.com/docker/docker/runconfig/opts" "github.com/docker/go-connections/nat" shlex "github.com/flynn-archive/go-shlex" + "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" "golang.org/x/net/context" @@ -136,7 +136,7 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID str clientSideRollback = true spec = service.PreviousSpec if spec == nil { - return fmt.Errorf("service does not have a previous specification to roll back to") + return errors.Errorf("service does not have a previous specification to roll back to") } } else { serverSideRollback = true @@ -621,7 +621,7 @@ func updateMounts(flags *pflag.FlagSet, mounts *[]mounttypes.Mount) error { values := flags.Lookup(flagMountAdd).Value.(*opts.MountOpt).Value() for _, mount := range values { if _, ok := mountsByTarget[mount.Target]; ok { - return fmt.Errorf("duplicate mount target") + return errors.Errorf("duplicate mount target") } mountsByTarget[mount.Target] = mount } @@ -819,7 +819,7 @@ func updateReplicas(flags *pflag.FlagSet, serviceMode *swarm.ServiceMode) error } if serviceMode == nil || serviceMode.Replicated == nil { - return fmt.Errorf("replicas can only be used with replicated mode") + return errors.Errorf("replicas can only be used with replicated mode") } serviceMode.Replicated.Replicas = flags.Lookup(flagReplicas).Value.(*Uint64Opt).Value() return nil @@ -908,7 +908,7 @@ func updateHealthcheck(flags *pflag.FlagSet, containerSpec *swarm.ContainerSpec) } return nil } - return fmt.Errorf("--%s conflicts with --health-* options", flagNoHealthcheck) + return errors.Errorf("--%s conflicts with --health-* options", flagNoHealthcheck) } if len(containerSpec.Healthcheck.Test) > 0 && containerSpec.Healthcheck.Test[0] == "NONE" { containerSpec.Healthcheck.Test = nil diff --git a/components/cli/command/stack/deploy.go b/components/cli/command/stack/deploy.go index 46af5f63b1..6789171702 100644 --- a/components/cli/command/stack/deploy.go +++ b/components/cli/command/stack/deploy.go @@ -52,9 +52,9 @@ func runDeploy(dockerCli *command.DockerCli, opts deployOptions) error { switch { case opts.bundlefile == "" && opts.composefile == "": - return fmt.Errorf("Please specify either a bundle file (with --bundle-file) or a Compose file (with --compose-file).") + return errors.Errorf("Please specify either a bundle file (with --bundle-file) or a Compose file (with --compose-file).") case opts.bundlefile != "" && opts.composefile != "": - return fmt.Errorf("You cannot specify both a bundle file and a Compose file.") + return errors.Errorf("You cannot specify both a bundle file and a Compose file.") case opts.bundlefile != "": return deployBundle(ctx, dockerCli, opts) default: diff --git a/components/cli/command/stack/deploy_composefile.go b/components/cli/command/stack/deploy_composefile.go index fde1beaa26..10963d1844 100644 --- a/components/cli/command/stack/deploy_composefile.go +++ b/components/cli/command/stack/deploy_composefile.go @@ -28,7 +28,7 @@ func deployCompose(ctx context.Context, dockerCli *command.DockerCli, opts deplo config, err := loader.Load(configDetails) if err != nil { if fpe, ok := err.(*loader.ForbiddenPropertiesError); ok { - return fmt.Errorf("Compose file contains unsupported options:\n\n%s\n", + return errors.Errorf("Compose file contains unsupported options:\n\n%s\n", propertyWarnings(fpe.Properties)) } @@ -168,12 +168,12 @@ func validateExternalNetworks( network, err := client.NetworkInspect(ctx, networkName, false) if err != nil { if dockerclient.IsErrNetworkNotFound(err) { - return fmt.Errorf("network %q is declared as external, but could not be found. You need to create the network before the stack is deployed (with overlay driver)", networkName) + return errors.Errorf("network %q is declared as external, but could not be found. You need to create the network before the stack is deployed (with overlay driver)", networkName) } return err } if network.Scope != "swarm" { - return fmt.Errorf("network %q is declared as external, but it is not in the right scope: %q instead of %q", networkName, network.Scope, "swarm") + return errors.Errorf("network %q is declared as external, but it is not in the right scope: %q instead of %q", networkName, network.Scope, "swarm") } } diff --git a/components/cli/command/stack/list.go b/components/cli/command/stack/list.go index 3d81242b7a..f27d5009ed 100644 --- a/components/cli/command/stack/list.go +++ b/components/cli/command/stack/list.go @@ -12,6 +12,7 @@ import ( "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/compose/convert" "github.com/docker/docker/client" + "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" ) @@ -100,7 +101,7 @@ func getStacks( labels := service.Spec.Labels name, ok := labels[convert.LabelNamespace] if !ok { - return nil, fmt.Errorf("cannot get label %s for service %s", + return nil, errors.Errorf("cannot get label %s for service %s", convert.LabelNamespace, service.ID) } ztack, ok := m[name] diff --git a/components/cli/command/stack/opts.go b/components/cli/command/stack/opts.go index 996ff68f23..0d7214e962 100644 --- a/components/cli/command/stack/opts.go +++ b/components/cli/command/stack/opts.go @@ -6,6 +6,7 @@ import ( "os" "github.com/docker/docker/cli/command/bundlefile" + "github.com/pkg/errors" "github.com/spf13/pflag" ) @@ -30,7 +31,7 @@ func loadBundlefile(stderr io.Writer, namespace string, path string) (*bundlefil path = defaultPath } if _, err := os.Stat(path); err != nil { - return nil, fmt.Errorf( + return nil, errors.Errorf( "Bundle %s not found. Specify the path with --file", path) } @@ -44,7 +45,7 @@ func loadBundlefile(stderr io.Writer, namespace string, path string) (*bundlefil bundle, err := bundlefile.LoadFile(reader) if err != nil { - return nil, fmt.Errorf("Error reading %s: %v\n", path, err) + return nil, errors.Errorf("Error reading %s: %v\n", path, err) } return bundle, err } diff --git a/components/cli/command/stack/remove.go b/components/cli/command/stack/remove.go index d466caf2b4..e976eccdaa 100644 --- a/components/cli/command/stack/remove.go +++ b/components/cli/command/stack/remove.go @@ -7,6 +7,7 @@ import ( "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" ) @@ -61,7 +62,7 @@ func runRemove(dockerCli *command.DockerCli, opts removeOptions) error { hasError = removeNetworks(ctx, dockerCli, networks) || hasError if hasError { - return fmt.Errorf("Failed to remove some resources") + return errors.Errorf("Failed to remove some resources") } return nil } diff --git a/components/cli/command/swarm/init.go b/components/cli/command/swarm/init.go index 57dc873804..37d96de113 100644 --- a/components/cli/command/swarm/init.go +++ b/components/cli/command/swarm/init.go @@ -64,7 +64,7 @@ func runInit(dockerCli command.Cli, flags *pflag.FlagSet, opts initOptions) erro case swarm.NodeAvailabilityActive, swarm.NodeAvailabilityPause, swarm.NodeAvailabilityDrain: req.Availability = availability default: - return fmt.Errorf("invalid availability %q, only active, pause and drain are supported", opts.availability) + return errors.Errorf("invalid availability %q, only active, pause and drain are supported", opts.availability) } } diff --git a/components/cli/command/swarm/init_test.go b/components/cli/command/swarm/init_test.go index 4f56de357f..c21433bdb9 100644 --- a/components/cli/command/swarm/init_test.go +++ b/components/cli/command/swarm/init_test.go @@ -11,6 +11,7 @@ import ( "github.com/docker/docker/cli/internal/test" "github.com/docker/docker/pkg/testutil/assert" "github.com/docker/docker/pkg/testutil/golden" + "github.com/pkg/errors" ) func TestSwarmInitErrorOnAPIFailure(t *testing.T) { @@ -26,28 +27,28 @@ func TestSwarmInitErrorOnAPIFailure(t *testing.T) { { name: "init-failed", swarmInitFunc: func() (string, error) { - return "", fmt.Errorf("error initializing the swarm") + return "", errors.Errorf("error initializing the swarm") }, expectedError: "error initializing the swarm", }, { name: "init-failed-with-ip-choice", swarmInitFunc: func() (string, error) { - return "", fmt.Errorf("could not choose an IP address to advertise") + return "", errors.Errorf("could not choose an IP address to advertise") }, expectedError: "could not choose an IP address to advertise - specify one with --advertise-addr", }, { name: "swarm-inspect-after-init-failed", swarmInspectFunc: func() (swarm.Swarm, error) { - return swarm.Swarm{}, fmt.Errorf("error inspecting the swarm") + return swarm.Swarm{}, errors.Errorf("error inspecting the swarm") }, expectedError: "error inspecting the swarm", }, { name: "node-inspect-after-init-failed", nodeInspectFunc: func() (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, fmt.Errorf("error inspecting the node") + return swarm.Node{}, []byte{}, errors.Errorf("error inspecting the node") }, expectedError: "error inspecting the node", }, @@ -57,7 +58,7 @@ func TestSwarmInitErrorOnAPIFailure(t *testing.T) { flagAutolock: "true", }, swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { - return types.SwarmUnlockKeyResponse{}, fmt.Errorf("error getting swarm unlock key") + return types.SwarmUnlockKeyResponse{}, errors.Errorf("error getting swarm unlock key") }, expectedError: "could not fetch unlock key: error getting swarm unlock key", }, diff --git a/components/cli/command/swarm/join.go b/components/cli/command/swarm/join.go index 3022f6e89a..873eaaefaa 100644 --- a/components/cli/command/swarm/join.go +++ b/components/cli/command/swarm/join.go @@ -9,6 +9,7 @@ import ( "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" ) @@ -61,7 +62,7 @@ func runJoin(dockerCli command.Cli, flags *pflag.FlagSet, opts joinOptions) erro case swarm.NodeAvailabilityActive, swarm.NodeAvailabilityPause, swarm.NodeAvailabilityDrain: req.Availability = availability default: - return fmt.Errorf("invalid availability %q, only active, pause and drain are supported", opts.availability) + return errors.Errorf("invalid availability %q, only active, pause and drain are supported", opts.availability) } } diff --git a/components/cli/command/swarm/join_test.go b/components/cli/command/swarm/join_test.go index 66dd6d66b6..6d92f0c4fa 100644 --- a/components/cli/command/swarm/join_test.go +++ b/components/cli/command/swarm/join_test.go @@ -2,7 +2,6 @@ package swarm import ( "bytes" - "fmt" "io/ioutil" "strings" "testing" @@ -11,6 +10,7 @@ import ( "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli/internal/test" "github.com/docker/docker/pkg/testutil/assert" + "github.com/pkg/errors" ) func TestSwarmJoinErrors(t *testing.T) { @@ -34,7 +34,7 @@ func TestSwarmJoinErrors(t *testing.T) { name: "join-failed", args: []string{"remote"}, swarmJoinFunc: func() error { - return fmt.Errorf("error joining the swarm") + return errors.Errorf("error joining the swarm") }, expectedError: "error joining the swarm", }, @@ -42,7 +42,7 @@ func TestSwarmJoinErrors(t *testing.T) { name: "join-failed-on-init", args: []string{"remote"}, infoFunc: func() (types.Info, error) { - return types.Info{}, fmt.Errorf("error asking for node info") + return types.Info{}, errors.Errorf("error asking for node info") }, expectedError: "error asking for node info", }, diff --git a/components/cli/command/swarm/join_token.go b/components/cli/command/swarm/join_token.go index 5c84c7a310..006ea07c3f 100644 --- a/components/cli/command/swarm/join_token.go +++ b/components/cli/command/swarm/join_token.go @@ -1,8 +1,8 @@ package swarm import ( - "errors" "fmt" + "github.com/pkg/errors" "github.com/spf13/cobra" diff --git a/components/cli/command/swarm/join_token_test.go b/components/cli/command/swarm/join_token_test.go index 6244016419..9b10369ad0 100644 --- a/components/cli/command/swarm/join_token_test.go +++ b/components/cli/command/swarm/join_token_test.go @@ -9,6 +9,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli/internal/test" + "github.com/pkg/errors" // Import builders to get the builder function as package function . "github.com/docker/docker/cli/internal/test/builders" "github.com/docker/docker/pkg/testutil/assert" @@ -44,7 +45,7 @@ func TestSwarmJoinTokenErrors(t *testing.T) { name: "swarm-inspect-failed", args: []string{"worker"}, swarmInspectFunc: func() (swarm.Swarm, error) { - return swarm.Swarm{}, fmt.Errorf("error inspecting the swarm") + return swarm.Swarm{}, errors.Errorf("error inspecting the swarm") }, expectedError: "error inspecting the swarm", }, @@ -55,7 +56,7 @@ func TestSwarmJoinTokenErrors(t *testing.T) { flagRotate: "true", }, swarmInspectFunc: func() (swarm.Swarm, error) { - return swarm.Swarm{}, fmt.Errorf("error inspecting the swarm") + return swarm.Swarm{}, errors.Errorf("error inspecting the swarm") }, expectedError: "error inspecting the swarm", }, @@ -66,7 +67,7 @@ func TestSwarmJoinTokenErrors(t *testing.T) { flagRotate: "true", }, swarmUpdateFunc: func(swarm swarm.Spec, flags swarm.UpdateFlags) error { - return fmt.Errorf("error updating the swarm") + return errors.Errorf("error updating the swarm") }, expectedError: "error updating the swarm", }, @@ -74,7 +75,7 @@ func TestSwarmJoinTokenErrors(t *testing.T) { name: "node-inspect-failed", args: []string{"worker"}, nodeInspectFunc: func() (swarm.Node, []byte, error) { - return swarm.Node{}, []byte{}, fmt.Errorf("error inspecting node") + return swarm.Node{}, []byte{}, errors.Errorf("error inspecting node") }, expectedError: "error inspecting node", }, @@ -82,7 +83,7 @@ func TestSwarmJoinTokenErrors(t *testing.T) { name: "info-failed", args: []string{"worker"}, infoFunc: func() (types.Info, error) { - return types.Info{}, fmt.Errorf("error asking for node info") + return types.Info{}, errors.Errorf("error asking for node info") }, expectedError: "error asking for node info", }, diff --git a/components/cli/command/swarm/leave_test.go b/components/cli/command/swarm/leave_test.go index 09b41b2511..93a58887a7 100644 --- a/components/cli/command/swarm/leave_test.go +++ b/components/cli/command/swarm/leave_test.go @@ -2,13 +2,13 @@ package swarm import ( "bytes" - "fmt" "io/ioutil" "strings" "testing" "github.com/docker/docker/cli/internal/test" "github.com/docker/docker/pkg/testutil/assert" + "github.com/pkg/errors" ) func TestSwarmLeaveErrors(t *testing.T) { @@ -26,7 +26,7 @@ func TestSwarmLeaveErrors(t *testing.T) { { name: "leave-failed", swarmLeaveFunc: func() error { - return fmt.Errorf("error leaving the swarm") + return errors.Errorf("error leaving the swarm") }, expectedError: "error leaving the swarm", }, diff --git a/components/cli/command/swarm/opts.go b/components/cli/command/swarm/opts.go index b32cc92106..6eddddccae 100644 --- a/components/cli/command/swarm/opts.go +++ b/components/cli/command/swarm/opts.go @@ -2,13 +2,13 @@ package swarm import ( "encoding/csv" - "errors" "fmt" "strings" "time" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/opts" + "github.com/pkg/errors" "github.com/spf13/pflag" ) @@ -139,7 +139,7 @@ func parseExternalCA(caSpec string) (*swarm.ExternalCA, error) { parts := strings.SplitN(field, "=", 2) if len(parts) != 2 { - return nil, fmt.Errorf("invalid field '%s' must be a key=value pair", field) + return nil, errors.Errorf("invalid field '%s' must be a key=value pair", field) } key, value := parts[0], parts[1] @@ -150,7 +150,7 @@ func parseExternalCA(caSpec string) (*swarm.ExternalCA, error) { if strings.ToLower(value) == string(swarm.ExternalCAProtocolCFSSL) { externalCA.Protocol = swarm.ExternalCAProtocolCFSSL } else { - return nil, fmt.Errorf("unrecognized external CA protocol %s", value) + return nil, errors.Errorf("unrecognized external CA protocol %s", value) } case "url": hasURL = true diff --git a/components/cli/command/swarm/unlock.go b/components/cli/command/swarm/unlock.go index 45dd6e79e3..bb3068f1ef 100644 --- a/components/cli/command/swarm/unlock.go +++ b/components/cli/command/swarm/unlock.go @@ -2,8 +2,8 @@ package swarm import ( "bufio" - "errors" "fmt" + "github.com/pkg/errors" "io" "strings" diff --git a/components/cli/command/swarm/unlock_key_test.go b/components/cli/command/swarm/unlock_key_test.go index 17a07d3fb1..7b644f70e9 100644 --- a/components/cli/command/swarm/unlock_key_test.go +++ b/components/cli/command/swarm/unlock_key_test.go @@ -9,6 +9,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli/internal/test" + "github.com/pkg/errors" // Import builders to get the builder function as package function . "github.com/docker/docker/cli/internal/test/builders" "github.com/docker/docker/pkg/testutil/assert" @@ -36,7 +37,7 @@ func TestSwarmUnlockKeyErrors(t *testing.T) { flagRotate: "true", }, swarmInspectFunc: func() (swarm.Swarm, error) { - return swarm.Swarm{}, fmt.Errorf("error inspecting the swarm") + return swarm.Swarm{}, errors.Errorf("error inspecting the swarm") }, expectedError: "error inspecting the swarm", }, @@ -59,14 +60,14 @@ func TestSwarmUnlockKeyErrors(t *testing.T) { return *Swarm(Autolock()), nil }, swarmUpdateFunc: func(swarm swarm.Spec, flags swarm.UpdateFlags) error { - return fmt.Errorf("error updating the swarm") + return errors.Errorf("error updating the swarm") }, expectedError: "error updating the swarm", }, { name: "swarm-get-unlock-key-failed", swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { - return types.SwarmUnlockKeyResponse{}, fmt.Errorf("error getting unlock key") + return types.SwarmUnlockKeyResponse{}, errors.Errorf("error getting unlock key") }, expectedError: "error getting unlock key", }, diff --git a/components/cli/command/swarm/unlock_test.go b/components/cli/command/swarm/unlock_test.go index abf858a289..620fecafed 100644 --- a/components/cli/command/swarm/unlock_test.go +++ b/components/cli/command/swarm/unlock_test.go @@ -2,7 +2,6 @@ package swarm import ( "bytes" - "fmt" "io/ioutil" "strings" "testing" @@ -11,6 +10,7 @@ import ( "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli/internal/test" "github.com/docker/docker/pkg/testutil/assert" + "github.com/pkg/errors" ) func TestSwarmUnlockErrors(t *testing.T) { @@ -59,7 +59,7 @@ func TestSwarmUnlockErrors(t *testing.T) { }, nil }, swarmUnlockFunc: func(req swarm.UnlockRequest) error { - return fmt.Errorf("error unlocking the swarm") + return errors.Errorf("error unlocking the swarm") }, expectedError: "error unlocking the swarm", }, @@ -90,7 +90,7 @@ func TestSwarmUnlock(t *testing.T) { }, swarmUnlockFunc: func(req swarm.UnlockRequest) error { if req.UnlockKey != input { - return fmt.Errorf("Invalid unlock key") + return errors.Errorf("Invalid unlock key") } return nil }, diff --git a/components/cli/command/swarm/update_test.go b/components/cli/command/swarm/update_test.go index c8a2860a00..0450c02979 100644 --- a/components/cli/command/swarm/update_test.go +++ b/components/cli/command/swarm/update_test.go @@ -10,6 +10,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli/internal/test" + "github.com/pkg/errors" // Import builders to get the builder function as package function . "github.com/docker/docker/cli/internal/test/builders" "github.com/docker/docker/pkg/testutil/assert" @@ -37,7 +38,7 @@ func TestSwarmUpdateErrors(t *testing.T) { flagTaskHistoryLimit: "10", }, swarmInspectFunc: func() (swarm.Swarm, error) { - return swarm.Swarm{}, fmt.Errorf("error inspecting the swarm") + return swarm.Swarm{}, errors.Errorf("error inspecting the swarm") }, expectedError: "error inspecting the swarm", }, @@ -47,7 +48,7 @@ func TestSwarmUpdateErrors(t *testing.T) { flagTaskHistoryLimit: "10", }, swarmUpdateFunc: func(swarm swarm.Spec, flags swarm.UpdateFlags) error { - return fmt.Errorf("error updating the swarm") + return errors.Errorf("error updating the swarm") }, expectedError: "error updating the swarm", }, @@ -60,7 +61,7 @@ func TestSwarmUpdateErrors(t *testing.T) { return *Swarm(), nil }, swarmGetUnlockKeyFunc: func() (types.SwarmUnlockKeyResponse, error) { - return types.SwarmUnlockKeyResponse{}, fmt.Errorf("error getting unlock key") + return types.SwarmUnlockKeyResponse{}, errors.Errorf("error getting unlock key") }, expectedError: "error getting unlock key", }, @@ -108,33 +109,33 @@ func TestSwarmUpdate(t *testing.T) { }, swarmUpdateFunc: func(swarm swarm.Spec, flags swarm.UpdateFlags) error { if *swarm.Orchestration.TaskHistoryRetentionLimit != 10 { - return fmt.Errorf("historyLimit not correctly set") + return errors.Errorf("historyLimit not correctly set") } heartbeatDuration, err := time.ParseDuration("10s") if err != nil { return err } if swarm.Dispatcher.HeartbeatPeriod != heartbeatDuration { - return fmt.Errorf("heartbeatPeriodLimit not correctly set") + return errors.Errorf("heartbeatPeriodLimit not correctly set") } certExpiryDuration, err := time.ParseDuration("20s") if err != nil { return err } if swarm.CAConfig.NodeCertExpiry != certExpiryDuration { - return fmt.Errorf("certExpiry not correctly set") + return errors.Errorf("certExpiry not correctly set") } if len(swarm.CAConfig.ExternalCAs) != 1 { - return fmt.Errorf("externalCA not correctly set") + return errors.Errorf("externalCA not correctly set") } if *swarm.Raft.KeepOldSnapshots != 10 { - return fmt.Errorf("keepOldSnapshots not correctly set") + return errors.Errorf("keepOldSnapshots not correctly set") } if swarm.Raft.SnapshotInterval != 100 { - return fmt.Errorf("snapshotInterval not correctly set") + return errors.Errorf("snapshotInterval not correctly set") } if !swarm.EncryptionConfig.AutoLockManagers { - return fmt.Errorf("autolock not correctly set") + return errors.Errorf("autolock not correctly set") } return nil }, @@ -147,7 +148,7 @@ func TestSwarmUpdate(t *testing.T) { }, swarmUpdateFunc: func(swarm swarm.Spec, flags swarm.UpdateFlags) error { if *swarm.Orchestration.TaskHistoryRetentionLimit != 10 { - return fmt.Errorf("historyLimit not correctly set") + return errors.Errorf("historyLimit not correctly set") } return nil }, diff --git a/components/cli/command/system/inspect.go b/components/cli/command/system/inspect.go index 6bb9cbe041..b937ea5b9d 100644 --- a/components/cli/command/system/inspect.go +++ b/components/cli/command/system/inspect.go @@ -10,6 +10,7 @@ import ( "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/inspect" apiclient "github.com/docker/docker/client" + "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -48,7 +49,7 @@ func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { case "", "container", "image", "node", "network", "service", "volume", "task", "plugin": elementSearcher = inspectAll(context.Background(), dockerCli, opts.size, opts.inspectType) default: - return fmt.Errorf("%q is not a valid value for --type", opts.inspectType) + return errors.Errorf("%q is not a valid value for --type", opts.inspectType) } return inspect.Inspect(dockerCli.Out(), opts.ids, opts.format, elementSearcher) } @@ -198,6 +199,6 @@ func inspectAll(ctx context.Context, dockerCli *command.DockerCli, getSize bool, } return v, raw, err } - return nil, nil, fmt.Errorf("Error: No such object: %s", ref) + return nil, nil, errors.Errorf("Error: No such object: %s", ref) } } diff --git a/components/cli/command/volume/create.go b/components/cli/command/volume/create.go index f7ca362150..8392cf0029 100644 --- a/components/cli/command/volume/create.go +++ b/components/cli/command/volume/create.go @@ -8,6 +8,7 @@ import ( "github.com/docker/docker/cli/command" "github.com/docker/docker/opts" runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" ) @@ -32,7 +33,7 @@ func newCreateCommand(dockerCli command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 1 { if opts.name != "" { - return fmt.Errorf("Conflicting options: either specify --name or provide positional arg, not both\n") + return errors.Errorf("Conflicting options: either specify --name or provide positional arg, not both\n") } opts.name = args[0] } diff --git a/components/cli/command/volume/create_test.go b/components/cli/command/volume/create_test.go index b7d5a443a5..ccb7ac75bf 100644 --- a/components/cli/command/volume/create_test.go +++ b/components/cli/command/volume/create_test.go @@ -2,7 +2,6 @@ package volume import ( "bytes" - "fmt" "io/ioutil" "strings" "testing" @@ -11,6 +10,7 @@ import ( volumetypes "github.com/docker/docker/api/types/volume" "github.com/docker/docker/cli/internal/test" "github.com/docker/docker/pkg/testutil/assert" + "github.com/pkg/errors" ) func TestVolumeCreateErrors(t *testing.T) { @@ -33,7 +33,7 @@ func TestVolumeCreateErrors(t *testing.T) { }, { volumeCreateFunc: func(createBody volumetypes.VolumesCreateBody) (types.Volume, error) { - return types.Volume{}, fmt.Errorf("error creating volume") + return types.Volume{}, errors.Errorf("error creating volume") }, expectedError: "error creating volume", }, @@ -60,7 +60,7 @@ func TestVolumeCreateWithName(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ volumeCreateFunc: func(body volumetypes.VolumesCreateBody) (types.Volume, error) { if body.Name != name { - return types.Volume{}, fmt.Errorf("expected name %q, got %q", name, body.Name) + return types.Volume{}, errors.Errorf("expected name %q, got %q", name, body.Name) } return types.Volume{ Name: body.Name, @@ -98,16 +98,16 @@ func TestVolumeCreateWithFlags(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ volumeCreateFunc: func(body volumetypes.VolumesCreateBody) (types.Volume, error) { if body.Name != "" { - return types.Volume{}, fmt.Errorf("expected empty name, got %q", body.Name) + return types.Volume{}, errors.Errorf("expected empty name, got %q", body.Name) } if body.Driver != expectedDriver { - return types.Volume{}, fmt.Errorf("expected driver %q, got %q", expectedDriver, body.Driver) + return types.Volume{}, errors.Errorf("expected driver %q, got %q", expectedDriver, body.Driver) } if !compareMap(body.DriverOpts, expectedOpts) { - return types.Volume{}, fmt.Errorf("expected drivers opts %v, got %v", expectedOpts, body.DriverOpts) + return types.Volume{}, errors.Errorf("expected drivers opts %v, got %v", expectedOpts, body.DriverOpts) } if !compareMap(body.Labels, expectedLabels) { - return types.Volume{}, fmt.Errorf("expected labels %v, got %v", expectedLabels, body.Labels) + return types.Volume{}, errors.Errorf("expected labels %v, got %v", expectedLabels, body.Labels) } return types.Volume{ Name: name, diff --git a/components/cli/command/volume/inspect_test.go b/components/cli/command/volume/inspect_test.go index e2ea7b35de..7c4cce39db 100644 --- a/components/cli/command/volume/inspect_test.go +++ b/components/cli/command/volume/inspect_test.go @@ -8,6 +8,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/cli/internal/test" + "github.com/pkg/errors" // Import builders to get the builder function as package function . "github.com/docker/docker/cli/internal/test/builders" "github.com/docker/docker/pkg/testutil/assert" @@ -27,7 +28,7 @@ func TestVolumeInspectErrors(t *testing.T) { { args: []string{"foo"}, volumeInspectFunc: func(volumeID string) (types.Volume, error) { - return types.Volume{}, fmt.Errorf("error while inspecting the volume") + return types.Volume{}, errors.Errorf("error while inspecting the volume") }, expectedError: "error while inspecting the volume", }, @@ -46,7 +47,7 @@ func TestVolumeInspectErrors(t *testing.T) { Name: "foo", }, nil } - return types.Volume{}, fmt.Errorf("error while inspecting the volume") + return types.Volume{}, errors.Errorf("error while inspecting the volume") }, expectedError: "error while inspecting the volume", }, @@ -78,7 +79,7 @@ func TestVolumeInspectWithoutFormat(t *testing.T) { args: []string{"foo"}, volumeInspectFunc: func(volumeID string) (types.Volume, error) { if volumeID != "foo" { - return types.Volume{}, fmt.Errorf("Invalid volumeID, expected %s, got %s", "foo", volumeID) + return types.Volume{}, errors.Errorf("Invalid volumeID, expected %s, got %s", "foo", volumeID) } return *Volume(), nil }, diff --git a/components/cli/command/volume/list_test.go b/components/cli/command/volume/list_test.go index 2f4a366333..b2306a5d8e 100644 --- a/components/cli/command/volume/list_test.go +++ b/components/cli/command/volume/list_test.go @@ -2,7 +2,6 @@ package volume import ( "bytes" - "fmt" "io/ioutil" "testing" @@ -11,6 +10,7 @@ import ( volumetypes "github.com/docker/docker/api/types/volume" "github.com/docker/docker/cli/config/configfile" "github.com/docker/docker/cli/internal/test" + "github.com/pkg/errors" // Import builders to get the builder function as package function . "github.com/docker/docker/cli/internal/test/builders" "github.com/docker/docker/pkg/testutil/assert" @@ -30,7 +30,7 @@ func TestVolumeListErrors(t *testing.T) { }, { volumeListFunc: func(filter filters.Args) (volumetypes.VolumesListOKBody, error) { - return volumetypes.VolumesListOKBody{}, fmt.Errorf("error listing volumes") + return volumetypes.VolumesListOKBody{}, errors.Errorf("error listing volumes") }, expectedError: "error listing volumes", }, diff --git a/components/cli/command/volume/prune_test.go b/components/cli/command/volume/prune_test.go index c07834675e..dab997f625 100644 --- a/components/cli/command/volume/prune_test.go +++ b/components/cli/command/volume/prune_test.go @@ -13,6 +13,7 @@ import ( "github.com/docker/docker/cli/internal/test" "github.com/docker/docker/pkg/testutil/assert" "github.com/docker/docker/pkg/testutil/golden" + "github.com/pkg/errors" ) func TestVolumePruneErrors(t *testing.T) { @@ -31,7 +32,7 @@ func TestVolumePruneErrors(t *testing.T) { "force": "true", }, volumePruneFunc: func(args filters.Args) (types.VolumesPruneReport, error) { - return types.VolumesPruneReport{}, fmt.Errorf("error pruning volumes") + return types.VolumesPruneReport{}, errors.Errorf("error pruning volumes") }, expectedError: "error pruning volumes", }, diff --git a/components/cli/command/volume/remove.go b/components/cli/command/volume/remove.go index c1267f1eab..683fe8139b 100644 --- a/components/cli/command/volume/remove.go +++ b/components/cli/command/volume/remove.go @@ -6,6 +6,7 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" ) @@ -53,7 +54,7 @@ func runRemove(dockerCli command.Cli, opts *removeOptions) error { } if len(errs) > 0 { - return fmt.Errorf("%s", strings.Join(errs, "\n")) + return errors.Errorf("%s", strings.Join(errs, "\n")) } return nil } diff --git a/components/cli/command/volume/remove_test.go b/components/cli/command/volume/remove_test.go index b2a106c22d..0154a5d551 100644 --- a/components/cli/command/volume/remove_test.go +++ b/components/cli/command/volume/remove_test.go @@ -2,12 +2,12 @@ package volume import ( "bytes" - "fmt" "io/ioutil" "testing" "github.com/docker/docker/cli/internal/test" "github.com/docker/docker/pkg/testutil/assert" + "github.com/pkg/errors" ) func TestVolumeRemoveErrors(t *testing.T) { @@ -22,7 +22,7 @@ func TestVolumeRemoveErrors(t *testing.T) { { args: []string{"nodeID"}, volumeRemoveFunc: func(volumeID string, force bool) error { - return fmt.Errorf("error removing the volume") + return errors.Errorf("error removing the volume") }, expectedError: "error removing the volume", }, diff --git a/components/cli/compose/convert/service.go b/components/cli/compose/convert/service.go index 8e31cbe8fb..fe9c281ae9 100644 --- a/components/cli/compose/convert/service.go +++ b/components/cli/compose/convert/service.go @@ -261,7 +261,7 @@ func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container ) if healthcheck.Disable { if len(healthcheck.Test) != 0 { - return nil, fmt.Errorf("test and disable can't be set at the same time") + return nil, errors.Errorf("test and disable can't be set at the same time") } return &container.HealthConfig{ Test: []string{"NONE"}, @@ -312,7 +312,7 @@ func convertRestartPolicy(restart string, source *composetypes.RestartPolicy) (* MaxAttempts: &attempts, }, nil default: - return nil, fmt.Errorf("unknown restart policy: %s", restart) + return nil, errors.Errorf("unknown restart policy: %s", restart) } } return &swarm.RestartPolicy{ @@ -418,13 +418,13 @@ func convertDeployMode(mode string, replicas *uint64) (swarm.ServiceMode, error) switch mode { case "global": if replicas != nil { - return serviceMode, fmt.Errorf("replicas can only be used with replicated mode") + return serviceMode, errors.Errorf("replicas can only be used with replicated mode") } serviceMode.Global = &swarm.GlobalService{} case "replicated", "": serviceMode.Replicated = &swarm.ReplicatedService{Replicas: replicas} default: - return serviceMode, fmt.Errorf("Unknown mode: %s", mode) + return serviceMode, errors.Errorf("Unknown mode: %s", mode) } return serviceMode, nil } diff --git a/components/cli/compose/loader/loader.go b/components/cli/compose/loader/loader.go index 821097bbf6..d69b530e6c 100644 --- a/components/cli/compose/loader/loader.go +++ b/components/cli/compose/loader/loader.go @@ -36,7 +36,7 @@ func ParseYAML(source []byte) (map[string]interface{}, error) { } cfgMap, ok := cfg.(map[interface{}]interface{}) if !ok { - return nil, fmt.Errorf("Top-level object must be a mapping") + return nil, errors.Errorf("Top-level object must be a mapping") } converted, err := convertToStringKeysRecursive(cfgMap, "") if err != nil { @@ -48,10 +48,10 @@ func ParseYAML(source []byte) (map[string]interface{}, error) { // Load reads a ConfigDetails and returns a fully loaded configuration func Load(configDetails types.ConfigDetails) (*types.Config, error) { if len(configDetails.ConfigFiles) < 1 { - return nil, fmt.Errorf("No files specified") + return nil, errors.Errorf("No files specified") } if len(configDetails.ConfigFiles) > 1 { - return nil, fmt.Errorf("Multiple files are not yet supported") + return nil, errors.Errorf("Multiple files are not yet supported") } configDict := getConfigDict(configDetails) @@ -310,7 +310,7 @@ func formatInvalidKeyError(keyPrefix string, key interface{}) error { } else { location = fmt.Sprintf("in %s", keyPrefix) } - return fmt.Errorf("Non-string key %s: %#v", location, key) + return errors.Errorf("Non-string key %s: %#v", location, key) } // LoadServices produces a ServiceConfig map from a compose file Dict @@ -415,7 +415,7 @@ func transformUlimits(data interface{}) (interface{}, error) { ulimit.Hard = value["hard"].(int) return ulimit, nil default: - return data, fmt.Errorf("invalid type %T for ulimits", value) + return data, errors.Errorf("invalid type %T for ulimits", value) } } @@ -503,7 +503,7 @@ func transformMapStringString(data interface{}) (interface{}, error) { case map[string]string: return value, nil default: - return data, fmt.Errorf("invalid type %T for map[string]string", value) + return data, errors.Errorf("invalid type %T for map[string]string", value) } } @@ -514,7 +514,7 @@ func transformExternal(data interface{}) (interface{}, error) { case map[string]interface{}: return map[string]interface{}{"external": true, "name": value["name"]}, nil default: - return data, fmt.Errorf("invalid type %T for external", value) + return data, errors.Errorf("invalid type %T for external", value) } } @@ -542,12 +542,12 @@ func transformServicePort(data interface{}) (interface{}, error) { case map[string]interface{}: ports = append(ports, value) default: - return data, fmt.Errorf("invalid type %T for port", value) + return data, errors.Errorf("invalid type %T for port", value) } } return ports, nil default: - return data, fmt.Errorf("invalid type %T for port", entries) + return data, errors.Errorf("invalid type %T for port", entries) } } @@ -558,7 +558,7 @@ func transformServiceSecret(data interface{}) (interface{}, error) { case map[string]interface{}: return data, nil default: - return data, fmt.Errorf("invalid type %T for secret", value) + return data, errors.Errorf("invalid type %T for secret", value) } } @@ -569,7 +569,7 @@ func transformServiceVolumeConfig(data interface{}) (interface{}, error) { case map[string]interface{}: return data, nil default: - return data, fmt.Errorf("invalid type %T for service volume", value) + return data, errors.Errorf("invalid type %T for service volume", value) } } @@ -601,7 +601,7 @@ func transformStringList(data interface{}) (interface{}, error) { case []interface{}: return value, nil default: - return data, fmt.Errorf("invalid type %T for string list", value) + return data, errors.Errorf("invalid type %T for string list", value) } } @@ -625,7 +625,7 @@ func transformMappingOrList(mappingOrList interface{}, sep string, allowNil bool } return result } - panic(fmt.Errorf("expected a map or a list, got %T: %#v", mappingOrList, mappingOrList)) + panic(errors.Errorf("expected a map or a list, got %T: %#v", mappingOrList, mappingOrList)) } func transformShellCommand(value interface{}) (interface{}, error) { @@ -642,7 +642,7 @@ func transformHealthCheckTest(data interface{}) (interface{}, error) { case []interface{}: return value, nil default: - return value, fmt.Errorf("invalid type %T for healthcheck.test", value) + return value, errors.Errorf("invalid type %T for healthcheck.test", value) } } @@ -653,7 +653,7 @@ func transformSize(value interface{}) (int64, error) { case string: return units.RAMInBytes(value) } - panic(fmt.Errorf("invalid type for size %T", value)) + panic(errors.Errorf("invalid type for size %T", value)) } func toServicePortConfigs(value string) ([]interface{}, error) { diff --git a/components/cli/compose/schema/bindata.go b/components/cli/compose/schema/bindata.go index e6ce0bfec2..0c6f8340f4 100644 --- a/components/cli/compose/schema/bindata.go +++ b/components/cli/compose/schema/bindata.go @@ -10,19 +10,20 @@ package schema import ( "bytes" "compress/gzip" - "fmt" "io" "io/ioutil" "os" "path/filepath" "strings" "time" + + "github.com/pkg/errors" ) func bindataRead(data []byte, name string) ([]byte, error) { gz, err := gzip.NewReader(bytes.NewBuffer(data)) if err != nil { - return nil, fmt.Errorf("Read %q: %v", name, err) + return nil, errors.Errorf("Read %q: %v", name, err) } var buf bytes.Buffer @@ -30,7 +31,7 @@ func bindataRead(data []byte, name string) ([]byte, error) { clErr := gz.Close() if err != nil { - return nil, fmt.Errorf("Read %q: %v", name, err) + return nil, errors.Errorf("Read %q: %v", name, err) } if clErr != nil { return nil, err @@ -138,11 +139,11 @@ func Asset(name string) ([]byte, error) { if f, ok := _bindata[cannonicalName]; ok { a, err := f() if err != nil { - return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) + return nil, errors.Errorf("Asset %s can't read by error: %v", name, err) } return a.bytes, nil } - return nil, fmt.Errorf("Asset %s not found", name) + return nil, errors.Errorf("Asset %s not found", name) } // MustAsset is like Asset but panics when Asset would return an error. @@ -164,11 +165,11 @@ func AssetInfo(name string) (os.FileInfo, error) { if f, ok := _bindata[cannonicalName]; ok { a, err := f() if err != nil { - return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) + return nil, errors.Errorf("AssetInfo %s can't read by error: %v", name, err) } return a.info, nil } - return nil, fmt.Errorf("AssetInfo %s not found", name) + return nil, errors.Errorf("AssetInfo %s not found", name) } // AssetNames returns the names of the assets. @@ -208,12 +209,12 @@ func AssetDir(name string) ([]string, error) { for _, p := range pathList { node = node.Children[p] if node == nil { - return nil, fmt.Errorf("Asset %s not found", name) + return nil, errors.Errorf("Asset %s not found", name) } } } if node.Func != nil { - return nil, fmt.Errorf("Asset %s not found", name) + return nil, errors.Errorf("Asset %s not found", name) } rv := make([]string, 0, len(node.Children)) for childName := range node.Children { @@ -226,6 +227,7 @@ type bintree struct { Func func() (*asset, error) Children map[string]*bintree } + var _bintree = &bintree{nil, map[string]*bintree{ "data": &bintree{nil, map[string]*bintree{ "config_schema_v3.0.json": &bintree{dataConfig_schema_v30Json, map[string]*bintree{}}, @@ -280,4 +282,3 @@ func _filePath(dir, name string) string { cannonicalName := strings.Replace(name, "\\", "/", -1) return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) } - diff --git a/components/cli/config/config.go b/components/cli/config/config.go index ab0fa5451a..9b21a2c902 100644 --- a/components/cli/config/config.go +++ b/components/cli/config/config.go @@ -1,7 +1,6 @@ package config import ( - "fmt" "io" "os" "path/filepath" @@ -9,6 +8,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/cli/config/configfile" "github.com/docker/docker/pkg/homedir" + "github.com/pkg/errors" ) const ( @@ -84,18 +84,18 @@ func Load(configDir string) (*configfile.ConfigFile, error) { if _, err := os.Stat(configFile.Filename); err == nil { file, err := os.Open(configFile.Filename) if err != nil { - return &configFile, fmt.Errorf("%s - %v", configFile.Filename, err) + return &configFile, errors.Errorf("%s - %v", configFile.Filename, err) } defer file.Close() err = configFile.LoadFromReader(file) if err != nil { - err = fmt.Errorf("%s - %v", configFile.Filename, err) + err = errors.Errorf("%s - %v", configFile.Filename, err) } return &configFile, err } else if !os.IsNotExist(err) { // if file is there but we can't stat it for any reason other // than it doesn't exist then stop - return &configFile, fmt.Errorf("%s - %v", configFile.Filename, err) + return &configFile, errors.Errorf("%s - %v", configFile.Filename, err) } // Can't find latest config file so check for the old one @@ -105,12 +105,12 @@ func Load(configDir string) (*configfile.ConfigFile, error) { } file, err := os.Open(confFile) if err != nil { - return &configFile, fmt.Errorf("%s - %v", confFile, err) + return &configFile, errors.Errorf("%s - %v", confFile, err) } defer file.Close() err = configFile.LegacyLoadFromReader(file) if err != nil { - return &configFile, fmt.Errorf("%s - %v", confFile, err) + return &configFile, errors.Errorf("%s - %v", confFile, err) } if configFile.HTTPHeaders == nil { diff --git a/components/cli/config/configfile/file.go b/components/cli/config/configfile/file.go index e97fbe47ba..cc1c3d0d54 100644 --- a/components/cli/config/configfile/file.go +++ b/components/cli/config/configfile/file.go @@ -3,7 +3,6 @@ package configfile import ( "encoding/base64" "encoding/json" - "fmt" "io" "io/ioutil" "os" @@ -11,6 +10,7 @@ import ( "strings" "github.com/docker/docker/api/types" + "github.com/pkg/errors" ) const ( @@ -51,12 +51,12 @@ func (configFile *ConfigFile) LegacyLoadFromReader(configData io.Reader) error { if err := json.Unmarshal(b, &configFile.AuthConfigs); err != nil { arr := strings.Split(string(b), "\n") if len(arr) < 2 { - return fmt.Errorf("The Auth config file is empty") + return errors.Errorf("The Auth config file is empty") } authConfig := types.AuthConfig{} origAuth := strings.Split(arr[0], " = ") if len(origAuth) != 2 { - return fmt.Errorf("Invalid Auth config file") + return errors.Errorf("Invalid Auth config file") } authConfig.Username, authConfig.Password, err = decodeAuth(origAuth[1]) if err != nil { @@ -135,7 +135,7 @@ func (configFile *ConfigFile) SaveToWriter(writer io.Writer) error { // Save encodes and writes out all the authorization information func (configFile *ConfigFile) Save() error { if configFile.Filename == "" { - return fmt.Errorf("Can't save config with empty filename") + return errors.Errorf("Can't save config with empty filename") } if err := os.MkdirAll(filepath.Dir(configFile.Filename), 0700); err != nil { @@ -176,11 +176,11 @@ func decodeAuth(authStr string) (string, string, error) { return "", "", err } if n > decLen { - return "", "", fmt.Errorf("Something went wrong decoding auth config") + return "", "", errors.Errorf("Something went wrong decoding auth config") } arr := strings.SplitN(string(decoded), ":", 2) if len(arr) != 2 { - return "", "", fmt.Errorf("Invalid auth configuration file") + return "", "", errors.Errorf("Invalid auth configuration file") } password := strings.Trim(arr[1], "\x00") return arr[0], password, nil diff --git a/components/cli/config/credentials/native_store_test.go b/components/cli/config/credentials/native_store_test.go index 7664faf9e1..360cc20efc 100644 --- a/components/cli/config/credentials/native_store_test.go +++ b/components/cli/config/credentials/native_store_test.go @@ -11,6 +11,7 @@ import ( "github.com/docker/docker-credential-helpers/client" "github.com/docker/docker-credential-helpers/credentials" "github.com/docker/docker/api/types" + "github.com/pkg/errors" ) const ( @@ -20,7 +21,7 @@ const ( missingCredsAddress = "https://missing.docker.io/v1" ) -var errCommandExited = fmt.Errorf("exited 1") +var errCommandExited = errors.Errorf("exited 1") // mockCommand simulates interactions between the docker client and a remote // credentials helper. diff --git a/components/cli/required.go b/components/cli/required.go index 8ee02c8429..d28af86be5 100644 --- a/components/cli/required.go +++ b/components/cli/required.go @@ -1,9 +1,9 @@ package cli import ( - "fmt" "strings" + "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -14,10 +14,10 @@ func NoArgs(cmd *cobra.Command, args []string) error { } if cmd.HasSubCommands() { - return fmt.Errorf("\n" + strings.TrimRight(cmd.UsageString(), "\n")) + return errors.Errorf("\n" + strings.TrimRight(cmd.UsageString(), "\n")) } - return fmt.Errorf( + return errors.Errorf( "\"%s\" accepts no argument(s).\nSee '%s --help'.\n\nUsage: %s\n\n%s", cmd.CommandPath(), cmd.CommandPath(), @@ -32,7 +32,7 @@ func RequiresMinArgs(min int) cobra.PositionalArgs { if len(args) >= min { return nil } - return fmt.Errorf( + return errors.Errorf( "\"%s\" requires at least %d argument(s).\nSee '%s --help'.\n\nUsage: %s\n\n%s", cmd.CommandPath(), min, @@ -49,7 +49,7 @@ func RequiresMaxArgs(max int) cobra.PositionalArgs { if len(args) <= max { return nil } - return fmt.Errorf( + return errors.Errorf( "\"%s\" requires at most %d argument(s).\nSee '%s --help'.\n\nUsage: %s\n\n%s", cmd.CommandPath(), max, @@ -66,7 +66,7 @@ func RequiresRangeArgs(min int, max int) cobra.PositionalArgs { if len(args) >= min && len(args) <= max { return nil } - return fmt.Errorf( + return errors.Errorf( "\"%s\" requires at least %d and at most %d argument(s).\nSee '%s --help'.\n\nUsage: %s\n\n%s", cmd.CommandPath(), min, @@ -84,7 +84,7 @@ func ExactArgs(number int) cobra.PositionalArgs { if len(args) == number { return nil } - return fmt.Errorf( + return errors.Errorf( "\"%s\" requires exactly %d argument(s).\nSee '%s --help'.\n\nUsage: %s\n\n%s", cmd.CommandPath(), number, diff --git a/components/cli/trust/trust.go b/components/cli/trust/trust.go index 777a611181..3c75e485cc 100644 --- a/components/cli/trust/trust.go +++ b/components/cli/trust/trust.go @@ -2,7 +2,6 @@ package trust import ( "encoding/json" - "fmt" "net" "net/http" "net/url" @@ -29,6 +28,7 @@ import ( "github.com/docker/notary/trustpinning" "github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/signed" + "github.com/pkg/errors" ) var ( @@ -57,7 +57,7 @@ func Server(index *registrytypes.IndexInfo) (string, error) { if s := os.Getenv("DOCKER_CONTENT_TRUST_SERVER"); s != "" { urlObj, err := url.Parse(s) if err != nil || urlObj.Scheme != "https" { - return "", fmt.Errorf("valid https URL required for trust server, got %s", s) + return "", errors.Errorf("valid https URL required for trust server, got %s", s) } return s, nil @@ -205,27 +205,27 @@ func NotaryError(repoName string, err error) error { switch err.(type) { case *json.SyntaxError: logrus.Debugf("Notary syntax error: %s", err) - return fmt.Errorf("Error: no trust data available for remote repository %s. Try running notary server and setting DOCKER_CONTENT_TRUST_SERVER to its HTTPS address?", repoName) + return errors.Errorf("Error: no trust data available for remote repository %s. Try running notary server and setting DOCKER_CONTENT_TRUST_SERVER to its HTTPS address?", repoName) case signed.ErrExpired: - return fmt.Errorf("Error: remote repository %s out-of-date: %v", repoName, err) + return errors.Errorf("Error: remote repository %s out-of-date: %v", repoName, err) case trustmanager.ErrKeyNotFound: - return fmt.Errorf("Error: signing keys for remote repository %s not found: %v", repoName, err) + return errors.Errorf("Error: signing keys for remote repository %s not found: %v", repoName, err) case storage.NetworkError: - return fmt.Errorf("Error: error contacting notary server: %v", err) + return errors.Errorf("Error: error contacting notary server: %v", err) case storage.ErrMetaNotFound: - return fmt.Errorf("Error: trust data missing for remote repository %s or remote repository not found: %v", repoName, err) + return errors.Errorf("Error: trust data missing for remote repository %s or remote repository not found: %v", repoName, err) case trustpinning.ErrRootRotationFail, trustpinning.ErrValidationFail, signed.ErrInvalidKeyType: - return fmt.Errorf("Warning: potential malicious behavior - trust data mismatch for remote repository %s: %v", repoName, err) + return errors.Errorf("Warning: potential malicious behavior - trust data mismatch for remote repository %s: %v", repoName, err) case signed.ErrNoKeys: - return fmt.Errorf("Error: could not find signing keys for remote repository %s, or could not decrypt signing key: %v", repoName, err) + return errors.Errorf("Error: could not find signing keys for remote repository %s, or could not decrypt signing key: %v", repoName, err) case signed.ErrLowVersion: - return fmt.Errorf("Warning: potential malicious behavior - trust data version is lower than expected for remote repository %s: %v", repoName, err) + return errors.Errorf("Warning: potential malicious behavior - trust data version is lower than expected for remote repository %s: %v", repoName, err) case signed.ErrRoleThreshold: - return fmt.Errorf("Warning: potential malicious behavior - trust data has insufficient signatures for remote repository %s: %v", repoName, err) + return errors.Errorf("Warning: potential malicious behavior - trust data has insufficient signatures for remote repository %s: %v", repoName, err) case client.ErrRepositoryNotExist: - return fmt.Errorf("Error: remote trust data does not exist for %s: %v", repoName, err) + return errors.Errorf("Error: remote trust data does not exist for %s: %v", repoName, err) case signed.ErrInsufficientSignatures: - return fmt.Errorf("Error: could not produce valid signature for %s. If Yubikey was used, was touch input provided?: %v", repoName, err) + return errors.Errorf("Error: could not produce valid signature for %s. If Yubikey was used, was touch input provided?: %v", repoName, err) } return err From 19fb367fca082d40946d981d7d064fa2f4f22ee8 Mon Sep 17 00:00:00 2001 From: Alessandro Boch Date: Thu, 9 Mar 2017 11:52:25 -0800 Subject: [PATCH 688/978] Allow user to modify ingress network Signed-off-by: Alessandro Boch Upstream-commit: 0f6dd9c2e8da7a502b4e4306927b12e45de7dcfa Component: cli --- components/cli/command/network/create.go | 4 ++++ components/cli/command/network/remove.go | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/components/cli/command/network/create.go b/components/cli/command/network/create.go index 21300d7839..2de64c1967 100644 --- a/components/cli/command/network/create.go +++ b/components/cli/command/network/create.go @@ -24,6 +24,7 @@ type createOptions struct { internal bool ipv6 bool attachable bool + ingress bool ipamDriver string ipamSubnet []string @@ -59,6 +60,8 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { flags.BoolVar(&opts.ipv6, "ipv6", false, "Enable IPv6 networking") flags.BoolVar(&opts.attachable, "attachable", false, "Enable manual container attachment") flags.SetAnnotation("attachable", "version", []string{"1.25"}) + flags.BoolVar(&opts.ingress, "ingress", false, "Create swarm routing-mesh network") + flags.SetAnnotation("ingress", "version", []string{"1.29"}) flags.StringVar(&opts.ipamDriver, "ipam-driver", "default", "IP Address Management Driver") flags.StringSliceVar(&opts.ipamSubnet, "subnet", []string{}, "Subnet in CIDR format that represents a network segment") @@ -92,6 +95,7 @@ func runCreate(dockerCli *command.DockerCli, opts createOptions) error { Internal: opts.internal, EnableIPv6: opts.ipv6, Attachable: opts.attachable, + Ingress: opts.ingress, Labels: runconfigopts.ConvertKVStringsToMap(opts.labels.GetAll()), } diff --git a/components/cli/command/network/remove.go b/components/cli/command/network/remove.go index 2034b8709e..b5f074a981 100644 --- a/components/cli/command/network/remove.go +++ b/components/cli/command/network/remove.go @@ -22,12 +22,22 @@ func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { } } +const ingressWarning = "WARNING! Before removing the routing-mesh network, " + + "make sure all the nodes in your swarm run the same docker engine version. " + + "Otherwise, removal may not be effective and functionality of newly create " + + "ingress networks will be impaired.\nAre you sure you want to continue?" + func runRemove(dockerCli *command.DockerCli, networks []string) error { client := dockerCli.Client() ctx := context.Background() status := 0 for _, name := range networks { + if nw, _, err := client.NetworkInspectWithRaw(ctx, name, false); err == nil && + nw.Ingress && + !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), ingressWarning) { + continue + } if err := client.NetworkRemove(ctx, name); err != nil { fmt.Fprintf(dockerCli.Err(), "%s\n", err) status = 1 From 6445b15529a5cb93df0122f9980b41ef7848d1ef Mon Sep 17 00:00:00 2001 From: allencloud Date: Tue, 6 Dec 2016 00:08:43 +0800 Subject: [PATCH 689/978] make secret ls support filters in CLI Signed-off-by: allencloud Upstream-commit: d6490e5de964157aa34c360468a7c872689f7520 Component: cli --- components/cli/command/secret/ls.go | 7 +++++-- components/cli/command/service/parse.go | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/components/cli/command/secret/ls.go b/components/cli/command/secret/ls.go index 211ebceb5f..1d60ff7c4d 100644 --- a/components/cli/command/secret/ls.go +++ b/components/cli/command/secret/ls.go @@ -5,6 +5,7 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/formatter" + "github.com/docker/docker/opts" "github.com/spf13/cobra" "golang.org/x/net/context" ) @@ -12,10 +13,11 @@ import ( type listOptions struct { quiet bool format string + filter opts.FilterOpt } func newSecretListCommand(dockerCli *command.DockerCli) *cobra.Command { - opts := listOptions{} + opts := listOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ Use: "ls [OPTIONS]", @@ -30,6 +32,7 @@ func newSecretListCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs") flags.StringVarP(&opts.format, "format", "", "", "Pretty-print secrets using a Go template") + flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") return cmd } @@ -38,7 +41,7 @@ func runSecretList(dockerCli *command.DockerCli, opts listOptions) error { client := dockerCli.Client() ctx := context.Background() - secrets, err := client.SecretList(ctx, types.SecretListOptions{}) + secrets, err := client.SecretList(ctx, types.SecretListOptions{Filters: opts.filter.Value()}) if err != nil { return err } diff --git a/components/cli/command/service/parse.go b/components/cli/command/service/parse.go index baf5e24547..77dfb25fbc 100644 --- a/components/cli/command/service/parse.go +++ b/components/cli/command/service/parse.go @@ -27,7 +27,7 @@ func ParseSecrets(client client.SecretAPIClient, requestedSecrets []*swarmtypes. args := filters.NewArgs() for _, s := range secretRefs { - args.Add("names", s.SecretName) + args.Add("name", s.SecretName) } secrets, err := client.SecretList(ctx, types.SecretListOptions{ From e595996da47f7946e9716f543702959131d77e1f Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 27 Mar 2017 11:42:15 +0200 Subject: [PATCH 690/978] Set the alias to the service name instead of the network name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This makes it work a little closer to compose part and it is more correct 👼 Signed-off-by: Vincent Demeester Upstream-commit: aaf865edb5a10a4a5909100d4136b97c04c258f0 Component: cli --- components/cli/command/stack/deploy_bundlefile.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/stack/deploy_bundlefile.go b/components/cli/command/stack/deploy_bundlefile.go index 14e627cafc..0f8f8d040b 100644 --- a/components/cli/command/stack/deploy_bundlefile.go +++ b/components/cli/command/stack/deploy_bundlefile.go @@ -54,7 +54,7 @@ func deployBundle(ctx context.Context, dockerCli *command.DockerCli, opts deploy for _, networkName := range service.Networks { nets = append(nets, swarm.NetworkAttachmentConfig{ Target: namespace.Scope(networkName), - Aliases: []string{networkName}, + Aliases: []string{internalName}, }) } From 8f6c4a1b5b4e74459844eadbd0059ac690bf59e4 Mon Sep 17 00:00:00 2001 From: Tibor Vass Date: Mon, 27 Mar 2017 18:21:59 -0700 Subject: [PATCH 691/978] Manually reorganize import paths to segregate stdlib and 3rd-party packages Signed-off-by: Tibor Vass Upstream-commit: d26a23ceb834757e4ef522625809af4be7ed447f Component: cli --- components/cli/command/cli.go | 2 +- components/cli/command/container/attach.go | 2 +- components/cli/command/container/diff.go | 2 +- components/cli/command/container/export.go | 2 +- components/cli/command/container/kill.go | 2 +- components/cli/command/container/pause.go | 2 +- components/cli/command/container/restart.go | 2 +- components/cli/command/container/rm.go | 2 +- components/cli/command/container/run.go | 2 +- components/cli/command/container/stats.go | 2 +- components/cli/command/container/stats_helpers.go | 2 +- components/cli/command/container/stop.go | 2 +- components/cli/command/container/unpause.go | 2 +- components/cli/command/container/update.go | 2 +- components/cli/command/container/wait.go | 2 +- components/cli/command/image/pull.go | 5 ++--- components/cli/command/image/save.go | 5 ++--- components/cli/command/in.go | 2 +- components/cli/command/swarm/join_token.go | 5 ++--- components/cli/command/swarm/unlock.go | 7 +++---- 20 files changed, 25 insertions(+), 29 deletions(-) diff --git a/components/cli/command/cli.go b/components/cli/command/cli.go index 9db5d8d0f5..e2a89eb0b2 100644 --- a/components/cli/command/cli.go +++ b/components/cli/command/cli.go @@ -2,7 +2,6 @@ package command import ( "fmt" - "github.com/pkg/errors" "io" "net/http" "os" @@ -21,6 +20,7 @@ import ( dopts "github.com/docker/docker/opts" "github.com/docker/go-connections/sockets" "github.com/docker/go-connections/tlsconfig" + "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" ) diff --git a/components/cli/command/container/attach.go b/components/cli/command/container/attach.go index 7d2869f76c..d37cc73603 100644 --- a/components/cli/command/container/attach.go +++ b/components/cli/command/container/attach.go @@ -1,7 +1,6 @@ package container import ( - "github.com/pkg/errors" "io" "net/http/httputil" @@ -10,6 +9,7 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/pkg/signal" + "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" ) diff --git a/components/cli/command/container/diff.go b/components/cli/command/container/diff.go index c279c4849c..95926f5867 100644 --- a/components/cli/command/container/diff.go +++ b/components/cli/command/container/diff.go @@ -2,11 +2,11 @@ package container import ( "fmt" - "github.com/pkg/errors" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/pkg/archive" + "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" ) diff --git a/components/cli/command/container/export.go b/components/cli/command/container/export.go index dfb514440e..cb0ddfe7a7 100644 --- a/components/cli/command/container/export.go +++ b/components/cli/command/container/export.go @@ -1,11 +1,11 @@ package container import ( - "github.com/pkg/errors" "io" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" ) diff --git a/components/cli/command/container/kill.go b/components/cli/command/container/kill.go index 32eea6c0b6..4cc3ee0fcb 100644 --- a/components/cli/command/container/kill.go +++ b/components/cli/command/container/kill.go @@ -2,11 +2,11 @@ package container import ( "fmt" - "github.com/pkg/errors" "strings" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" ) diff --git a/components/cli/command/container/pause.go b/components/cli/command/container/pause.go index 742d6d5560..095a0db2c2 100644 --- a/components/cli/command/container/pause.go +++ b/components/cli/command/container/pause.go @@ -2,11 +2,11 @@ package container import ( "fmt" - "github.com/pkg/errors" "strings" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" ) diff --git a/components/cli/command/container/restart.go b/components/cli/command/container/restart.go index 7cfc9c0eab..73cd2507ee 100644 --- a/components/cli/command/container/restart.go +++ b/components/cli/command/container/restart.go @@ -2,12 +2,12 @@ package container import ( "fmt" - "github.com/pkg/errors" "strings" "time" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" ) diff --git a/components/cli/command/container/rm.go b/components/cli/command/container/rm.go index 7e6fd4588b..887b5c5d34 100644 --- a/components/cli/command/container/rm.go +++ b/components/cli/command/container/rm.go @@ -2,12 +2,12 @@ package container import ( "fmt" - "github.com/pkg/errors" "strings" "github.com/docker/docker/api/types" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" ) diff --git a/components/cli/command/container/run.go b/components/cli/command/container/run.go index 4fd05c74bb..bab6a9cf13 100644 --- a/components/cli/command/container/run.go +++ b/components/cli/command/container/run.go @@ -2,7 +2,6 @@ package container import ( "fmt" - "github.com/pkg/errors" "io" "net/http/httputil" "os" @@ -18,6 +17,7 @@ import ( "github.com/docker/docker/pkg/promise" "github.com/docker/docker/pkg/signal" "github.com/docker/libnetwork/resolvconf/dns" + "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" "golang.org/x/net/context" diff --git a/components/cli/command/container/stats.go b/components/cli/command/container/stats.go index 9d2d59a5b4..c420e8151e 100644 --- a/components/cli/command/container/stats.go +++ b/components/cli/command/container/stats.go @@ -2,7 +2,6 @@ package container import ( "fmt" - "github.com/pkg/errors" "io" "strings" "sync" @@ -14,6 +13,7 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/formatter" + "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" ) diff --git a/components/cli/command/container/stats_helpers.go b/components/cli/command/container/stats_helpers.go index 8f7a924f2d..5cbcf03e40 100644 --- a/components/cli/command/container/stats_helpers.go +++ b/components/cli/command/container/stats_helpers.go @@ -2,7 +2,6 @@ package container import ( "encoding/json" - "github.com/pkg/errors" "io" "strings" "sync" @@ -12,6 +11,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/cli/command/formatter" "github.com/docker/docker/client" + "github.com/pkg/errors" "golang.org/x/net/context" ) diff --git a/components/cli/command/container/stop.go b/components/cli/command/container/stop.go index cba20c77ae..32729e1eae 100644 --- a/components/cli/command/container/stop.go +++ b/components/cli/command/container/stop.go @@ -2,12 +2,12 @@ package container import ( "fmt" - "github.com/pkg/errors" "strings" "time" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" ) diff --git a/components/cli/command/container/unpause.go b/components/cli/command/container/unpause.go index 1842991540..8105b17551 100644 --- a/components/cli/command/container/unpause.go +++ b/components/cli/command/container/unpause.go @@ -2,11 +2,11 @@ package container import ( "fmt" - "github.com/pkg/errors" "strings" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" ) diff --git a/components/cli/command/container/update.go b/components/cli/command/container/update.go index 22b2863974..283cd3314e 100644 --- a/components/cli/command/container/update.go +++ b/components/cli/command/container/update.go @@ -2,7 +2,6 @@ package container import ( "fmt" - "github.com/pkg/errors" "strings" containertypes "github.com/docker/docker/api/types/container" @@ -10,6 +9,7 @@ import ( "github.com/docker/docker/cli/command" "github.com/docker/docker/opts" runconfigopts "github.com/docker/docker/runconfig/opts" + "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" ) diff --git a/components/cli/command/container/wait.go b/components/cli/command/container/wait.go index 9b46318f5b..f978207b94 100644 --- a/components/cli/command/container/wait.go +++ b/components/cli/command/container/wait.go @@ -2,11 +2,11 @@ package container import ( "fmt" - "github.com/pkg/errors" "strings" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" ) diff --git a/components/cli/command/image/pull.go b/components/cli/command/image/pull.go index 2c702e898c..5dd523c6d1 100644 --- a/components/cli/command/image/pull.go +++ b/components/cli/command/image/pull.go @@ -2,16 +2,15 @@ package image import ( "fmt" - "github.com/pkg/errors" "strings" - "golang.org/x/net/context" - "github.com/docker/distribution/reference" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/registry" + "github.com/pkg/errors" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type pullOptions struct { diff --git a/components/cli/command/image/save.go b/components/cli/command/image/save.go index f475f17ff2..e01d2c7302 100644 --- a/components/cli/command/image/save.go +++ b/components/cli/command/image/save.go @@ -1,14 +1,13 @@ package image import ( - "github.com/pkg/errors" "io" - "golang.org/x/net/context" - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/pkg/errors" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type saveOptions struct { diff --git a/components/cli/command/in.go b/components/cli/command/in.go index d12af6fd96..50de77ee9b 100644 --- a/components/cli/command/in.go +++ b/components/cli/command/in.go @@ -1,12 +1,12 @@ package command import ( - "github.com/pkg/errors" "io" "os" "runtime" "github.com/docker/docker/pkg/term" + "github.com/pkg/errors" ) // InStream is an input stream used by the DockerCli to read user input diff --git a/components/cli/command/swarm/join_token.go b/components/cli/command/swarm/join_token.go index 006ea07c3f..dc69e909e0 100644 --- a/components/cli/command/swarm/join_token.go +++ b/components/cli/command/swarm/join_token.go @@ -2,13 +2,12 @@ package swarm import ( "fmt" - "github.com/pkg/errors" - - "github.com/spf13/cobra" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/pkg/errors" + "github.com/spf13/cobra" "golang.org/x/net/context" ) diff --git a/components/cli/command/swarm/unlock.go b/components/cli/command/swarm/unlock.go index bb3068f1ef..c1d9b99189 100644 --- a/components/cli/command/swarm/unlock.go +++ b/components/cli/command/swarm/unlock.go @@ -3,16 +3,15 @@ package swarm import ( "bufio" "fmt" - "github.com/pkg/errors" "io" "strings" - "github.com/spf13/cobra" - "golang.org/x/crypto/ssh/terminal" - "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "golang.org/x/crypto/ssh/terminal" "golang.org/x/net/context" ) From cda26f1da07cdc1055afe31969459570092acd58 Mon Sep 17 00:00:00 2001 From: Tibor Vass Date: Mon, 27 Mar 2017 18:33:41 -0700 Subject: [PATCH 692/978] Do not replace fmt.Errorf in generated file Signed-off-by: Tibor Vass Upstream-commit: 96e610e67a463cdf076c83f1ee23c2d7d0008194 Component: cli --- components/cli/compose/schema/bindata.go | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/components/cli/compose/schema/bindata.go b/components/cli/compose/schema/bindata.go index 0c6f8340f4..e6ce0bfec2 100644 --- a/components/cli/compose/schema/bindata.go +++ b/components/cli/compose/schema/bindata.go @@ -10,20 +10,19 @@ package schema import ( "bytes" "compress/gzip" + "fmt" "io" "io/ioutil" "os" "path/filepath" "strings" "time" - - "github.com/pkg/errors" ) func bindataRead(data []byte, name string) ([]byte, error) { gz, err := gzip.NewReader(bytes.NewBuffer(data)) if err != nil { - return nil, errors.Errorf("Read %q: %v", name, err) + return nil, fmt.Errorf("Read %q: %v", name, err) } var buf bytes.Buffer @@ -31,7 +30,7 @@ func bindataRead(data []byte, name string) ([]byte, error) { clErr := gz.Close() if err != nil { - return nil, errors.Errorf("Read %q: %v", name, err) + return nil, fmt.Errorf("Read %q: %v", name, err) } if clErr != nil { return nil, err @@ -139,11 +138,11 @@ func Asset(name string) ([]byte, error) { if f, ok := _bindata[cannonicalName]; ok { a, err := f() if err != nil { - return nil, errors.Errorf("Asset %s can't read by error: %v", name, err) + return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) } return a.bytes, nil } - return nil, errors.Errorf("Asset %s not found", name) + return nil, fmt.Errorf("Asset %s not found", name) } // MustAsset is like Asset but panics when Asset would return an error. @@ -165,11 +164,11 @@ func AssetInfo(name string) (os.FileInfo, error) { if f, ok := _bindata[cannonicalName]; ok { a, err := f() if err != nil { - return nil, errors.Errorf("AssetInfo %s can't read by error: %v", name, err) + return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) } return a.info, nil } - return nil, errors.Errorf("AssetInfo %s not found", name) + return nil, fmt.Errorf("AssetInfo %s not found", name) } // AssetNames returns the names of the assets. @@ -209,12 +208,12 @@ func AssetDir(name string) ([]string, error) { for _, p := range pathList { node = node.Children[p] if node == nil { - return nil, errors.Errorf("Asset %s not found", name) + return nil, fmt.Errorf("Asset %s not found", name) } } } if node.Func != nil { - return nil, errors.Errorf("Asset %s not found", name) + return nil, fmt.Errorf("Asset %s not found", name) } rv := make([]string, 0, len(node.Children)) for childName := range node.Children { @@ -227,7 +226,6 @@ type bintree struct { Func func() (*asset, error) Children map[string]*bintree } - var _bintree = &bintree{nil, map[string]*bintree{ "data": &bintree{nil, map[string]*bintree{ "config_schema_v3.0.json": &bintree{dataConfig_schema_v30Json, map[string]*bintree{}}, @@ -282,3 +280,4 @@ func _filePath(dir, name string) string { cannonicalName := strings.Replace(name, "\\", "/", -1) return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) } + From cc486b1c0baa6b3a437204c6f886c3716e43a3e1 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 27 Mar 2017 09:58:09 +0200 Subject: [PATCH 693/978] Add support for `--type=secret` in `docker inspect` Signed-off-by: Vincent Demeester Upstream-commit: ba785f32f88bdda396a43ebc5972536b5a0177dc Component: cli --- components/cli/command/system/inspect.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/components/cli/command/system/inspect.go b/components/cli/command/system/inspect.go index 6bb9cbe041..2b5ac22245 100644 --- a/components/cli/command/system/inspect.go +++ b/components/cli/command/system/inspect.go @@ -4,13 +4,12 @@ import ( "fmt" "strings" - "golang.org/x/net/context" - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/docker/docker/cli/command/inspect" apiclient "github.com/docker/docker/client" "github.com/spf13/cobra" + "golang.org/x/net/context" ) type inspectOptions struct { @@ -45,7 +44,7 @@ func NewInspectCommand(dockerCli *command.DockerCli) *cobra.Command { func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error { var elementSearcher inspect.GetRefFunc switch opts.inspectType { - case "", "container", "image", "node", "network", "service", "volume", "task", "plugin": + case "", "container", "image", "node", "network", "service", "volume", "task", "plugin", "secret": elementSearcher = inspectAll(context.Background(), dockerCli, opts.size, opts.inspectType) default: return fmt.Errorf("%q is not a valid value for --type", opts.inspectType) @@ -101,6 +100,12 @@ func inspectPlugin(ctx context.Context, dockerCli *command.DockerCli) inspect.Ge } } +func inspectSecret(ctx context.Context, dockerCli *command.DockerCli) inspect.GetRefFunc { + return func(ref string) (interface{}, []byte, error) { + return dockerCli.Client().SecretInspectWithRaw(ctx, ref) + } +} + func inspectAll(ctx context.Context, dockerCli *command.DockerCli, getSize bool, typeConstraint string) inspect.GetRefFunc { var inspectAutodetect = []struct { objectType string @@ -144,6 +149,11 @@ func inspectAll(ctx context.Context, dockerCli *command.DockerCli, getSize bool, objectType: "plugin", objectInspector: inspectPlugin(ctx, dockerCli), }, + { + objectType: "secret", + isSwarmObject: true, + objectInspector: inspectSecret(ctx, dockerCli), + }, } // isSwarmManager does an Info API call to verify that the daemon is From 9942af35073ea3f787df9a54fd829450633f85f3 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 7 Dec 2016 14:37:55 -0500 Subject: [PATCH 694/978] Add entrypoint flags to service cli. Signed-off-by: Daniel Nephin Upstream-commit: 951fdd11cda0ed76fd435b1d034118b2ab017761 Component: cli --- components/cli/command/service/opts.go | 29 ++++++++++++++++++++++++ components/cli/command/service/update.go | 13 ++++------- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index 2afae80c52..1ff6575c09 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -10,6 +10,7 @@ import ( "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/opts" runconfigopts "github.com/docker/docker/runconfig/opts" + shlex "github.com/flynn-archive/go-shlex" "github.com/pkg/errors" "github.com/spf13/pflag" ) @@ -157,6 +158,30 @@ func (opts *placementPrefOpts) Type() string { return "pref" } +// ShlexOpt is a flag Value which parses a string as a list of shell words +type ShlexOpt []string + +// Set the value +func (s *ShlexOpt) Set(value string) error { + valueSlice, err := shlex.Split(value) + *s = ShlexOpt(valueSlice) + return err +} + +// Type returns the tyep of the value +func (s *ShlexOpt) Type() string { + return "command" +} + +func (s *ShlexOpt) String() string { + return fmt.Sprint(*s) +} + +// Value returns the value as a string slice +func (s *ShlexOpt) Value() []string { + return []string(*s) +} + type updateOptions struct { parallelism uint64 delay time.Duration @@ -312,6 +337,7 @@ type serviceOptions struct { labels opts.ListOpts containerLabels opts.ListOpts image string + entrypoint ShlexOpt args []string hostname string env opts.ListOpts @@ -427,6 +453,7 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { ContainerSpec: swarm.ContainerSpec{ Image: opts.image, Args: opts.args, + Command: opts.entrypoint.Value(), Env: currentEnv, Hostname: opts.hostname, Labels: runconfigopts.ConvertKVStringsToMap(opts.containerLabels.GetAll()), @@ -473,6 +500,7 @@ func addServiceFlags(flags *pflag.FlagSet, opts *serviceOptions) { flags.StringVarP(&opts.user, flagUser, "u", "", "Username or UID (format: [:])") flags.StringVar(&opts.hostname, flagHostname, "", "Container hostname") flags.SetAnnotation(flagHostname, "version", []string{"1.25"}) + flags.Var(&opts.entrypoint, flagEntrypoint, "Overwrite the default ENTRYPOINT of the image") flags.Var(&opts.resources.limitCPU, flagLimitCPU, "Limit CPUs") flags.Var(&opts.resources.limitMemBytes, flagLimitMemory, "Limit Memory") @@ -554,6 +582,7 @@ const ( flagDNSSearchRemove = "dns-search-rm" flagDNSSearchAdd = "dns-search-add" flagEndpointMode = "endpoint-mode" + flagEntrypoint = "entrypoint" flagHost = "host" flagHostAdd = "host-add" flagHostRemove = "host-rm" diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index 6470d25989..77b980f599 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -17,7 +17,6 @@ import ( "github.com/docker/docker/opts" runconfigopts "github.com/docker/docker/runconfig/opts" "github.com/docker/go-connections/nat" - shlex "github.com/flynn-archive/go-shlex" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -38,7 +37,7 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { flags := cmd.Flags() flags.String("image", "", "Service image tag") - flags.String("args", "", "Service command args") + flags.Var(&ShlexOpt{}, "args", "Service command args") flags.Bool("rollback", false, "Rollback to previous specification") flags.SetAnnotation("rollback", "version", []string{"1.25"}) flags.Bool("force", false, "Force update even if no changes require it") @@ -258,6 +257,7 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error { updateContainerLabels(flags, &cspec.Labels) updateString("image", &cspec.Image) updateStringToSlice(flags, "args", &cspec.Args) + updateStringToSlice(flags, flagEntrypoint, &cspec.Command) updateEnvironment(flags, &cspec.Env) updateString(flagWorkdir, &cspec.Dir) updateString(flagUser, &cspec.User) @@ -409,15 +409,12 @@ func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error { return nil } -func updateStringToSlice(flags *pflag.FlagSet, flag string, field *[]string) error { +func updateStringToSlice(flags *pflag.FlagSet, flag string, field *[]string) { if !flags.Changed(flag) { - return nil + return } - value, _ := flags.GetString(flag) - valueSlice, err := shlex.Split(value) - *field = valueSlice - return err + *field = flags.Lookup(flag).Value.(*ShlexOpt).Value() } func anyChanged(flags *pflag.FlagSet, fields ...string) bool { From 5387d6dcb0f2f8cb45e227b8a35402b7edd751ed Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Tue, 28 Mar 2017 14:20:25 -0700 Subject: [PATCH 695/978] api: Omit Cluster, Nodes, and Managers from swarm info when unavailable Currently these fields are included in the response JSON with zero values. It's better not to include them if the information is unavailable (for example, on a worker node). This turns Cluster into a pointer so that it can be left out. Signed-off-by: Aaron Lehmann Upstream-commit: e6445629d7f76897b426257f640b1a78cd00e5b4 Component: cli --- components/cli/command/system/info.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/system/info.go b/components/cli/command/system/info.go index 448fc30514..8498dd8c55 100644 --- a/components/cli/command/system/info.go +++ b/components/cli/command/system/info.go @@ -97,7 +97,7 @@ func prettyPrintInfo(dockerCli *command.DockerCli, info types.Info) error { fmt.Fprintf(dockerCli.Out(), " Error: %v\n", info.Swarm.Error) } fmt.Fprintf(dockerCli.Out(), " Is Manager: %v\n", info.Swarm.ControlAvailable) - if info.Swarm.ControlAvailable && info.Swarm.Error == "" && info.Swarm.LocalNodeState != swarm.LocalNodeStateError { + if info.Swarm.Cluster != nil && info.Swarm.ControlAvailable && info.Swarm.Error == "" && info.Swarm.LocalNodeState != swarm.LocalNodeStateError { fmt.Fprintf(dockerCli.Out(), " ClusterID: %s\n", info.Swarm.Cluster.ID) fmt.Fprintf(dockerCli.Out(), " Managers: %d\n", info.Swarm.Managers) fmt.Fprintf(dockerCli.Out(), " Nodes: %d\n", info.Swarm.Nodes) From ac0fab4ce0450923dae210aaf358933e882b8b8b Mon Sep 17 00:00:00 2001 From: Daniel Zhang Date: Wed, 15 Feb 2017 08:21:40 +0800 Subject: [PATCH 696/978] Docker version output is not consistent when there are downgrades or incompatibilities. Signed-off-by: Daniel Zhang Upstream-commit: ce972716be1a5a593aaca7b40de705d74d5f359d Component: cli --- components/cli/command/system/version.go | 50 ++++++++++++++++-------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/components/cli/command/system/version.go b/components/cli/command/system/version.go index 569da21886..468db7d03a 100644 --- a/components/cli/command/system/version.go +++ b/components/cli/command/system/version.go @@ -1,7 +1,6 @@ package system import ( - "fmt" "runtime" "time" @@ -17,7 +16,7 @@ import ( var versionTemplate = `Client: Version: {{.Client.Version}} - API version: {{.Client.APIVersion}} + API version: {{.Client.APIVersion}}{{if ne .Client.APIVersion .Client.DefaultAPIVersion}} (downgraded from {{.Client.DefaultAPIVersion}}){{end}} Go version: {{.Client.GoVersion}} Git commit: {{.Client.GitCommit}} Built: {{.Client.BuildTime}} @@ -36,6 +35,29 @@ type versionOptions struct { format string } +// versionInfo contains version information of both the Client, and Server +type versionInfo struct { + Client clientVersion + Server *types.Version +} + +type clientVersion struct { + Version string + APIVersion string `json:"ApiVersion"` + DefaultAPIVersion string `json:"DefaultAPIVersion,omitempty"` + GitCommit string + GoVersion string + Os string + Arch string + BuildTime string `json:",omitempty"` +} + +// ServerOK returns true when the client could connect to the docker server +// and parse the information received. It returns false otherwise. +func (v versionInfo) ServerOK() bool { + return v.Server != nil +} + // NewVersionCommand creates a new cobra.Command for `docker version` func NewVersionCommand(dockerCli *command.DockerCli) *cobra.Command { var opts versionOptions @@ -70,20 +92,16 @@ func runVersion(dockerCli *command.DockerCli, opts *versionOptions) error { Status: "Template parsing error: " + err.Error()} } - APIVersion := dockerCli.Client().ClientVersion() - if defaultAPIVersion := dockerCli.DefaultVersion(); APIVersion != defaultAPIVersion { - APIVersion = fmt.Sprintf("%s (downgraded from %s)", APIVersion, defaultAPIVersion) - } - - vd := types.VersionResponse{ - Client: &types.Version{ - Version: dockerversion.Version, - APIVersion: APIVersion, - GoVersion: runtime.Version(), - GitCommit: dockerversion.GitCommit, - BuildTime: dockerversion.BuildTime, - Os: runtime.GOOS, - Arch: runtime.GOARCH, + vd := versionInfo{ + Client: clientVersion{ + Version: dockerversion.Version, + APIVersion: dockerCli.Client().ClientVersion(), + DefaultAPIVersion: dockerCli.DefaultVersion(), + GoVersion: runtime.Version(), + GitCommit: dockerversion.GitCommit, + BuildTime: dockerversion.BuildTime, + Os: runtime.GOOS, + Arch: runtime.GOARCH, }, } From 523a49ca3f219ab636cec3fa50d8e5d7d2cf0b5a Mon Sep 17 00:00:00 2001 From: Boaz Shuster Date: Mon, 6 Mar 2017 22:45:12 +0200 Subject: [PATCH 697/978] Use formatter in docker diff Signed-off-by: Boaz Shuster Upstream-commit: 6fd69bd855df7d24cc4b54acc0184d34e5f0e793 Component: cli --- components/cli/command/container/diff.go | 22 ++---- components/cli/command/formatter/diff.go | 72 +++++++++++++++++++ components/cli/command/formatter/diff_test.go | 59 +++++++++++++++ 3 files changed, 136 insertions(+), 17 deletions(-) create mode 100644 components/cli/command/formatter/diff.go create mode 100644 components/cli/command/formatter/diff_test.go diff --git a/components/cli/command/container/diff.go b/components/cli/command/container/diff.go index 95926f5867..816a0a56a3 100644 --- a/components/cli/command/container/diff.go +++ b/components/cli/command/container/diff.go @@ -1,11 +1,9 @@ package container import ( - "fmt" - "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" - "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/cli/command/formatter" "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/net/context" @@ -40,19 +38,9 @@ func runDiff(dockerCli *command.DockerCli, opts *diffOptions) error { if err != nil { return err } - - for _, change := range changes { - var kind string - switch change.Kind { - case archive.ChangeModify: - kind = "C" - case archive.ChangeAdd: - kind = "A" - case archive.ChangeDelete: - kind = "D" - } - fmt.Fprintln(dockerCli.Out(), kind, change.Path) + diffCtx := formatter.Context{ + Output: dockerCli.Out(), + Format: formatter.NewDiffFormat("{{.Type}} {{.Path}}"), } - - return nil + return formatter.DiffWrite(diffCtx, changes) } diff --git a/components/cli/command/formatter/diff.go b/components/cli/command/formatter/diff.go new file mode 100644 index 0000000000..9b4681934c --- /dev/null +++ b/components/cli/command/formatter/diff.go @@ -0,0 +1,72 @@ +package formatter + +import ( + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/pkg/archive" +) + +const ( + defaultDiffTableFormat = "table {{.Type}}\t{{.Path}}" + + changeTypeHeader = "CHANGE TYPE" + pathHeader = "PATH" +) + +// NewDiffFormat returns a format for use with a diff Context +func NewDiffFormat(source string) Format { + switch source { + case TableFormatKey: + return defaultDiffTableFormat + } + return Format(source) +} + +// DiffWrite writes formatted diff using the Context +func DiffWrite(ctx Context, changes []container.ContainerChangeResponseItem) error { + + render := func(format func(subContext subContext) error) error { + for _, change := range changes { + if err := format(&diffContext{c: change}); err != nil { + return err + } + } + return nil + } + return ctx.Write(newDiffContext(), render) +} + +type diffContext struct { + HeaderContext + c container.ContainerChangeResponseItem +} + +func newDiffContext() *diffContext { + diffCtx := diffContext{} + diffCtx.header = map[string]string{ + "Type": changeTypeHeader, + "Path": pathHeader, + } + return &diffCtx +} + +func (d *diffContext) MarshalJSON() ([]byte, error) { + return marshalJSON(d) +} + +func (d *diffContext) Type() string { + var kind string + switch d.c.Kind { + case archive.ChangeModify: + kind = "C" + case archive.ChangeAdd: + kind = "A" + case archive.ChangeDelete: + kind = "D" + } + return kind + +} + +func (d *diffContext) Path() string { + return d.c.Path +} diff --git a/components/cli/command/formatter/diff_test.go b/components/cli/command/formatter/diff_test.go new file mode 100644 index 0000000000..52080354f5 --- /dev/null +++ b/components/cli/command/formatter/diff_test.go @@ -0,0 +1,59 @@ +package formatter + +import ( + "bytes" + "testing" + + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestDiffContextFormatWrite(t *testing.T) { + // Check default output format (verbose and non-verbose mode) for table headers + cases := []struct { + context Context + expected string + }{ + { + Context{Format: NewDiffFormat("table")}, + `CHANGE TYPE PATH +C /var/log/app.log +A /usr/app/app.js +D /usr/app/old_app.js +`, + }, + { + Context{Format: NewDiffFormat("table {{.Path}}")}, + `PATH +/var/log/app.log +/usr/app/app.js +/usr/app/old_app.js +`, + }, + { + Context{Format: NewDiffFormat("{{.Type}}: {{.Path}}")}, + `C: /var/log/app.log +A: /usr/app/app.js +D: /usr/app/old_app.js +`, + }, + } + + diffs := []container.ContainerChangeResponseItem{ + {archive.ChangeModify, "/var/log/app.log"}, + {archive.ChangeAdd, "/usr/app/app.js"}, + {archive.ChangeDelete, "/usr/app/old_app.js"}, + } + + for _, testcase := range cases { + out := bytes.NewBufferString("") + testcase.context.Output = out + err := DiffWrite(testcase.context, diffs) + if err != nil { + assert.Error(t, err, testcase.expected) + } else { + assert.Equal(t, out.String(), testcase.expected) + } + } +} From de8ce4417c5b8525c3f07b3415bcab017209d589 Mon Sep 17 00:00:00 2001 From: Misty Stanley-Jones Date: Fri, 31 Mar 2017 13:22:21 -0700 Subject: [PATCH 698/978] Clarify meaning of docker attach Signed-off-by: Misty Stanley-Jones Upstream-commit: 081ac522bd0e9fdc1f3e0f57159d6ea6613860dd Component: cli --- components/cli/command/container/attach.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/cli/command/container/attach.go b/components/cli/command/container/attach.go index d37cc73603..0564bdcd0f 100644 --- a/components/cli/command/container/attach.go +++ b/components/cli/command/container/attach.go @@ -28,7 +28,7 @@ func NewAttachCommand(dockerCli *command.DockerCli) *cobra.Command { cmd := &cobra.Command{ Use: "attach [OPTIONS] CONTAINER", - Short: "Attach to a running container", + Short: "Attach local standard input, output, and error streams to a running container", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { opts.container = args[0] From ec52e4b8dff6800e100d583785dc6eb53006ec03 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Thu, 16 Feb 2017 17:05:36 -0800 Subject: [PATCH 699/978] Synchronous service create and service update Change "service create" and "service update" to wait until the creation or update finishes, when --detach=false is specified. Show progress bars for the overall operation and for each individual task (when there are a small enough number of tasks), unless "-q" / "--quiet" is specified. Signed-off-by: Aaron Lehmann Upstream-commit: d8ab3840e02bb1f0e1f1df4767eb492fcddaac76 Component: cli --- components/cli/command/service/create.go | 16 +- components/cli/command/service/helpers.go | 39 ++ components/cli/command/service/opts.go | 6 + .../cli/command/service/progress/progress.go | 409 ++++++++++++++++++ components/cli/command/service/update.go | 15 +- 5 files changed, 479 insertions(+), 6 deletions(-) create mode 100644 components/cli/command/service/helpers.go create mode 100644 components/cli/command/service/progress/progress.go diff --git a/components/cli/command/service/create.go b/components/cli/command/service/create.go index 7fd0884930..76b61f6c2e 100644 --- a/components/cli/command/service/create.go +++ b/components/cli/command/service/create.go @@ -7,6 +7,7 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/spf13/cobra" + "github.com/spf13/pflag" "golang.org/x/net/context" ) @@ -22,7 +23,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { if len(args) > 1 { opts.args = args[1:] } - return runCreate(dockerCli, opts) + return runCreate(dockerCli, cmd.Flags(), opts) }, } flags := cmd.Flags() @@ -58,7 +59,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runCreate(dockerCli *command.DockerCli, opts *serviceOptions) error { +func runCreate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *serviceOptions) error { apiClient := dockerCli.Client() createOpts := types.ServiceCreateOptions{} @@ -104,5 +105,14 @@ func runCreate(dockerCli *command.DockerCli, opts *serviceOptions) error { } fmt.Fprintf(dockerCli.Out(), "%s\n", response.ID) - return nil + + if opts.detach { + if !flags.Changed("detach") { + fmt.Fprintln(dockerCli.Err(), "Since --detach=false was not specified, tasks will be created in the background.\n"+ + "In a future release, --detach=false will become the default.") + } + return nil + } + + return waitOnService(ctx, dockerCli, response.ID, opts) } diff --git a/components/cli/command/service/helpers.go b/components/cli/command/service/helpers.go new file mode 100644 index 0000000000..2289369908 --- /dev/null +++ b/components/cli/command/service/helpers.go @@ -0,0 +1,39 @@ +package service + +import ( + "io" + + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/service/progress" + "github.com/docker/docker/pkg/jsonmessage" + "golang.org/x/net/context" +) + +// waitOnService waits for the service to converge. It outputs a progress bar, +// if appopriate based on the CLI flags. +func waitOnService(ctx context.Context, dockerCli *command.DockerCli, serviceID string, opts *serviceOptions) error { + errChan := make(chan error, 1) + pipeReader, pipeWriter := io.Pipe() + + go func() { + errChan <- progress.ServiceProgress(ctx, dockerCli.Client(), serviceID, pipeWriter) + }() + + if opts.quiet { + go func() { + for { + var buf [1024]byte + if _, err := pipeReader.Read(buf[:]); err != nil { + return + } + } + }() + return <-errChan + } + + err := jsonmessage.DisplayJSONMessagesToStream(pipeReader, dockerCli.Out(), nil) + if err == nil { + err = <-errChan + } + return err +} diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index 1ff6575c09..cdfe513177 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -333,6 +333,9 @@ func convertExtraHostsToSwarmHosts(extraHosts []string) []string { } type serviceOptions struct { + detach bool + quiet bool + name string labels opts.ListOpts containerLabels opts.ListOpts @@ -496,6 +499,9 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { // addServiceFlags adds all flags that are common to both `create` and `update`. // Any flags that are not common are added separately in the individual command func addServiceFlags(flags *pflag.FlagSet, opts *serviceOptions) { + flags.BoolVarP(&opts.detach, "detach", "d", true, "Exit immediately instead of waiting for the service to converge") + flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress progress output") + flags.StringVarP(&opts.workdir, flagWorkdir, "w", "", "Working directory inside the container") flags.StringVarP(&opts.user, flagUser, "u", "", "Username or UID (format: [:])") flags.StringVar(&opts.hostname, flagHostname, "", "Container hostname") diff --git a/components/cli/command/service/progress/progress.go b/components/cli/command/service/progress/progress.go new file mode 100644 index 0000000000..ccc7e60cfc --- /dev/null +++ b/components/cli/command/service/progress/progress.go @@ -0,0 +1,409 @@ +package progress + +import ( + "errors" + "fmt" + "io" + "os" + "os/signal" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/client" + "github.com/docker/docker/pkg/progress" + "github.com/docker/docker/pkg/streamformatter" + "github.com/docker/docker/pkg/stringid" + "golang.org/x/net/context" +) + +var ( + numberedStates = map[swarm.TaskState]int64{ + swarm.TaskStateNew: 1, + swarm.TaskStateAllocated: 2, + swarm.TaskStatePending: 3, + swarm.TaskStateAssigned: 4, + swarm.TaskStateAccepted: 5, + swarm.TaskStatePreparing: 6, + swarm.TaskStateReady: 7, + swarm.TaskStateStarting: 8, + swarm.TaskStateRunning: 9, + } + + longestState int +) + +const ( + maxProgress = 9 + maxProgressBars = 20 +) + +type progressUpdater interface { + update(service swarm.Service, tasks []swarm.Task, activeNodes map[string]swarm.Node, rollback bool) (bool, error) +} + +func init() { + for state := range numberedStates { + if len(state) > longestState { + longestState = len(state) + } + } +} + +func stateToProgress(state swarm.TaskState, rollback bool) int64 { + if !rollback { + return numberedStates[state] + } + return int64(len(numberedStates)) - numberedStates[state] +} + +// ServiceProgress outputs progress information for convergence of a service. +func ServiceProgress(ctx context.Context, client client.APIClient, serviceID string, progressWriter io.WriteCloser) error { + defer progressWriter.Close() + + progressOut := streamformatter.NewJSONStreamFormatter().NewProgressOutput(progressWriter, false) + + sigint := make(chan os.Signal, 1) + signal.Notify(sigint, os.Interrupt) + defer signal.Stop(sigint) + + taskFilter := filters.NewArgs() + taskFilter.Add("service", serviceID) + taskFilter.Add("_up-to-date", "true") + + getUpToDateTasks := func() ([]swarm.Task, error) { + return client.TaskList(ctx, types.TaskListOptions{Filters: taskFilter}) + } + + var ( + updater progressUpdater + converged bool + convergedAt time.Time + monitor = 5 * time.Second + rollback bool + ) + + for { + service, _, err := client.ServiceInspectWithRaw(ctx, serviceID) + if err != nil { + return err + } + + if service.Spec.UpdateConfig != nil && service.Spec.UpdateConfig.Monitor != 0 { + monitor = service.Spec.UpdateConfig.Monitor + } + + if updater == nil { + updater, err = initializeUpdater(service, progressOut) + if err != nil { + return err + } + } + + if service.UpdateStatus != nil { + switch service.UpdateStatus.State { + case swarm.UpdateStateUpdating: + rollback = false + case swarm.UpdateStateCompleted: + if !converged { + return nil + } + case swarm.UpdateStatePaused: + return fmt.Errorf("service update paused: %s", service.UpdateStatus.Message) + case swarm.UpdateStateRollbackStarted: + if !rollback && service.UpdateStatus.Message != "" { + progressOut.WriteProgress(progress.Progress{ + ID: "rollback", + Action: service.UpdateStatus.Message, + }) + } + rollback = true + case swarm.UpdateStateRollbackPaused: + return fmt.Errorf("service rollback paused: %s", service.UpdateStatus.Message) + case swarm.UpdateStateRollbackCompleted: + if !converged { + return fmt.Errorf("service rolled back: %s", service.UpdateStatus.Message) + } + } + } + if converged && time.Since(convergedAt) >= monitor { + return nil + } + + tasks, err := getUpToDateTasks() + if err != nil { + return err + } + + activeNodes, err := getActiveNodes(ctx, client) + if err != nil { + return err + } + + converged, err = updater.update(service, tasks, activeNodes, rollback) + if err != nil { + return err + } + if converged { + if convergedAt.IsZero() { + convergedAt = time.Now() + } + wait := monitor - time.Since(convergedAt) + if wait >= 0 { + progressOut.WriteProgress(progress.Progress{ + // Ideally this would have no ID, but + // the progress rendering code behaves + // poorly on an "action" with no ID. It + // returns the cursor to the beginning + // of the line, so the first character + // may be difficult to read. Then the + // output is overwritten by the shell + // prompt when the command finishes. + ID: "verify", + Action: fmt.Sprintf("Waiting %d seconds to verify that tasks are stable...", wait/time.Second+1), + }) + } + } else { + if !convergedAt.IsZero() { + progressOut.WriteProgress(progress.Progress{ + ID: "verify", + Action: "Detected task failure", + }) + } + convergedAt = time.Time{} + } + + select { + case <-time.After(200 * time.Millisecond): + case <-sigint: + if !converged { + progress.Message(progressOut, "", "Operation continuing in background.") + progress.Messagef(progressOut, "", "Use `docker service ps %s` to check progress.", serviceID) + } + return nil + } + } +} + +func getActiveNodes(ctx context.Context, client client.APIClient) (map[string]swarm.Node, error) { + nodes, err := client.NodeList(ctx, types.NodeListOptions{}) + if err != nil { + return nil, err + } + + activeNodes := make(map[string]swarm.Node) + for _, n := range nodes { + if n.Status.State != swarm.NodeStateDown { + activeNodes[n.ID] = n + } + } + return activeNodes, nil +} + +func initializeUpdater(service swarm.Service, progressOut progress.Output) (progressUpdater, error) { + if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil { + return &replicatedProgressUpdater{ + progressOut: progressOut, + }, nil + } + if service.Spec.Mode.Global != nil { + return &globalProgressUpdater{ + progressOut: progressOut, + }, nil + } + return nil, errors.New("unrecognized service mode") +} + +func writeOverallProgress(progressOut progress.Output, numerator, denominator int, rollback bool) { + if rollback { + progressOut.WriteProgress(progress.Progress{ + ID: "overall progress", + Action: fmt.Sprintf("rolling back update: %d out of %d tasks", numerator, denominator), + }) + return + } + progressOut.WriteProgress(progress.Progress{ + ID: "overall progress", + Action: fmt.Sprintf("%d out of %d tasks", numerator, denominator), + }) +} + +type replicatedProgressUpdater struct { + progressOut progress.Output + + // used for maping slots to a contiguous space + // this also causes progress bars to appear in order + slotMap map[int]int + + initialized bool + done bool +} + +func (u *replicatedProgressUpdater) update(service swarm.Service, tasks []swarm.Task, activeNodes map[string]swarm.Node, rollback bool) (bool, error) { + if service.Spec.Mode.Replicated == nil || service.Spec.Mode.Replicated.Replicas == nil { + return false, errors.New("no replica count") + } + replicas := *service.Spec.Mode.Replicated.Replicas + + if !u.initialized { + u.slotMap = make(map[int]int) + + // Draw progress bars in order + writeOverallProgress(u.progressOut, 0, int(replicas), rollback) + + if replicas <= maxProgressBars { + for i := uint64(1); i <= replicas; i++ { + progress.Update(u.progressOut, fmt.Sprintf("%d/%d", i, replicas), " ") + } + } + u.initialized = true + } + + // If there are multiple tasks with the same slot number, favor the one + // with the *lowest* desired state. This can happen in restart + // scenarios. + tasksBySlot := make(map[int]swarm.Task) + for _, task := range tasks { + if numberedStates[task.DesiredState] == 0 { + continue + } + if existingTask, ok := tasksBySlot[task.Slot]; ok { + if numberedStates[existingTask.DesiredState] <= numberedStates[task.DesiredState] { + continue + } + } + if _, nodeActive := activeNodes[task.NodeID]; nodeActive { + tasksBySlot[task.Slot] = task + } + } + + // If we had reached a converged state, check if we are still converged. + if u.done { + for _, task := range tasksBySlot { + if task.Status.State != swarm.TaskStateRunning { + u.done = false + break + } + } + } + + running := uint64(0) + + for _, task := range tasksBySlot { + mappedSlot := u.slotMap[task.Slot] + if mappedSlot == 0 { + mappedSlot = len(u.slotMap) + 1 + u.slotMap[task.Slot] = mappedSlot + } + + if !u.done && replicas <= maxProgressBars && uint64(mappedSlot) <= replicas { + u.progressOut.WriteProgress(progress.Progress{ + ID: fmt.Sprintf("%d/%d", mappedSlot, replicas), + Action: fmt.Sprintf("%-[1]*s", longestState, task.Status.State), + Current: stateToProgress(task.Status.State, rollback), + Total: maxProgress, + HideCounts: true, + }) + } + if task.Status.State == swarm.TaskStateRunning { + running++ + } + } + + if !u.done { + writeOverallProgress(u.progressOut, int(running), int(replicas), rollback) + + if running == replicas { + u.done = true + } + } + + return running == replicas, nil +} + +type globalProgressUpdater struct { + progressOut progress.Output + + initialized bool + done bool +} + +func (u *globalProgressUpdater) update(service swarm.Service, tasks []swarm.Task, activeNodes map[string]swarm.Node, rollback bool) (bool, error) { + // If there are multiple tasks with the same node ID, favor the one + // with the *lowest* desired state. This can happen in restart + // scenarios. + tasksByNode := make(map[string]swarm.Task) + for _, task := range tasks { + if numberedStates[task.DesiredState] == 0 { + continue + } + if existingTask, ok := tasksByNode[task.NodeID]; ok { + if numberedStates[existingTask.DesiredState] <= numberedStates[task.DesiredState] { + continue + } + } + tasksByNode[task.NodeID] = task + } + + // We don't have perfect knowledge of how many nodes meet the + // constraints for this service. But the orchestrator creates tasks + // for all eligible nodes at the same time, so we should see all those + // nodes represented among the up-to-date tasks. + nodeCount := len(tasksByNode) + + if !u.initialized { + if nodeCount == 0 { + // Two possibilities: either the orchestrator hasn't created + // the tasks yet, or the service doesn't meet constraints for + // any node. Either way, we wait. + u.progressOut.WriteProgress(progress.Progress{ + ID: "overall progress", + Action: "waiting for new tasks", + }) + return false, nil + } + + writeOverallProgress(u.progressOut, 0, nodeCount, rollback) + u.initialized = true + } + + // If we had reached a converged state, check if we are still converged. + if u.done { + for _, task := range tasksByNode { + if task.Status.State != swarm.TaskStateRunning { + u.done = false + break + } + } + } + + running := 0 + + for _, task := range tasksByNode { + if node, nodeActive := activeNodes[task.NodeID]; nodeActive { + if !u.done && nodeCount <= maxProgressBars { + u.progressOut.WriteProgress(progress.Progress{ + ID: stringid.TruncateID(node.ID), + Action: fmt.Sprintf("%-[1]*s", longestState, task.Status.State), + Current: stateToProgress(task.Status.State, rollback), + Total: maxProgress, + HideCounts: true, + }) + } + if task.Status.State == swarm.TaskStateRunning { + running++ + } + } + } + + if !u.done { + writeOverallProgress(u.progressOut, running, nodeCount, rollback) + + if running == nodeCount { + u.done = true + } + } + + return running == nodeCount, nil +} diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index 77b980f599..afa0f807e9 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -31,7 +31,7 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Update a service", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return runUpdate(dockerCli, cmd.Flags(), args[0]) + return runUpdate(dockerCli, cmd.Flags(), serviceOpts, args[0]) }, } @@ -93,7 +93,7 @@ func newListOptsVar() *opts.ListOpts { return opts.NewListOptsRef(&[]string{}, nil) } -func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID string) error { +func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *serviceOptions, serviceID string) error { apiClient := dockerCli.Client() ctx := context.Background() @@ -195,7 +195,16 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID str } fmt.Fprintf(dockerCli.Out(), "%s\n", serviceID) - return nil + + if opts.detach { + if !flags.Changed("detach") { + fmt.Fprintln(dockerCli.Err(), "Since --detach=false was not specified, tasks will be updated in the background.\n"+ + "In a future release, --detach=false will become the default.") + } + return nil + } + + return waitOnService(ctx, dockerCli, serviceID, opts) } func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error { From 9828b12c06237a71612c392e3d147d8bc51cc096 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 3 Apr 2017 17:42:16 -0400 Subject: [PATCH 700/978] Fix endpoint mode in Compose format. Signed-off-by: Daniel Nephin Upstream-commit: 64c6b9a938df7b6d32ee4a4dda36b88a46e3f087 Component: cli --- components/cli/compose/loader/full-example.yml | 3 ++- components/cli/compose/loader/loader.go | 4 +--- components/cli/compose/loader/loader_test.go | 1 + components/cli/compose/types/types.go | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/components/cli/compose/loader/full-example.yml b/components/cli/compose/loader/full-example.yml index e8f3716013..3ffbcc3ea1 100644 --- a/components/cli/compose/loader/full-example.yml +++ b/components/cli/compose/loader/full-example.yml @@ -1,4 +1,4 @@ -version: "3" +version: "3.2" services: foo: @@ -45,6 +45,7 @@ services: window: 120s placement: constraints: [node=foo] + endpoint_mode: dnsrr devices: - "/dev/ttyUSB0:/dev/ttyUSB0" diff --git a/components/cli/compose/loader/loader.go b/components/cli/compose/loader/loader.go index d69b530e6c..2394ff8e2f 100644 --- a/components/cli/compose/loader/loader.go +++ b/components/cli/compose/loader/loader.go @@ -220,9 +220,7 @@ func transform(source map[string]interface{}, target interface{}) error { if err != nil { return err } - err = decoder.Decode(source) - // TODO: log unused keys - return err + return decoder.Decode(source) } func transformHook( diff --git a/components/cli/compose/loader/loader_test.go b/components/cli/compose/loader/loader_test.go index e7e2992ada..9e042d0a12 100644 --- a/components/cli/compose/loader/loader_test.go +++ b/components/cli/compose/loader/loader_test.go @@ -674,6 +674,7 @@ func TestFullExample(t *testing.T) { Placement: types.Placement{ Constraints: []string{"node=foo"}, }, + EndpointMode: "dnsrr", }, Devices: []string{"/dev/ttyUSB0:/dev/ttyUSB0"}, DNS: []string{"8.8.8.8", "9.9.9.9"}, diff --git a/components/cli/compose/types/types.go b/components/cli/compose/types/types.go index 19500e195d..1b4c4015ec 100644 --- a/components/cli/compose/types/types.go +++ b/components/cli/compose/types/types.go @@ -158,7 +158,7 @@ type DeployConfig struct { Resources Resources RestartPolicy *RestartPolicy `mapstructure:"restart_policy"` Placement Placement - EndpointMode string + EndpointMode string `mapstructure:"endpoint_mode"` } // HealthCheckConfig the healthcheck configuration for a service From f9c12f483ad0b643d31f8066dd8ae23be192c5f2 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Tue, 7 Feb 2017 22:51:33 -0800 Subject: [PATCH 701/978] Add `PORTS` field for `docker service ls` (`ingress`) This fix is related to 30232 wherw `docker service ls` does not show `PORTS` information like `docker service ps`. This fix adds `PORTS` fields for services that publish ports in ingress mode. Additional unit tests cases have been updated. This fix is related to 30232. Signed-off-by: Yong Tang Upstream-commit: e0d4e672a1cb8cce18fa956be52dc2fb0c149673 Component: cli --- components/cli/command/formatter/service.go | 23 +++++- .../cli/command/formatter/service_test.go | 80 ++++++++++++++++--- 2 files changed, 92 insertions(+), 11 deletions(-) diff --git a/components/cli/command/formatter/service.go b/components/cli/command/formatter/service.go index 4a4bae2cff..740bd8d53f 100644 --- a/components/cli/command/formatter/service.go +++ b/components/cli/command/formatter/service.go @@ -1,6 +1,7 @@ package formatter import ( + "fmt" "strings" "time" @@ -391,7 +392,7 @@ func (ctx *serviceInspectContext) Ports() []swarm.PortConfig { } const ( - defaultServiceTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Mode}}\t{{.Replicas}}\t{{.Image}}" + defaultServiceTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Mode}}\t{{.Replicas}}\t{{.Image}}\t{{.Ports}}" serviceIDHeader = "ID" modeHeader = "MODE" @@ -410,7 +411,7 @@ func NewServiceListFormat(source string, quiet bool) Format { if quiet { return `id: {{.ID}}` } - return `id: {{.ID}}\nname: {{.Name}}\nmode: {{.Mode}}\nreplicas: {{.Replicas}}\nimage: {{.Image}}\n` + return `id: {{.ID}}\nname: {{.Name}}\nmode: {{.Mode}}\nreplicas: {{.Replicas}}\nimage: {{.Image}}\nports: {{.Ports}}\n` } return Format(source) } @@ -439,6 +440,7 @@ func ServiceListWrite(ctx Context, services []swarm.Service, info map[string]Ser "Mode": modeHeader, "Replicas": replicasHeader, "Image": imageHeader, + "Ports": portsHeader, } return ctx.Write(&serviceCtx, render) } @@ -483,3 +485,20 @@ func (c *serviceContext) Image() string { return image } + +func (c *serviceContext) Ports() string { + if c.service.Spec.EndpointSpec == nil || c.service.Spec.EndpointSpec.Ports == nil { + return "" + } + ports := []string{} + for _, pConfig := range c.service.Spec.EndpointSpec.Ports { + if pConfig.PublishMode == swarm.PortConfigPublishModeIngress { + ports = append(ports, fmt.Sprintf("*:%d->%d/%s", + pConfig.PublishedPort, + pConfig.TargetPort, + pConfig.Protocol, + )) + } + } + return strings.Join(ports, ",") +} diff --git a/components/cli/command/formatter/service_test.go b/components/cli/command/formatter/service_test.go index d4474297db..93ffc92a3b 100644 --- a/components/cli/command/formatter/service_test.go +++ b/components/cli/command/formatter/service_test.go @@ -29,9 +29,9 @@ func TestServiceContextWrite(t *testing.T) { // Table format { Context{Format: NewServiceListFormat("table", false)}, - `ID NAME MODE REPLICAS IMAGE -id_baz baz global 2/4 -id_bar bar replicated 2/4 + `ID NAME MODE REPLICAS IMAGE PORTS +id_baz baz global 2/4 *:80->8080/tcp +id_bar bar replicated 2/4 *:80->8080/tcp `, }, { @@ -62,12 +62,14 @@ name: baz mode: global replicas: 2/4 image: +ports: *:80->8080/tcp id: id_bar name: bar mode: replicated replicas: 2/4 image: +ports: *:80->8080/tcp `, }, @@ -88,8 +90,38 @@ bar for _, testcase := range cases { services := []swarm.Service{ - {ID: "id_baz", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "baz"}}}, - {ID: "id_bar", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "bar"}}}, + { + ID: "id_baz", + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{Name: "baz"}, + EndpointSpec: &swarm.EndpointSpec{ + Ports: []swarm.PortConfig{ + { + PublishMode: "ingress", + PublishedPort: 80, + TargetPort: 8080, + Protocol: "tcp", + }, + }, + }, + }, + }, + { + ID: "id_bar", + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{Name: "bar"}, + EndpointSpec: &swarm.EndpointSpec{ + Ports: []swarm.PortConfig{ + { + PublishMode: "ingress", + PublishedPort: 80, + TargetPort: 8080, + Protocol: "tcp", + }, + }, + }, + }, + }, } info := map[string]ServiceListInfo{ "id_baz": { @@ -114,8 +146,38 @@ bar func TestServiceContextWriteJSON(t *testing.T) { services := []swarm.Service{ - {ID: "id_baz", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "baz"}}}, - {ID: "id_bar", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "bar"}}}, + { + ID: "id_baz", + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{Name: "baz"}, + EndpointSpec: &swarm.EndpointSpec{ + Ports: []swarm.PortConfig{ + { + PublishMode: "ingress", + PublishedPort: 80, + TargetPort: 8080, + Protocol: "tcp", + }, + }, + }, + }, + }, + { + ID: "id_bar", + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{Name: "bar"}, + EndpointSpec: &swarm.EndpointSpec{ + Ports: []swarm.PortConfig{ + { + PublishMode: "ingress", + PublishedPort: 80, + TargetPort: 8080, + Protocol: "tcp", + }, + }, + }, + }, + }, } info := map[string]ServiceListInfo{ "id_baz": { @@ -128,8 +190,8 @@ func TestServiceContextWriteJSON(t *testing.T) { }, } expectedJSONs := []map[string]interface{}{ - {"ID": "id_baz", "Name": "baz", "Mode": "global", "Replicas": "2/4", "Image": ""}, - {"ID": "id_bar", "Name": "bar", "Mode": "replicated", "Replicas": "2/4", "Image": ""}, + {"ID": "id_baz", "Name": "baz", "Mode": "global", "Replicas": "2/4", "Image": "", "Ports": "*:80->8080/tcp"}, + {"ID": "id_bar", "Name": "bar", "Mode": "replicated", "Replicas": "2/4", "Image": "", "Ports": "*:80->8080/tcp"}, } out := bytes.NewBufferString("") From 1ea7589fcbcbd13d81cac2be78f2c7bfaec8d504 Mon Sep 17 00:00:00 2001 From: Drew Erny Date: Tue, 21 Mar 2017 11:35:55 -0700 Subject: [PATCH 702/978] 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 Upstream-commit: b4ca6ebb098b78da7e4a697232ac9eaa4ddc568c Component: cli --- components/cli/command/service/logs.go | 69 ++++++++++++++++++-------- 1 file changed, 48 insertions(+), 21 deletions(-) diff --git a/components/cli/command/service/logs.go b/components/cli/command/service/logs.go index 1bf5723ae0..da2374f9dd 100644 --- a/components/cli/command/service/logs.go +++ b/components/cli/command/service/logs.go @@ -30,9 +30,14 @@ type logsOptions struct { timestamps bool tail string - service string + target string } +// TODO(dperny) the whole CLI for this is kind of a mess IMHOIRL and it needs +// to be refactored agressively. There may be changes to the implementation of +// details, which will be need to be reflected in this code. The refactoring +// should be put off until we make those changes, tho, because I think the +// decisions made WRT details will impact the design of the CLI. func newLogsCommand(dockerCli *command.DockerCli) *cobra.Command { var opts logsOptions @@ -41,16 +46,16 @@ func newLogsCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Fetch the logs of a service", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - opts.service = args[0] + opts.target = args[0] return runLogs(dockerCli, &opts) }, Tags: map[string]string{"experimental": ""}, } flags := cmd.Flags() - flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names") + flags.BoolVar(&opts.noResolve, "no-resolve", false, "Do not map IDs to Names in output") flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Do not truncate output") - flags.BoolVar(&opts.noTaskIDs, "no-task-ids", false, "Do not include task IDs") + flags.BoolVar(&opts.noTaskIDs, "no-task-ids", false, "Do not include task IDs in output") flags.BoolVarP(&opts.follow, "follow", "f", false, "Follow log output") flags.StringVar(&opts.since, "since", "", "Show logs since timestamp (e.g. 2013-01-02T13:23:37) or relative (e.g. 42m for 42 minutes)") flags.BoolVarP(&opts.timestamps, "timestamps", "t", false, "Show timestamps") @@ -70,28 +75,44 @@ func runLogs(dockerCli *command.DockerCli, opts *logsOptions) error { Tail: opts.tail, } - client := dockerCli.Client() + cli := dockerCli.Client() - service, _, err := client.ServiceInspectWithRaw(ctx, opts.service) - if err != nil { - return err - } + var ( + maxLength = 1 + responseBody io.ReadCloser + ) - responseBody, err := client.ServiceLogs(ctx, opts.service, options) + service, _, err := cli.ServiceInspectWithRaw(ctx, opts.target) if err != nil { - return err + // if it's any error other than service not found, it's Real + if !client.IsErrServiceNotFound(err) { + return err + } + task, _, err := cli.TaskInspectWithRaw(ctx, opts.target) + if err != nil { + if client.IsErrTaskNotFound(err) { + // if the task ALSO isn't found, rewrite the error to be clear + // that we looked for services AND tasks + err = fmt.Errorf("No such task or service") + } + return err + } + maxLength = getMaxLength(task.Slot) + responseBody, err = cli.TaskLogs(ctx, opts.target, options) + } else { + responseBody, err = cli.ServiceLogs(ctx, opts.target, options) + if err != nil { + return err + } + if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil { + // if replicas are initialized, figure out if we need to pad them + replicas := *service.Spec.Mode.Replicated.Replicas + maxLength = getMaxLength(int(replicas)) + } } defer responseBody.Close() - var replicas uint64 - padding := 1 - if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil { - // if replicas are initialized, figure out if we need to pad them - replicas = *service.Spec.Mode.Replicated.Replicas - padding = len(strconv.FormatUint(replicas, 10)) - } - - taskFormatter := newTaskFormatter(client, opts, padding) + taskFormatter := newTaskFormatter(cli, opts, maxLength) stdout := &logWriter{ctx: ctx, opts: opts, f: taskFormatter, w: dockerCli.Out()} stderr := &logWriter{ctx: ctx, opts: opts, f: taskFormatter, w: dockerCli.Err()} @@ -101,6 +122,11 @@ func runLogs(dockerCli *command.DockerCli, opts *logsOptions) error { return err } +// getMaxLength gets the maximum length of the number in base 10 +func getMaxLength(i int) int { + return len(strconv.FormatInt(int64(i), 10)) +} + type taskFormatter struct { client client.APIClient opts *logsOptions @@ -148,7 +174,8 @@ func (f *taskFormatter) format(ctx context.Context, logCtx logContext) (string, taskName += fmt.Sprintf(".%s", stringid.TruncateID(task.ID)) } } - padding := strings.Repeat(" ", f.padding-len(strconv.FormatInt(int64(task.Slot), 10))) + + padding := strings.Repeat(" ", f.padding-getMaxLength(task.Slot)) formatted := fmt.Sprintf("%s@%s%s", taskName, nodeName, padding) f.cache[logCtx] = formatted return formatted, nil From ba8766363d4056931fba8b33dd3cde269ec5c818 Mon Sep 17 00:00:00 2001 From: Drew Erny Date: Tue, 21 Mar 2017 11:35:55 -0700 Subject: [PATCH 703/978] 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 Upstream-commit: 4dcceaf70eddee3c0b144b475e05dc14c514b2b6 Component: cli --- components/cli/interface.go | 1 + components/cli/task_logs.go | 52 +++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 components/cli/task_logs.go diff --git a/components/cli/interface.go b/components/cli/interface.go index ae4146bb4a..6f8c094b31 100644 --- a/components/cli/interface.go +++ b/components/cli/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/components/cli/task_logs.go b/components/cli/task_logs.go new file mode 100644 index 0000000000..2ed19543a4 --- /dev/null +++ b/components/cli/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 bb53f2056e3126ee2524a4fb745c90023ab40992 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Tue, 24 Jan 2017 13:17:40 -0800 Subject: [PATCH 704/978] Add `--format` for `docker node ls` This fix tries to address the comment https://github.com/docker/docker/pull/30376#discussion_r97465334 where it was not possible to specify `--format` for `docker node ls`. The `--format` flag is a quite useful flag that could be used in many places such as completion. This fix implements `--format` for `docker node ls` and add `nodesFormat` in config.json so that it is possible to specify the output when `docker node ls` is invoked. Related documentations have been updated. A set of unit tests have been added. This fix is related to #30376. Signed-off-by: Yong Tang Upstream-commit: 4fc1d6782ce86b0e783aaee34b5540a5b446e3ba Component: cli --- components/cli/command/formatter/node.go | 99 +++++++++ components/cli/command/formatter/node_test.go | 188 ++++++++++++++++++ components/cli/command/node/list.go | 70 ++----- components/cli/command/node/list_test.go | 129 ++++++++---- components/cli/config/configfile/file.go | 1 + 5 files changed, 397 insertions(+), 90 deletions(-) create mode 100644 components/cli/command/formatter/node.go create mode 100644 components/cli/command/formatter/node_test.go diff --git a/components/cli/command/formatter/node.go b/components/cli/command/formatter/node.go new file mode 100644 index 0000000000..bd478e57f2 --- /dev/null +++ b/components/cli/command/formatter/node.go @@ -0,0 +1,99 @@ +package formatter + +import ( + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/command" +) + +const ( + defaultNodeTableFormat = "table {{.ID}}\t{{.Hostname}}\t{{.Status}}\t{{.Availability}}\t{{.ManagerStatus}}" + + nodeIDHeader = "ID" + hostnameHeader = "HOSTNAME" + availabilityHeader = "AVAILABILITY" + managerStatusHeader = "MANAGER STATUS" +) + +// NewNodeFormat returns a Format for rendering using a node Context +func NewNodeFormat(source string, quiet bool) Format { + switch source { + case TableFormatKey: + if quiet { + return defaultQuietFormat + } + return defaultNodeTableFormat + case RawFormatKey: + if quiet { + return `node_id: {{.ID}}` + } + return `node_id: {{.ID}}\nhostname: {{.Hostname}}\nstatus: {{.Status}}\navailability: {{.Availability}}\nmanager_status: {{.ManagerStatus}}\n` + } + return Format(source) +} + +// NodeWrite writes the context +func NodeWrite(ctx Context, nodes []swarm.Node, info types.Info) error { + render := func(format func(subContext subContext) error) error { + for _, node := range nodes { + nodeCtx := &nodeContext{n: node, info: info} + if err := format(nodeCtx); err != nil { + return err + } + } + return nil + } + nodeCtx := nodeContext{} + nodeCtx.header = nodeHeaderContext{ + "ID": nodeIDHeader, + "Hostname": hostnameHeader, + "Status": statusHeader, + "Availability": availabilityHeader, + "ManagerStatus": managerStatusHeader, + } + return ctx.Write(&nodeCtx, render) +} + +type nodeHeaderContext map[string]string + +type nodeContext struct { + HeaderContext + n swarm.Node + info types.Info +} + +func (c *nodeContext) MarshalJSON() ([]byte, error) { + return marshalJSON(c) +} + +func (c *nodeContext) ID() string { + nodeID := c.n.ID + if nodeID == c.info.Swarm.NodeID { + nodeID = nodeID + " *" + } + return nodeID +} + +func (c *nodeContext) Hostname() string { + return c.n.Description.Hostname +} + +func (c *nodeContext) Status() string { + return command.PrettyPrint(string(c.n.Status.State)) +} + +func (c *nodeContext) Availability() string { + return command.PrettyPrint(string(c.n.Spec.Availability)) +} + +func (c *nodeContext) ManagerStatus() string { + reachability := "" + if c.n.ManagerStatus != nil { + if c.n.ManagerStatus.Leader { + reachability = "Leader" + } else { + reachability = string(c.n.ManagerStatus.Reachability) + } + } + return command.PrettyPrint(reachability) +} diff --git a/components/cli/command/formatter/node_test.go b/components/cli/command/formatter/node_test.go new file mode 100644 index 0000000000..e3e341fc8b --- /dev/null +++ b/components/cli/command/formatter/node_test.go @@ -0,0 +1,188 @@ +package formatter + +import ( + "bytes" + "encoding/json" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestNodeContext(t *testing.T) { + nodeID := stringid.GenerateRandomID() + + var ctx nodeContext + cases := []struct { + nodeCtx nodeContext + expValue string + call func() string + }{ + {nodeContext{ + n: swarm.Node{ID: nodeID}, + }, nodeID, ctx.ID}, + {nodeContext{ + n: swarm.Node{Description: swarm.NodeDescription{Hostname: "node_hostname"}}, + }, "node_hostname", ctx.Hostname}, + {nodeContext{ + n: swarm.Node{Status: swarm.NodeStatus{State: swarm.NodeState("foo")}}, + }, "Foo", ctx.Status}, + {nodeContext{ + n: swarm.Node{Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("drain")}}, + }, "Drain", ctx.Availability}, + {nodeContext{ + n: swarm.Node{ManagerStatus: &swarm.ManagerStatus{Leader: true}}, + }, "Leader", ctx.ManagerStatus}, + } + + for _, c := range cases { + ctx = c.nodeCtx + v := c.call() + if strings.Contains(v, ",") { + compareMultipleValues(t, v, c.expValue) + } else if v != c.expValue { + t.Fatalf("Expected %s, was %s\n", c.expValue, v) + } + } +} + +func TestNodeContextWrite(t *testing.T) { + cases := []struct { + context Context + expected string + }{ + + // Errors + { + Context{Format: "{{InvalidFunction}}"}, + `Template parsing error: template: :1: function "InvalidFunction" not defined +`, + }, + { + Context{Format: "{{nil}}"}, + `Template parsing error: template: :1:2: executing "" at : nil is not a command +`, + }, + // Table format + { + Context{Format: NewNodeFormat("table", false)}, + `ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS +nodeID1 foobar_baz Foo Drain Leader +nodeID2 foobar_bar Bar Active Reachable +`, + }, + { + Context{Format: NewNodeFormat("table", true)}, + `nodeID1 +nodeID2 +`, + }, + { + Context{Format: NewNodeFormat("table {{.Hostname}}", false)}, + `HOSTNAME +foobar_baz +foobar_bar +`, + }, + { + Context{Format: NewNodeFormat("table {{.Hostname}}", true)}, + `HOSTNAME +foobar_baz +foobar_bar +`, + }, + // Raw Format + { + Context{Format: NewNodeFormat("raw", false)}, + `node_id: nodeID1 +hostname: foobar_baz +status: Foo +availability: Drain +manager_status: Leader + +node_id: nodeID2 +hostname: foobar_bar +status: Bar +availability: Active +manager_status: Reachable + +`, + }, + { + Context{Format: NewNodeFormat("raw", true)}, + `node_id: nodeID1 +node_id: nodeID2 +`, + }, + // Custom Format + { + Context{Format: NewNodeFormat("{{.Hostname}}", false)}, + `foobar_baz +foobar_bar +`, + }, + } + + for _, testcase := range cases { + nodes := []swarm.Node{ + {ID: "nodeID1", Description: swarm.NodeDescription{Hostname: "foobar_baz"}, Status: swarm.NodeStatus{State: swarm.NodeState("foo")}, Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("drain")}, ManagerStatus: &swarm.ManagerStatus{Leader: true}}, + {ID: "nodeID2", Description: swarm.NodeDescription{Hostname: "foobar_bar"}, Status: swarm.NodeStatus{State: swarm.NodeState("bar")}, Spec: swarm.NodeSpec{Availability: swarm.NodeAvailability("active")}, ManagerStatus: &swarm.ManagerStatus{Leader: false, Reachability: swarm.Reachability("Reachable")}}, + } + out := bytes.NewBufferString("") + testcase.context.Output = out + err := NodeWrite(testcase.context, nodes, types.Info{}) + if err != nil { + assert.Error(t, err, testcase.expected) + } else { + assert.Equal(t, out.String(), testcase.expected) + } + } +} + +func TestNodeContextWriteJSON(t *testing.T) { + nodes := []swarm.Node{ + {ID: "nodeID1", Description: swarm.NodeDescription{Hostname: "foobar_baz"}}, + {ID: "nodeID2", Description: swarm.NodeDescription{Hostname: "foobar_bar"}}, + } + expectedJSONs := []map[string]interface{}{ + {"Availability": "", "Hostname": "foobar_baz", "ID": "nodeID1", "ManagerStatus": "", "Status": ""}, + {"Availability": "", "Hostname": "foobar_bar", "ID": "nodeID2", "ManagerStatus": "", "Status": ""}, + } + + out := bytes.NewBufferString("") + err := NodeWrite(Context{Format: "{{json .}}", Output: out}, nodes, types.Info{}) + if err != nil { + t.Fatal(err) + } + for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { + t.Logf("Output: line %d: %s", i, line) + var m map[string]interface{} + if err := json.Unmarshal([]byte(line), &m); err != nil { + t.Fatal(err) + } + assert.DeepEqual(t, m, expectedJSONs[i]) + } +} + +func TestNodeContextWriteJSONField(t *testing.T) { + nodes := []swarm.Node{ + {ID: "nodeID1", Description: swarm.NodeDescription{Hostname: "foobar_baz"}}, + {ID: "nodeID2", Description: swarm.NodeDescription{Hostname: "foobar_bar"}}, + } + out := bytes.NewBufferString("") + err := NodeWrite(Context{Format: "{{json .ID}}", Output: out}, nodes, types.Info{}) + if err != nil { + t.Fatal(err) + } + for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { + t.Logf("Output: line %d: %s", i, line) + var s string + if err := json.Unmarshal([]byte(line), &s); err != nil { + t.Fatal(err) + } + assert.Equal(t, s, nodes[i].ID) + } +} diff --git a/components/cli/command/node/list.go b/components/cli/command/node/list.go index d166401ab7..9c6224dd19 100644 --- a/components/cli/command/node/list.go +++ b/components/cli/command/node/list.go @@ -1,26 +1,19 @@ package node import ( - "fmt" - "io" - "text/tabwriter" - "golang.org/x/net/context" "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/formatter" "github.com/docker/docker/opts" "github.com/spf13/cobra" ) -const ( - listItemFmt = "%s\t%s\t%s\t%s\t%s\n" -) - type listOptions struct { quiet bool + format string filter opts.FilterOpt } @@ -38,6 +31,7 @@ func newListCommand(dockerCli command.Cli) *cobra.Command { } flags := cmd.Flags() flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs") + flags.StringVar(&opts.format, "format", "", "Pretty-print nodes using a Go template") flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") return cmd @@ -45,7 +39,6 @@ func newListCommand(dockerCli command.Cli) *cobra.Command { func runList(dockerCli command.Cli, opts listOptions) error { client := dockerCli.Client() - out := dockerCli.Out() ctx := context.Background() nodes, err := client.NodeList( @@ -55,61 +48,26 @@ func runList(dockerCli command.Cli, opts listOptions) error { return err } + info := types.Info{} if len(nodes) > 0 && !opts.quiet { // only non-empty nodes and not quiet, should we call /info api - info, err := client.Info(ctx) + info, err = client.Info(ctx) if err != nil { return err } - printTable(out, nodes, info) - } else if !opts.quiet { - // no nodes and not quiet, print only one line with columns ID, HOSTNAME, ... - printTable(out, nodes, types.Info{}) - } else { - printQuiet(out, nodes) } - return nil -} - -func printTable(out io.Writer, nodes []swarm.Node, info types.Info) { - writer := tabwriter.NewWriter(out, 0, 4, 2, ' ', 0) - - // Ignore flushing errors - defer writer.Flush() - - fmt.Fprintf(writer, listItemFmt, "ID", "HOSTNAME", "STATUS", "AVAILABILITY", "MANAGER STATUS") - for _, node := range nodes { - name := node.Description.Hostname - availability := string(node.Spec.Availability) - - reachability := "" - if node.ManagerStatus != nil { - if node.ManagerStatus.Leader { - reachability = "Leader" - } else { - reachability = string(node.ManagerStatus.Reachability) - } + format := opts.format + if len(format) == 0 { + format = formatter.TableFormatKey + if len(dockerCli.ConfigFile().NodesFormat) > 0 && !opts.quiet { + format = dockerCli.ConfigFile().NodesFormat } - - ID := node.ID - if node.ID == info.Swarm.NodeID { - ID = ID + " *" - } - - fmt.Fprintf( - writer, - listItemFmt, - ID, - name, - command.PrettyPrint(string(node.Status.State)), - command.PrettyPrint(availability), - command.PrettyPrint(reachability)) } -} -func printQuiet(out io.Writer, nodes []swarm.Node) { - for _, node := range nodes { - fmt.Fprintln(out, node.ID) + nodesCtx := formatter.Context{ + Output: dockerCli.Out(), + Format: formatter.NewNodeFormat(format, opts.quiet), } + return formatter.NodeWrite(nodesCtx, nodes, info) } diff --git a/components/cli/command/node/list_test.go b/components/cli/command/node/list_test.go index 7b657cd73c..13a21d1b52 100644 --- a/components/cli/command/node/list_test.go +++ b/components/cli/command/node/list_test.go @@ -7,6 +7,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/config/configfile" "github.com/docker/docker/cli/internal/test" "github.com/pkg/errors" // Import builders to get the builder function as package function @@ -42,11 +43,12 @@ func TestNodeListErrorOnAPIFailure(t *testing.T) { } for _, tc := range testCases { buf := new(bytes.Buffer) - cmd := newListCommand( - test.NewFakeCli(&fakeClient{ - nodeListFunc: tc.nodeListFunc, - infoFunc: tc.infoFunc, - }, buf)) + cli := test.NewFakeCli(&fakeClient{ + nodeListFunc: tc.nodeListFunc, + infoFunc: tc.infoFunc, + }, buf) + cli.SetConfigfile(&configfile.ConfigFile{}) + cmd := newListCommand(cli) cmd.SetOutput(ioutil.Discard) assert.Error(t, cmd.Execute(), tc.expectedError) } @@ -54,39 +56,41 @@ func TestNodeListErrorOnAPIFailure(t *testing.T) { func TestNodeList(t *testing.T) { buf := new(bytes.Buffer) - cmd := newListCommand( - test.NewFakeCli(&fakeClient{ - nodeListFunc: func() ([]swarm.Node, error) { - return []swarm.Node{ - *Node(NodeID("nodeID1"), Hostname("nodeHostname1"), Manager(Leader())), - *Node(NodeID("nodeID2"), Hostname("nodeHostname2"), Manager()), - *Node(NodeID("nodeID3"), Hostname("nodeHostname3")), - }, nil - }, - infoFunc: func() (types.Info, error) { - return types.Info{ - Swarm: swarm.Info{ - NodeID: "nodeID1", - }, - }, nil - }, - }, buf)) + cli := test.NewFakeCli(&fakeClient{ + nodeListFunc: func() ([]swarm.Node, error) { + return []swarm.Node{ + *Node(NodeID("nodeID1"), Hostname("nodeHostname1"), Manager(Leader())), + *Node(NodeID("nodeID2"), Hostname("nodeHostname2"), Manager()), + *Node(NodeID("nodeID3"), Hostname("nodeHostname3")), + }, nil + }, + infoFunc: func() (types.Info, error) { + return types.Info{ + Swarm: swarm.Info{ + NodeID: "nodeID1", + }, + }, nil + }, + }, buf) + cli.SetConfigfile(&configfile.ConfigFile{}) + cmd := newListCommand(cli) assert.NilError(t, cmd.Execute()) - assert.Contains(t, buf.String(), `nodeID1 * nodeHostname1 Ready Active Leader`) - assert.Contains(t, buf.String(), `nodeID2 nodeHostname2 Ready Active Reachable`) - assert.Contains(t, buf.String(), `nodeID3 nodeHostname3 Ready Active`) + assert.Contains(t, buf.String(), `nodeID1 * nodeHostname1 Ready Active Leader`) + assert.Contains(t, buf.String(), `nodeID2 nodeHostname2 Ready Active Reachable`) + assert.Contains(t, buf.String(), `nodeID3 nodeHostname3 Ready Active`) } func TestNodeListQuietShouldOnlyPrintIDs(t *testing.T) { buf := new(bytes.Buffer) - cmd := newListCommand( - test.NewFakeCli(&fakeClient{ - nodeListFunc: func() ([]swarm.Node, error) { - return []swarm.Node{ - *Node(), - }, nil - }, - }, buf)) + cli := test.NewFakeCli(&fakeClient{ + nodeListFunc: func() ([]swarm.Node, error) { + return []swarm.Node{ + *Node(), + }, nil + }, + }, buf) + cli.SetConfigfile(&configfile.ConfigFile{}) + cmd := newListCommand(cli) cmd.Flags().Set("quiet", "true") assert.NilError(t, cmd.Execute()) assert.Contains(t, buf.String(), "nodeID") @@ -95,7 +99,64 @@ func TestNodeListQuietShouldOnlyPrintIDs(t *testing.T) { // Test case for #24090 func TestNodeListContainsHostname(t *testing.T) { buf := new(bytes.Buffer) - cmd := newListCommand(test.NewFakeCli(&fakeClient{}, buf)) + cli := test.NewFakeCli(&fakeClient{}, buf) + cli.SetConfigfile(&configfile.ConfigFile{}) + cmd := newListCommand(cli) assert.NilError(t, cmd.Execute()) assert.Contains(t, buf.String(), "HOSTNAME") } + +func TestNodeListDefaultFormat(t *testing.T) { + buf := new(bytes.Buffer) + cli := test.NewFakeCli(&fakeClient{ + nodeListFunc: func() ([]swarm.Node, error) { + return []swarm.Node{ + *Node(NodeID("nodeID1"), Hostname("nodeHostname1"), Manager(Leader())), + *Node(NodeID("nodeID2"), Hostname("nodeHostname2"), Manager()), + *Node(NodeID("nodeID3"), Hostname("nodeHostname3")), + }, nil + }, + infoFunc: func() (types.Info, error) { + return types.Info{ + Swarm: swarm.Info{ + NodeID: "nodeID1", + }, + }, nil + }, + }, buf) + cli.SetConfigfile(&configfile.ConfigFile{ + NodesFormat: "{{.ID}}: {{.Hostname}} {{.Status}}/{{.ManagerStatus}}", + }) + cmd := newListCommand(cli) + assert.NilError(t, cmd.Execute()) + assert.Contains(t, buf.String(), `nodeID1 *: nodeHostname1 Ready/Leader`) + assert.Contains(t, buf.String(), `nodeID2: nodeHostname2 Ready/Reachable`) + assert.Contains(t, buf.String(), `nodeID3: nodeHostname3 Ready`) +} + +func TestNodeListFormat(t *testing.T) { + buf := new(bytes.Buffer) + cli := test.NewFakeCli(&fakeClient{ + nodeListFunc: func() ([]swarm.Node, error) { + return []swarm.Node{ + *Node(NodeID("nodeID1"), Hostname("nodeHostname1"), Manager(Leader())), + *Node(NodeID("nodeID2"), Hostname("nodeHostname2"), Manager()), + }, nil + }, + infoFunc: func() (types.Info, error) { + return types.Info{ + Swarm: swarm.Info{ + NodeID: "nodeID1", + }, + }, nil + }, + }, buf) + cli.SetConfigfile(&configfile.ConfigFile{ + NodesFormat: "{{.ID}}: {{.Hostname}} {{.Status}}/{{.ManagerStatus}}", + }) + cmd := newListCommand(cli) + cmd.Flags().Set("format", "{{.Hostname}}: {{.ManagerStatus}}") + assert.NilError(t, cmd.Execute()) + assert.Contains(t, buf.String(), `nodeHostname1: Leader`) + assert.Contains(t, buf.String(), `nodeHostname2: Reachable`) +} diff --git a/components/cli/config/configfile/file.go b/components/cli/config/configfile/file.go index cc1c3d0d54..f0f6924049 100644 --- a/components/cli/config/configfile/file.go +++ b/components/cli/config/configfile/file.go @@ -38,6 +38,7 @@ type ConfigFile struct { ServicesFormat string `json:"servicesFormat,omitempty"` TasksFormat string `json:"tasksFormat,omitempty"` SecretFormat string `json:"secretFormat,omitempty"` + NodesFormat string `json:"nodesFormat,omitempty"` } // LegacyLoadFromReader reads the non-nested configuration data given and sets up the From b2ea4b4caf1583499ee9c2509753eb0d5d54f44a Mon Sep 17 00:00:00 2001 From: Arash Deshmeh Date: Sat, 1 Apr 2017 03:07:22 -0400 Subject: [PATCH 705/978] added unit tests for package cli/command/secret Signed-off-by: Arash Deshmeh Upstream-commit: d5dca7c687ae81c4bec73a5dfdf1049b6e5ed3d8 Component: cli --- components/cli/command/secret/client_test.go | 44 +++++ components/cli/command/secret/create.go | 4 +- components/cli/command/secret/create_test.go | 126 +++++++++++++ components/cli/command/secret/inspect.go | 4 +- components/cli/command/secret/inspect_test.go | 149 +++++++++++++++ components/cli/command/secret/ls.go | 4 +- components/cli/command/secret/ls_test.go | 172 ++++++++++++++++++ components/cli/command/secret/remove.go | 4 +- components/cli/command/secret/remove_test.go | 81 +++++++++ .../testdata/secret-create-with-name.golden | 1 + ...t-inspect-with-format.json-template.golden | 1 + ...inspect-with-format.simple-template.golden | 1 + ...format.multiple-secrets-with-labels.golden | 26 +++ ...nspect-without-format.single-secret.golden | 12 ++ .../secret-list-with-config-format.golden | 2 + .../testdata/secret-list-with-filter.golden | 3 + .../testdata/secret-list-with-format.golden | 2 + .../secret-list-with-quiet-option.golden | 2 + .../secret/testdata/secret-list.golden | 3 + .../cli/internal/test/builders/secret.go | 61 +++++++ 20 files changed, 694 insertions(+), 8 deletions(-) create mode 100644 components/cli/command/secret/client_test.go create mode 100644 components/cli/command/secret/create_test.go create mode 100644 components/cli/command/secret/inspect_test.go create mode 100644 components/cli/command/secret/ls_test.go create mode 100644 components/cli/command/secret/remove_test.go create mode 100644 components/cli/command/secret/testdata/secret-create-with-name.golden create mode 100644 components/cli/command/secret/testdata/secret-inspect-with-format.json-template.golden create mode 100644 components/cli/command/secret/testdata/secret-inspect-with-format.simple-template.golden create mode 100644 components/cli/command/secret/testdata/secret-inspect-without-format.multiple-secrets-with-labels.golden create mode 100644 components/cli/command/secret/testdata/secret-inspect-without-format.single-secret.golden create mode 100644 components/cli/command/secret/testdata/secret-list-with-config-format.golden create mode 100644 components/cli/command/secret/testdata/secret-list-with-filter.golden create mode 100644 components/cli/command/secret/testdata/secret-list-with-format.golden create mode 100644 components/cli/command/secret/testdata/secret-list-with-quiet-option.golden create mode 100644 components/cli/command/secret/testdata/secret-list.golden create mode 100644 components/cli/internal/test/builders/secret.go diff --git a/components/cli/command/secret/client_test.go b/components/cli/command/secret/client_test.go new file mode 100644 index 0000000000..bb4b412fc2 --- /dev/null +++ b/components/cli/command/secret/client_test.go @@ -0,0 +1,44 @@ +package secret + +import ( + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/client" + "golang.org/x/net/context" +) + +type fakeClient struct { + client.Client + secretCreateFunc func(swarm.SecretSpec) (types.SecretCreateResponse, error) + secretInspectFunc func(string) (swarm.Secret, []byte, error) + secretListFunc func(types.SecretListOptions) ([]swarm.Secret, error) + secretRemoveFunc func(string) error +} + +func (c *fakeClient) SecretCreate(ctx context.Context, spec swarm.SecretSpec) (types.SecretCreateResponse, error) { + if c.secretCreateFunc != nil { + return c.secretCreateFunc(spec) + } + return types.SecretCreateResponse{}, nil +} + +func (c *fakeClient) SecretInspectWithRaw(ctx context.Context, id string) (swarm.Secret, []byte, error) { + if c.secretInspectFunc != nil { + return c.secretInspectFunc(id) + } + return swarm.Secret{}, nil, nil +} + +func (c *fakeClient) SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) { + if c.secretListFunc != nil { + return c.secretListFunc(options) + } + return []swarm.Secret{}, nil +} + +func (c *fakeClient) SecretRemove(ctx context.Context, name string) error { + if c.secretRemoveFunc != nil { + return c.secretRemoveFunc(name) + } + return nil +} diff --git a/components/cli/command/secret/create.go b/components/cli/command/secret/create.go index 11a85a22ca..59b0798178 100644 --- a/components/cli/command/secret/create.go +++ b/components/cli/command/secret/create.go @@ -22,7 +22,7 @@ type createOptions struct { labels opts.ListOpts } -func newSecretCreateCommand(dockerCli *command.DockerCli) *cobra.Command { +func newSecretCreateCommand(dockerCli command.Cli) *cobra.Command { createOpts := createOptions{ labels: opts.NewListOpts(opts.ValidateEnv), } @@ -43,7 +43,7 @@ func newSecretCreateCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runSecretCreate(dockerCli *command.DockerCli, options createOptions) error { +func runSecretCreate(dockerCli command.Cli, options createOptions) error { client := dockerCli.Client() ctx := context.Background() diff --git a/components/cli/command/secret/create_test.go b/components/cli/command/secret/create_test.go new file mode 100644 index 0000000000..cbdfd63338 --- /dev/null +++ b/components/cli/command/secret/create_test.go @@ -0,0 +1,126 @@ +package secret + +import ( + "bytes" + "io/ioutil" + "path/filepath" + "strings" + "testing" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/internal/test" + "github.com/docker/docker/pkg/testutil/assert" + "github.com/docker/docker/pkg/testutil/golden" + "github.com/pkg/errors" +) + +const secretDataFile = "secret-create-with-name.golden" + +func TestSecretCreateErrors(t *testing.T) { + testCases := []struct { + args []string + secretCreateFunc func(swarm.SecretSpec) (types.SecretCreateResponse, error) + expectedError string + }{ + { + args: []string{"too_few"}, + expectedError: "requires exactly 2 argument(s)", + }, + {args: []string{"too", "many", "arguments"}, + expectedError: "requires exactly 2 argument(s)", + }, + { + args: []string{"name", filepath.Join("testdata", secretDataFile)}, + secretCreateFunc: func(secretSpec swarm.SecretSpec) (types.SecretCreateResponse, error) { + return types.SecretCreateResponse{}, errors.Errorf("error creating secret") + }, + expectedError: "error creating secret", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newSecretCreateCommand( + test.NewFakeCli(&fakeClient{ + secretCreateFunc: tc.secretCreateFunc, + }, buf), + ) + cmd.SetArgs(tc.args) + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestSecretCreateWithName(t *testing.T) { + name := "foo" + buf := new(bytes.Buffer) + var actual []byte + cli := test.NewFakeCli(&fakeClient{ + secretCreateFunc: func(spec swarm.SecretSpec) (types.SecretCreateResponse, error) { + if spec.Name != name { + return types.SecretCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name) + } + + actual = spec.Data + + return types.SecretCreateResponse{ + ID: "ID-" + spec.Name, + }, nil + }, + }, buf) + + cmd := newSecretCreateCommand(cli) + cmd.SetArgs([]string{name, filepath.Join("testdata", secretDataFile)}) + assert.NilError(t, cmd.Execute()) + expected := golden.Get(t, actual, secretDataFile) + assert.Equal(t, string(actual), string(expected)) + assert.Equal(t, strings.TrimSpace(buf.String()), "ID-"+name) +} + +func TestSecretCreateWithLabels(t *testing.T) { + expectedLabels := map[string]string{ + "lbl1": "Label-foo", + "lbl2": "Label-bar", + } + name := "foo" + + buf := new(bytes.Buffer) + cli := test.NewFakeCli(&fakeClient{ + secretCreateFunc: func(spec swarm.SecretSpec) (types.SecretCreateResponse, error) { + if spec.Name != name { + return types.SecretCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name) + } + + if !compareMap(spec.Labels, expectedLabels) { + return types.SecretCreateResponse{}, errors.Errorf("expected labels %v, got %v", expectedLabels, spec.Labels) + } + + return types.SecretCreateResponse{ + ID: "ID-" + spec.Name, + }, nil + }, + }, buf) + + cmd := newSecretCreateCommand(cli) + cmd.SetArgs([]string{name, filepath.Join("testdata", secretDataFile)}) + cmd.Flags().Set("label", "lbl1=Label-foo") + cmd.Flags().Set("label", "lbl2=Label-bar") + assert.NilError(t, cmd.Execute()) + assert.Equal(t, strings.TrimSpace(buf.String()), "ID-"+name) +} + +func compareMap(actual map[string]string, expected map[string]string) bool { + if len(actual) != len(expected) { + return false + } + for key, value := range actual { + if expectedValue, ok := expected[key]; ok { + if expectedValue != value { + return false + } + } else { + return false + } + } + return true +} diff --git a/components/cli/command/secret/inspect.go b/components/cli/command/secret/inspect.go index fb694c5fbe..8b3c3c682e 100644 --- a/components/cli/command/secret/inspect.go +++ b/components/cli/command/secret/inspect.go @@ -13,7 +13,7 @@ type inspectOptions struct { format string } -func newSecretInspectCommand(dockerCli *command.DockerCli) *cobra.Command { +func newSecretInspectCommand(dockerCli command.Cli) *cobra.Command { opts := inspectOptions{} cmd := &cobra.Command{ Use: "inspect [OPTIONS] SECRET [SECRET...]", @@ -29,7 +29,7 @@ func newSecretInspectCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runSecretInspect(dockerCli *command.DockerCli, opts inspectOptions) error { +func runSecretInspect(dockerCli command.Cli, opts inspectOptions) error { client := dockerCli.Client() ctx := context.Background() diff --git a/components/cli/command/secret/inspect_test.go b/components/cli/command/secret/inspect_test.go new file mode 100644 index 0000000000..558e23d7c7 --- /dev/null +++ b/components/cli/command/secret/inspect_test.go @@ -0,0 +1,149 @@ +package secret + +import ( + "bytes" + "fmt" + "io/ioutil" + "testing" + + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/internal/test" + "github.com/pkg/errors" + // Import builders to get the builder function as package function + . "github.com/docker/docker/cli/internal/test/builders" + "github.com/docker/docker/pkg/testutil/assert" + "github.com/docker/docker/pkg/testutil/golden" +) + +func TestSecretInspectErrors(t *testing.T) { + testCases := []struct { + args []string + flags map[string]string + secretInspectFunc func(secretID string) (swarm.Secret, []byte, error) + expectedError string + }{ + { + expectedError: "requires at least 1 argument", + }, + { + args: []string{"foo"}, + secretInspectFunc: func(secretID string) (swarm.Secret, []byte, error) { + return swarm.Secret{}, nil, errors.Errorf("error while inspecting the secret") + }, + expectedError: "error while inspecting the secret", + }, + { + args: []string{"foo"}, + flags: map[string]string{ + "format": "{{invalid format}}", + }, + expectedError: "Template parsing error", + }, + { + args: []string{"foo", "bar"}, + secretInspectFunc: func(secretID string) (swarm.Secret, []byte, error) { + if secretID == "foo" { + return *Secret(SecretName("foo")), nil, nil + } + return swarm.Secret{}, nil, errors.Errorf("error while inspecting the secret") + }, + expectedError: "error while inspecting the secret", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newSecretInspectCommand( + test.NewFakeCli(&fakeClient{ + secretInspectFunc: tc.secretInspectFunc, + }, buf), + ) + cmd.SetArgs(tc.args) + for key, value := range tc.flags { + cmd.Flags().Set(key, value) + } + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestSecretInspectWithoutFormat(t *testing.T) { + testCases := []struct { + name string + args []string + secretInspectFunc func(secretID string) (swarm.Secret, []byte, error) + }{ + { + name: "single-secret", + args: []string{"foo"}, + secretInspectFunc: func(name string) (swarm.Secret, []byte, error) { + if name != "foo" { + return swarm.Secret{}, nil, errors.Errorf("Invalid name, expected %s, got %s", "foo", name) + } + return *Secret(SecretID("ID-foo"), SecretName("foo")), nil, nil + }, + }, + { + name: "multiple-secrets-with-labels", + args: []string{"foo", "bar"}, + secretInspectFunc: func(name string) (swarm.Secret, []byte, error) { + return *Secret(SecretID("ID-"+name), SecretName(name), SecretLabels(map[string]string{ + "label1": "label-foo", + })), nil, nil + }, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newSecretInspectCommand( + test.NewFakeCli(&fakeClient{ + secretInspectFunc: tc.secretInspectFunc, + }, buf), + ) + cmd.SetArgs(tc.args) + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), fmt.Sprintf("secret-inspect-without-format.%s.golden", tc.name)) + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) + } +} + +func TestSecretInspectWithFormat(t *testing.T) { + secretInspectFunc := func(name string) (swarm.Secret, []byte, error) { + return *Secret(SecretName("foo"), SecretLabels(map[string]string{ + "label1": "label-foo", + })), nil, nil + } + testCases := []struct { + name string + format string + args []string + secretInspectFunc func(name string) (swarm.Secret, []byte, error) + }{ + { + name: "simple-template", + format: "{{.Spec.Name}}", + args: []string{"foo"}, + secretInspectFunc: secretInspectFunc, + }, + { + name: "json-template", + format: "{{json .Spec.Labels}}", + args: []string{"foo"}, + secretInspectFunc: secretInspectFunc, + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newSecretInspectCommand( + test.NewFakeCli(&fakeClient{ + secretInspectFunc: tc.secretInspectFunc, + }, buf), + ) + cmd.SetArgs(tc.args) + cmd.Flags().Set("format", tc.format) + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), fmt.Sprintf("secret-inspect-with-format.%s.golden", tc.name)) + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) + } +} diff --git a/components/cli/command/secret/ls.go b/components/cli/command/secret/ls.go index 1d60ff7c4d..384ee26509 100644 --- a/components/cli/command/secret/ls.go +++ b/components/cli/command/secret/ls.go @@ -16,7 +16,7 @@ type listOptions struct { filter opts.FilterOpt } -func newSecretListCommand(dockerCli *command.DockerCli) *cobra.Command { +func newSecretListCommand(dockerCli command.Cli) *cobra.Command { opts := listOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ @@ -37,7 +37,7 @@ func newSecretListCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runSecretList(dockerCli *command.DockerCli, opts listOptions) error { +func runSecretList(dockerCli command.Cli, opts listOptions) error { client := dockerCli.Client() ctx := context.Background() diff --git a/components/cli/command/secret/ls_test.go b/components/cli/command/secret/ls_test.go new file mode 100644 index 0000000000..d9a4324b75 --- /dev/null +++ b/components/cli/command/secret/ls_test.go @@ -0,0 +1,172 @@ +package secret + +import ( + "bytes" + "io/ioutil" + "testing" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/config/configfile" + "github.com/docker/docker/cli/internal/test" + "github.com/pkg/errors" + // Import builders to get the builder function as package function + . "github.com/docker/docker/cli/internal/test/builders" + "github.com/docker/docker/pkg/testutil/assert" + "github.com/docker/docker/pkg/testutil/golden" +) + +func TestSecretListErrors(t *testing.T) { + testCases := []struct { + args []string + secretListFunc func(types.SecretListOptions) ([]swarm.Secret, error) + expectedError string + }{ + { + args: []string{"foo"}, + expectedError: "accepts no argument", + }, + { + secretListFunc: func(options types.SecretListOptions) ([]swarm.Secret, error) { + return []swarm.Secret{}, errors.Errorf("error listing secrets") + }, + expectedError: "error listing secrets", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newSecretListCommand( + test.NewFakeCli(&fakeClient{ + secretListFunc: tc.secretListFunc, + }, buf), + ) + cmd.SetArgs(tc.args) + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestSecretList(t *testing.T) { + buf := new(bytes.Buffer) + cli := test.NewFakeCli(&fakeClient{ + secretListFunc: func(options types.SecretListOptions) ([]swarm.Secret, error) { + return []swarm.Secret{ + *Secret(SecretID("ID-foo"), + SecretName("foo"), + SecretVersion(swarm.Version{Index: 10}), + SecretCreatedAt(time.Now().Add(-2*time.Hour)), + SecretUpdatedAt(time.Now().Add(-1*time.Hour)), + ), + *Secret(SecretID("ID-bar"), + SecretName("bar"), + SecretVersion(swarm.Version{Index: 11}), + SecretCreatedAt(time.Now().Add(-2*time.Hour)), + SecretUpdatedAt(time.Now().Add(-1*time.Hour)), + ), + }, nil + }, + }, buf) + cli.SetConfigfile(&configfile.ConfigFile{}) + cmd := newSecretListCommand(cli) + cmd.SetOutput(buf) + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "secret-list.golden") + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) +} + +func TestSecretListWithQuietOption(t *testing.T) { + buf := new(bytes.Buffer) + cli := test.NewFakeCli(&fakeClient{ + secretListFunc: func(options types.SecretListOptions) ([]swarm.Secret, error) { + return []swarm.Secret{ + *Secret(SecretID("ID-foo"), SecretName("foo")), + *Secret(SecretID("ID-bar"), SecretName("bar"), SecretLabels(map[string]string{ + "label": "label-bar", + })), + }, nil + }, + }, buf) + cli.SetConfigfile(&configfile.ConfigFile{}) + cmd := newSecretListCommand(cli) + cmd.Flags().Set("quiet", "true") + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "secret-list-with-quiet-option.golden") + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) +} + +func TestSecretListWithConfigFormat(t *testing.T) { + buf := new(bytes.Buffer) + cli := test.NewFakeCli(&fakeClient{ + secretListFunc: func(options types.SecretListOptions) ([]swarm.Secret, error) { + return []swarm.Secret{ + *Secret(SecretID("ID-foo"), SecretName("foo")), + *Secret(SecretID("ID-bar"), SecretName("bar"), SecretLabels(map[string]string{ + "label": "label-bar", + })), + }, nil + }, + }, buf) + cli.SetConfigfile(&configfile.ConfigFile{ + SecretFormat: "{{ .Name }} {{ .Labels }}", + }) + cmd := newSecretListCommand(cli) + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "secret-list-with-config-format.golden") + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) +} + +func TestSecretListWithFormat(t *testing.T) { + buf := new(bytes.Buffer) + cli := test.NewFakeCli(&fakeClient{ + secretListFunc: func(options types.SecretListOptions) ([]swarm.Secret, error) { + return []swarm.Secret{ + *Secret(SecretID("ID-foo"), SecretName("foo")), + *Secret(SecretID("ID-bar"), SecretName("bar"), SecretLabels(map[string]string{ + "label": "label-bar", + })), + }, nil + }, + }, buf) + cmd := newSecretListCommand(cli) + cmd.Flags().Set("format", "{{ .Name }} {{ .Labels }}") + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "secret-list-with-format.golden") + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) +} + +func TestSecretListWithFilter(t *testing.T) { + buf := new(bytes.Buffer) + cli := test.NewFakeCli(&fakeClient{ + secretListFunc: func(options types.SecretListOptions) ([]swarm.Secret, error) { + assert.Equal(t, options.Filters.Get("name")[0], "foo") + assert.Equal(t, options.Filters.Get("label")[0], "lbl1=Label-bar") + return []swarm.Secret{ + *Secret(SecretID("ID-foo"), + SecretName("foo"), + SecretVersion(swarm.Version{Index: 10}), + SecretCreatedAt(time.Now().Add(-2*time.Hour)), + SecretUpdatedAt(time.Now().Add(-1*time.Hour)), + ), + *Secret(SecretID("ID-bar"), + SecretName("bar"), + SecretVersion(swarm.Version{Index: 11}), + SecretCreatedAt(time.Now().Add(-2*time.Hour)), + SecretUpdatedAt(time.Now().Add(-1*time.Hour)), + ), + }, nil + }, + }, buf) + cli.SetConfigfile(&configfile.ConfigFile{}) + cmd := newSecretListCommand(cli) + cmd.Flags().Set("filter", "name=foo") + cmd.Flags().Set("filter", "label=lbl1=Label-bar") + assert.NilError(t, cmd.Execute()) + actual := buf.String() + expected := golden.Get(t, []byte(actual), "secret-list-with-filter.golden") + assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected)) +} diff --git a/components/cli/command/secret/remove.go b/components/cli/command/secret/remove.go index 9115550d4d..a4b501d176 100644 --- a/components/cli/command/secret/remove.go +++ b/components/cli/command/secret/remove.go @@ -15,7 +15,7 @@ type removeOptions struct { names []string } -func newSecretRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { +func newSecretRemoveCommand(dockerCli command.Cli) *cobra.Command { return &cobra.Command{ Use: "rm SECRET [SECRET...]", Aliases: []string{"remove"}, @@ -30,7 +30,7 @@ func newSecretRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { } } -func runSecretRemove(dockerCli *command.DockerCli, opts removeOptions) error { +func runSecretRemove(dockerCli command.Cli, opts removeOptions) error { client := dockerCli.Client() ctx := context.Background() diff --git a/components/cli/command/secret/remove_test.go b/components/cli/command/secret/remove_test.go new file mode 100644 index 0000000000..92ca9b9b9d --- /dev/null +++ b/components/cli/command/secret/remove_test.go @@ -0,0 +1,81 @@ +package secret + +import ( + "bytes" + "io/ioutil" + "strings" + "testing" + + "github.com/docker/docker/cli/internal/test" + "github.com/docker/docker/pkg/testutil/assert" + "github.com/pkg/errors" +) + +func TestSecretRemoveErrors(t *testing.T) { + testCases := []struct { + args []string + secretRemoveFunc func(string) error + expectedError string + }{ + { + args: []string{}, + expectedError: "requires at least 1 argument(s).", + }, + { + args: []string{"foo"}, + secretRemoveFunc: func(name string) error { + return errors.Errorf("error removing secret") + }, + expectedError: "error removing secret", + }, + } + for _, tc := range testCases { + buf := new(bytes.Buffer) + cmd := newSecretRemoveCommand( + test.NewFakeCli(&fakeClient{ + secretRemoveFunc: tc.secretRemoveFunc, + }, buf), + ) + cmd.SetArgs(tc.args) + cmd.SetOutput(ioutil.Discard) + assert.Error(t, cmd.Execute(), tc.expectedError) + } +} + +func TestSecretRemoveWithName(t *testing.T) { + names := []string{"foo", "bar"} + buf := new(bytes.Buffer) + var removedSecrets []string + cli := test.NewFakeCli(&fakeClient{ + secretRemoveFunc: func(name string) error { + removedSecrets = append(removedSecrets, name) + return nil + }, + }, buf) + cmd := newSecretRemoveCommand(cli) + cmd.SetArgs(names) + assert.NilError(t, cmd.Execute()) + assert.EqualStringSlice(t, strings.Split(strings.TrimSpace(buf.String()), "\n"), names) + assert.EqualStringSlice(t, removedSecrets, names) +} + +func TestSecretRemoveContinueAfterError(t *testing.T) { + names := []string{"foo", "bar"} + buf := new(bytes.Buffer) + var removedSecrets []string + + cli := test.NewFakeCli(&fakeClient{ + secretRemoveFunc: func(name string) error { + removedSecrets = append(removedSecrets, name) + if name == "foo" { + return errors.Errorf("error removing secret: %s", name) + } + return nil + }, + }, buf) + + cmd := newSecretRemoveCommand(cli) + cmd.SetArgs(names) + assert.Error(t, cmd.Execute(), "error removing secret: foo") + assert.EqualStringSlice(t, removedSecrets, names) +} diff --git a/components/cli/command/secret/testdata/secret-create-with-name.golden b/components/cli/command/secret/testdata/secret-create-with-name.golden new file mode 100644 index 0000000000..788642a93a --- /dev/null +++ b/components/cli/command/secret/testdata/secret-create-with-name.golden @@ -0,0 +1 @@ +secret_foo_bar diff --git a/components/cli/command/secret/testdata/secret-inspect-with-format.json-template.golden b/components/cli/command/secret/testdata/secret-inspect-with-format.json-template.golden new file mode 100644 index 0000000000..aab678f85d --- /dev/null +++ b/components/cli/command/secret/testdata/secret-inspect-with-format.json-template.golden @@ -0,0 +1 @@ +{"label1":"label-foo"} diff --git a/components/cli/command/secret/testdata/secret-inspect-with-format.simple-template.golden b/components/cli/command/secret/testdata/secret-inspect-with-format.simple-template.golden new file mode 100644 index 0000000000..257cc5642c --- /dev/null +++ b/components/cli/command/secret/testdata/secret-inspect-with-format.simple-template.golden @@ -0,0 +1 @@ +foo diff --git a/components/cli/command/secret/testdata/secret-inspect-without-format.multiple-secrets-with-labels.golden b/components/cli/command/secret/testdata/secret-inspect-without-format.multiple-secrets-with-labels.golden new file mode 100644 index 0000000000..6887c185f1 --- /dev/null +++ b/components/cli/command/secret/testdata/secret-inspect-without-format.multiple-secrets-with-labels.golden @@ -0,0 +1,26 @@ +[ + { + "ID": "ID-foo", + "Version": {}, + "CreatedAt": "0001-01-01T00:00:00Z", + "UpdatedAt": "0001-01-01T00:00:00Z", + "Spec": { + "Name": "foo", + "Labels": { + "label1": "label-foo" + } + } + }, + { + "ID": "ID-bar", + "Version": {}, + "CreatedAt": "0001-01-01T00:00:00Z", + "UpdatedAt": "0001-01-01T00:00:00Z", + "Spec": { + "Name": "bar", + "Labels": { + "label1": "label-foo" + } + } + } +] diff --git a/components/cli/command/secret/testdata/secret-inspect-without-format.single-secret.golden b/components/cli/command/secret/testdata/secret-inspect-without-format.single-secret.golden new file mode 100644 index 0000000000..ea42ec6f4f --- /dev/null +++ b/components/cli/command/secret/testdata/secret-inspect-without-format.single-secret.golden @@ -0,0 +1,12 @@ +[ + { + "ID": "ID-foo", + "Version": {}, + "CreatedAt": "0001-01-01T00:00:00Z", + "UpdatedAt": "0001-01-01T00:00:00Z", + "Spec": { + "Name": "foo", + "Labels": null + } + } +] diff --git a/components/cli/command/secret/testdata/secret-list-with-config-format.golden b/components/cli/command/secret/testdata/secret-list-with-config-format.golden new file mode 100644 index 0000000000..9a47538804 --- /dev/null +++ b/components/cli/command/secret/testdata/secret-list-with-config-format.golden @@ -0,0 +1,2 @@ +foo +bar label=label-bar diff --git a/components/cli/command/secret/testdata/secret-list-with-filter.golden b/components/cli/command/secret/testdata/secret-list-with-filter.golden new file mode 100644 index 0000000000..29983de8e9 --- /dev/null +++ b/components/cli/command/secret/testdata/secret-list-with-filter.golden @@ -0,0 +1,3 @@ +ID NAME CREATED UPDATED +ID-foo foo 2 hours ago About an hour ago +ID-bar bar 2 hours ago About an hour ago diff --git a/components/cli/command/secret/testdata/secret-list-with-format.golden b/components/cli/command/secret/testdata/secret-list-with-format.golden new file mode 100644 index 0000000000..9a47538804 --- /dev/null +++ b/components/cli/command/secret/testdata/secret-list-with-format.golden @@ -0,0 +1,2 @@ +foo +bar label=label-bar diff --git a/components/cli/command/secret/testdata/secret-list-with-quiet-option.golden b/components/cli/command/secret/testdata/secret-list-with-quiet-option.golden new file mode 100644 index 0000000000..83fb6e8979 --- /dev/null +++ b/components/cli/command/secret/testdata/secret-list-with-quiet-option.golden @@ -0,0 +1,2 @@ +ID-foo +ID-bar diff --git a/components/cli/command/secret/testdata/secret-list.golden b/components/cli/command/secret/testdata/secret-list.golden new file mode 100644 index 0000000000..29983de8e9 --- /dev/null +++ b/components/cli/command/secret/testdata/secret-list.golden @@ -0,0 +1,3 @@ +ID NAME CREATED UPDATED +ID-foo foo 2 hours ago About an hour ago +ID-bar bar 2 hours ago About an hour ago diff --git a/components/cli/internal/test/builders/secret.go b/components/cli/internal/test/builders/secret.go new file mode 100644 index 0000000000..9e0f910e93 --- /dev/null +++ b/components/cli/internal/test/builders/secret.go @@ -0,0 +1,61 @@ +package builders + +import ( + "time" + + "github.com/docker/docker/api/types/swarm" +) + +// Secret creates a secret with default values. +// Any number of secret builder functions can be passed to augment it. +func Secret(builders ...func(secret *swarm.Secret)) *swarm.Secret { + secret := &swarm.Secret{} + + for _, builder := range builders { + builder(secret) + } + + return secret +} + +// SecretLabels sets the secret's labels +func SecretLabels(labels map[string]string) func(secret *swarm.Secret) { + return func(secret *swarm.Secret) { + secret.Spec.Labels = labels + } +} + +// SecretName sets the secret's name +func SecretName(name string) func(secret *swarm.Secret) { + return func(secret *swarm.Secret) { + secret.Spec.Name = name + } +} + +// SecretID sets the secret's ID +func SecretID(ID string) func(secret *swarm.Secret) { + return func(secret *swarm.Secret) { + secret.ID = ID + } +} + +// SecretVersion sets the version for the secret +func SecretVersion(v swarm.Version) func(*swarm.Secret) { + return func(secret *swarm.Secret) { + secret.Version = v + } +} + +// SecretCreatedAt sets the creation time for the secret +func SecretCreatedAt(t time.Time) func(*swarm.Secret) { + return func(secret *swarm.Secret) { + secret.CreatedAt = t + } +} + +// SecretUpdatedAt sets the update time for the secret +func SecretUpdatedAt(t time.Time) func(*swarm.Secret) { + return func(secret *swarm.Secret) { + secret.UpdatedAt = t + } +} From 5cb6ccf57166068dce1b97386d84b0ea084501cf Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 8 Mar 2017 10:29:15 -0800 Subject: [PATCH 706/978] Add hidden placeholder of `.Self` for `docker node ls --format` This commit adds a hidden placeholder of `.Self` for `docker node ls --format` so that if the node is the same as the current docker daemon, then a `*` is outputed. Signed-off-by: Yong Tang Upstream-commit: b807d24e56d6affcad722c5238c6bda1639c9455 Component: cli --- components/cli/command/formatter/node.go | 14 ++++++++------ components/cli/command/formatter/node_test.go | 4 ++-- components/cli/command/node/list_test.go | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/components/cli/command/formatter/node.go b/components/cli/command/formatter/node.go index bd478e57f2..6a6fb43c1a 100644 --- a/components/cli/command/formatter/node.go +++ b/components/cli/command/formatter/node.go @@ -7,9 +7,10 @@ import ( ) const ( - defaultNodeTableFormat = "table {{.ID}}\t{{.Hostname}}\t{{.Status}}\t{{.Availability}}\t{{.ManagerStatus}}" + defaultNodeTableFormat = "table {{.ID}} {{if .Self}}*{{else}} {{ end }}\t{{.Hostname}}\t{{.Status}}\t{{.Availability}}\t{{.ManagerStatus}}" nodeIDHeader = "ID" + selfHeader = "" hostnameHeader = "HOSTNAME" availabilityHeader = "AVAILABILITY" managerStatusHeader = "MANAGER STATUS" @@ -46,6 +47,7 @@ func NodeWrite(ctx Context, nodes []swarm.Node, info types.Info) error { nodeCtx := nodeContext{} nodeCtx.header = nodeHeaderContext{ "ID": nodeIDHeader, + "Self": selfHeader, "Hostname": hostnameHeader, "Status": statusHeader, "Availability": availabilityHeader, @@ -67,11 +69,11 @@ func (c *nodeContext) MarshalJSON() ([]byte, error) { } func (c *nodeContext) ID() string { - nodeID := c.n.ID - if nodeID == c.info.Swarm.NodeID { - nodeID = nodeID + " *" - } - return nodeID + return c.n.ID +} + +func (c *nodeContext) Self() bool { + return c.n.ID == c.info.Swarm.NodeID } func (c *nodeContext) Hostname() string { diff --git a/components/cli/command/formatter/node_test.go b/components/cli/command/formatter/node_test.go index e3e341fc8b..86f4979d3f 100644 --- a/components/cli/command/formatter/node_test.go +++ b/components/cli/command/formatter/node_test.go @@ -148,8 +148,8 @@ func TestNodeContextWriteJSON(t *testing.T) { {ID: "nodeID2", Description: swarm.NodeDescription{Hostname: "foobar_bar"}}, } expectedJSONs := []map[string]interface{}{ - {"Availability": "", "Hostname": "foobar_baz", "ID": "nodeID1", "ManagerStatus": "", "Status": ""}, - {"Availability": "", "Hostname": "foobar_bar", "ID": "nodeID2", "ManagerStatus": "", "Status": ""}, + {"Availability": "", "Hostname": "foobar_baz", "ID": "nodeID1", "ManagerStatus": "", "Status": "", "Self": false}, + {"Availability": "", "Hostname": "foobar_bar", "ID": "nodeID2", "ManagerStatus": "", "Status": "", "Self": false}, } out := bytes.NewBufferString("") diff --git a/components/cli/command/node/list_test.go b/components/cli/command/node/list_test.go index 13a21d1b52..4b8d906c3a 100644 --- a/components/cli/command/node/list_test.go +++ b/components/cli/command/node/list_test.go @@ -129,7 +129,7 @@ func TestNodeListDefaultFormat(t *testing.T) { }) cmd := newListCommand(cli) assert.NilError(t, cmd.Execute()) - assert.Contains(t, buf.String(), `nodeID1 *: nodeHostname1 Ready/Leader`) + assert.Contains(t, buf.String(), `nodeID1: nodeHostname1 Ready/Leader`) assert.Contains(t, buf.String(), `nodeID2: nodeHostname2 Ready/Reachable`) assert.Contains(t, buf.String(), `nodeID3: nodeHostname3 Ready`) } From 9696fa517f3d6f897d0be16640ad43a3c92d4167 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Tue, 4 Apr 2017 18:16:57 -0700 Subject: [PATCH 707/978] cli: Preserve order of environment variables Unless we are adding or removing environment variables, their order shouldn't be changed. This makes it look like the service's TaskSpec has changed relative to the old version of the service, and containers need to be redeployed. The existing code always rebuilds the list of environment variables by converting them to a map and back, but there's no reason to do this if no environment variables are being added. Signed-off-by: Aaron Lehmann Upstream-commit: 71d1b0507ea5bfd46ca0f99c1575de53ca049193 Component: cli --- components/cli/command/service/update.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index afa0f807e9..1933ff38eb 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -524,20 +524,21 @@ func updateLabels(flags *pflag.FlagSet, field *map[string]string) { } func updateEnvironment(flags *pflag.FlagSet, field *[]string) { - envSet := map[string]string{} - for _, v := range *field { - envSet[envKey(v)] = v - } if flags.Changed(flagEnvAdd) { + envSet := map[string]string{} + for _, v := range *field { + envSet[envKey(v)] = v + } + value := flags.Lookup(flagEnvAdd).Value.(*opts.ListOpts) for _, v := range value.GetAll() { envSet[envKey(v)] = v } - } - *field = []string{} - for _, v := range envSet { - *field = append(*field, v) + *field = []string{} + for _, v := range envSet { + *field = append(*field, v) + } } toRemove := buildToRemoveSet(flags, flagEnvRemove) From 32f6be8e3e23ab47c702a02239ab403da54e1b22 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 31 Mar 2017 06:41:45 +0000 Subject: [PATCH 708/978] cli: add `--mount` to `docker run` Signed-off-by: Akihiro Suda Upstream-commit: 02b904588a479f7a101fe6ed6388eeeec38ca8d4 Component: cli --- components/cli/command/container/opts.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/components/cli/command/container/opts.go b/components/cli/command/container/opts.go index c8ba4cd255..fc4ac855d5 100644 --- a/components/cli/command/container/opts.go +++ b/components/cli/command/container/opts.go @@ -11,6 +11,7 @@ import ( "strings" "time" + "github.com/Sirupsen/logrus" "github.com/docker/docker/api/types/container" networktypes "github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/strslice" @@ -31,6 +32,7 @@ type containerOptions struct { attach opts.ListOpts volumes opts.ListOpts tmpfs opts.ListOpts + mounts opts.MountOpt blkioWeightDevice opts.WeightdeviceOpt deviceReadBps opts.ThrottledeviceOpt deviceWriteBps opts.ThrottledeviceOpt @@ -223,6 +225,7 @@ func addFlags(flags *pflag.FlagSet) *containerOptions { flags.Var(&copts.tmpfs, "tmpfs", "Mount a tmpfs directory") flags.Var(&copts.volumesFrom, "volumes-from", "Mount volumes from the specified container(s)") flags.VarP(&copts.volumes, "volume", "v", "Bind mount a volume") + flags.Var(&copts.mounts, "mount", "Attach a filesystem mount to the container") // Health-checking flags.StringVar(&copts.healthCmd, "health-cmd", "", "Command to run to check health") @@ -321,6 +324,10 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err return nil, errors.Errorf("invalid value: %d. Valid memory swappiness range is 0-100", swappiness) } + mounts := copts.mounts.Value() + if len(mounts) > 0 && copts.volumeDriver != "" { + logrus.Warn("`--volume-driver` is ignored for volumes specified via `--mount`. Use `--mount type=volume,volume-driver=...` instead.") + } var binds []string volumes := copts.volumes.GetMap() // add any bind targets to the list of container volumes @@ -589,6 +596,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err Tmpfs: tmpfs, Sysctls: copts.sysctls.GetAll(), Runtime: copts.runtime, + Mounts: mounts, } if copts.autoRemove && !hostConfig.RestartPolicy.IsNone() { From 260f839d34f562ba01fd154ee1d8709b276517c3 Mon Sep 17 00:00:00 2001 From: Arash Deshmeh Date: Sun, 26 Mar 2017 02:23:24 -0400 Subject: [PATCH 709/978] stack rm should accept multiple arguments Signed-off-by: Arash Deshmeh Upstream-commit: 585e5a000122538d89beb4f050ca0628905c006b Component: cli --- components/cli/command/stack/client_test.go | 153 ++++++++++++++++++++ components/cli/command/stack/deploy_test.go | 31 +--- components/cli/command/stack/remove.go | 72 +++++---- components/cli/command/stack/remove_test.go | 107 ++++++++++++++ 4 files changed, 302 insertions(+), 61 deletions(-) create mode 100644 components/cli/command/stack/client_test.go create mode 100644 components/cli/command/stack/remove_test.go diff --git a/components/cli/command/stack/client_test.go b/components/cli/command/stack/client_test.go new file mode 100644 index 0000000000..0cd8612b6d --- /dev/null +++ b/components/cli/command/stack/client_test.go @@ -0,0 +1,153 @@ +package stack + +import ( + "strings" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/cli/compose/convert" + "github.com/docker/docker/client" + "golang.org/x/net/context" +) + +type fakeClient struct { + client.Client + + services []string + networks []string + secrets []string + + removedServices []string + removedNetworks []string + removedSecrets []string + + serviceListFunc func(options types.ServiceListOptions) ([]swarm.Service, error) + networkListFunc func(options types.NetworkListOptions) ([]types.NetworkResource, error) + secretListFunc func(options types.SecretListOptions) ([]swarm.Secret, error) + serviceRemoveFunc func(serviceID string) error + networkRemoveFunc func(networkID string) error + secretRemoveFunc func(secretID string) error +} + +func (cli *fakeClient) ServiceList(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) { + if cli.serviceListFunc != nil { + return cli.serviceListFunc(options) + } + + namespace := namespaceFromFilters(options.Filters) + servicesList := []swarm.Service{} + for _, name := range cli.services { + if belongToNamespace(name, namespace) { + servicesList = append(servicesList, serviceFromName(name)) + } + } + return servicesList, nil +} + +func (cli *fakeClient) NetworkList(ctx context.Context, options types.NetworkListOptions) ([]types.NetworkResource, error) { + if cli.networkListFunc != nil { + return cli.networkListFunc(options) + } + + namespace := namespaceFromFilters(options.Filters) + networksList := []types.NetworkResource{} + for _, name := range cli.networks { + if belongToNamespace(name, namespace) { + networksList = append(networksList, networkFromName(name)) + } + } + return networksList, nil +} + +func (cli *fakeClient) SecretList(ctx context.Context, options types.SecretListOptions) ([]swarm.Secret, error) { + if cli.secretListFunc != nil { + return cli.secretListFunc(options) + } + + namespace := namespaceFromFilters(options.Filters) + secretsList := []swarm.Secret{} + for _, name := range cli.secrets { + if belongToNamespace(name, namespace) { + secretsList = append(secretsList, secretFromName(name)) + } + } + return secretsList, nil +} + +func (cli *fakeClient) ServiceRemove(ctx context.Context, serviceID string) error { + if cli.serviceRemoveFunc != nil { + return cli.serviceRemoveFunc(serviceID) + } + + cli.removedServices = append(cli.removedServices, serviceID) + return nil +} + +func (cli *fakeClient) NetworkRemove(ctx context.Context, networkID string) error { + if cli.networkRemoveFunc != nil { + return cli.networkRemoveFunc(networkID) + } + + cli.removedNetworks = append(cli.removedNetworks, networkID) + return nil +} + +func (cli *fakeClient) SecretRemove(ctx context.Context, secretID string) error { + if cli.secretRemoveFunc != nil { + return cli.secretRemoveFunc(secretID) + } + + cli.removedSecrets = append(cli.removedSecrets, secretID) + return nil +} + +func serviceFromName(name string) swarm.Service { + return swarm.Service{ + ID: "ID-" + name, + Spec: swarm.ServiceSpec{ + Annotations: swarm.Annotations{Name: name}, + }, + } +} + +func networkFromName(name string) types.NetworkResource { + return types.NetworkResource{ + ID: "ID-" + name, + Name: name, + } +} + +func secretFromName(name string) swarm.Secret { + return swarm.Secret{ + ID: "ID-" + name, + Spec: swarm.SecretSpec{ + Annotations: swarm.Annotations{Name: name}, + }, + } +} + +func namespaceFromFilters(filters filters.Args) string { + label := filters.Get("label")[0] + return strings.TrimPrefix(label, convert.LabelNamespace+"=") +} + +func belongToNamespace(id, namespace string) bool { + return strings.HasPrefix(id, namespace+"_") +} + +func objectName(namespace, name string) string { + return namespace + "_" + name +} + +func objectID(name string) string { + return "ID-" + name +} + +func buildObjectIDs(objectNames []string) []string { + IDs := make([]string, len(objectNames)) + for i, name := range objectNames { + IDs[i] = objectID(name) + } + return IDs +} diff --git a/components/cli/command/stack/deploy_test.go b/components/cli/command/stack/deploy_test.go index dac1350547..328222af53 100644 --- a/components/cli/command/stack/deploy_test.go +++ b/components/cli/command/stack/deploy_test.go @@ -4,39 +4,12 @@ import ( "bytes" "testing" - "github.com/docker/docker/api/types" - "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/cli/compose/convert" "github.com/docker/docker/cli/internal/test" - "github.com/docker/docker/client" "github.com/docker/docker/pkg/testutil/assert" "golang.org/x/net/context" ) -type fakeClient struct { - client.Client - serviceList []string - removedIDs []string -} - -func (cli *fakeClient) ServiceList(ctx context.Context, options types.ServiceListOptions) ([]swarm.Service, error) { - services := []swarm.Service{} - for _, name := range cli.serviceList { - services = append(services, swarm.Service{ - ID: name, - Spec: swarm.ServiceSpec{ - Annotations: swarm.Annotations{Name: name}, - }, - }) - } - return services, nil -} - -func (cli *fakeClient) ServiceRemove(ctx context.Context, serviceID string) error { - cli.removedIDs = append(cli.removedIDs, serviceID) - return nil -} - func TestPruneServices(t *testing.T) { ctx := context.Background() namespace := convert.NewNamespace("foo") @@ -44,11 +17,11 @@ func TestPruneServices(t *testing.T) { "new": {}, "keep": {}, } - client := &fakeClient{serviceList: []string{"foo_keep", "foo_remove"}} + client := &fakeClient{services: []string{objectName("foo", "keep"), objectName("foo", "remove")}} dockerCli := test.NewFakeCli(client, &bytes.Buffer{}) dockerCli.SetErr(&bytes.Buffer{}) pruneServices(ctx, dockerCli, namespace, services) - assert.DeepEqual(t, client.removedIDs, []string{"foo_remove"}) + assert.DeepEqual(t, client.removedServices, buildObjectIDs([]string{objectName("foo", "remove")})) } diff --git a/components/cli/command/stack/remove.go b/components/cli/command/stack/remove.go index e976eccdaa..7df4e4c0ed 100644 --- a/components/cli/command/stack/remove.go +++ b/components/cli/command/stack/remove.go @@ -2,6 +2,7 @@ package stack import ( "fmt" + "strings" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/swarm" @@ -13,56 +14,63 @@ import ( ) type removeOptions struct { - namespace string + namespaces []string } -func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command { +func newRemoveCommand(dockerCli command.Cli) *cobra.Command { var opts removeOptions cmd := &cobra.Command{ - Use: "rm STACK", + Use: "rm STACK [STACK...]", Aliases: []string{"remove", "down"}, - Short: "Remove the stack", - Args: cli.ExactArgs(1), + Short: "Remove one or more stacks", + Args: cli.RequiresMinArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - opts.namespace = args[0] + opts.namespaces = args return runRemove(dockerCli, opts) }, } return cmd } -func runRemove(dockerCli *command.DockerCli, opts removeOptions) error { - namespace := opts.namespace +func runRemove(dockerCli command.Cli, opts removeOptions) error { + namespaces := opts.namespaces client := dockerCli.Client() ctx := context.Background() - services, err := getServices(ctx, client, namespace) - if err != nil { - return err + var errs []string + for _, namespace := range namespaces { + services, err := getServices(ctx, client, namespace) + if err != nil { + return err + } + + networks, err := getStackNetworks(ctx, client, namespace) + if err != nil { + return err + } + + secrets, err := getStackSecrets(ctx, client, namespace) + if err != nil { + return err + } + + if len(services)+len(networks)+len(secrets) == 0 { + fmt.Fprintf(dockerCli.Out(), "Nothing found in stack: %s\n", namespace) + continue + } + + hasError := removeServices(ctx, dockerCli, services) + hasError = removeSecrets(ctx, dockerCli, secrets) || hasError + hasError = removeNetworks(ctx, dockerCli, networks) || hasError + + if hasError { + errs = append(errs, fmt.Sprintf("Failed to remove some resources from stack: %s", namespace)) + } } - networks, err := getStackNetworks(ctx, client, namespace) - if err != nil { - return err - } - - secrets, err := getStackSecrets(ctx, client, namespace) - if err != nil { - return err - } - - if len(services)+len(networks)+len(secrets) == 0 { - fmt.Fprintf(dockerCli.Out(), "Nothing found in stack: %s\n", namespace) - return nil - } - - hasError := removeServices(ctx, dockerCli, services) - hasError = removeSecrets(ctx, dockerCli, secrets) || hasError - hasError = removeNetworks(ctx, dockerCli, networks) || hasError - - if hasError { - return errors.Errorf("Failed to remove some resources") + if len(errs) > 0 { + return errors.Errorf(strings.Join(errs, "\n")) } return nil } diff --git a/components/cli/command/stack/remove_test.go b/components/cli/command/stack/remove_test.go new file mode 100644 index 0000000000..7f64fb5505 --- /dev/null +++ b/components/cli/command/stack/remove_test.go @@ -0,0 +1,107 @@ +package stack + +import ( + "bytes" + "errors" + "strings" + "testing" + + "github.com/docker/docker/cli/internal/test" + "github.com/docker/docker/pkg/testutil/assert" +) + +func TestRemoveStack(t *testing.T) { + allServices := []string{ + objectName("foo", "service1"), + objectName("foo", "service2"), + objectName("bar", "service1"), + objectName("bar", "service2"), + } + allServicesIDs := buildObjectIDs(allServices) + + allNetworks := []string{ + objectName("foo", "network1"), + objectName("bar", "network1"), + } + allNetworksIDs := buildObjectIDs(allNetworks) + + allSecrets := []string{ + objectName("foo", "secret1"), + objectName("foo", "secret2"), + objectName("bar", "secret1"), + } + allSecretsIDs := buildObjectIDs(allSecrets) + + cli := &fakeClient{ + services: allServices, + networks: allNetworks, + secrets: allSecrets, + } + cmd := newRemoveCommand(test.NewFakeCli(cli, &bytes.Buffer{})) + cmd.SetArgs([]string{"foo", "bar"}) + + assert.NilError(t, cmd.Execute()) + assert.DeepEqual(t, cli.removedServices, allServicesIDs) + assert.DeepEqual(t, cli.removedNetworks, allNetworksIDs) + assert.DeepEqual(t, cli.removedSecrets, allSecretsIDs) +} + +func TestSkipEmptyStack(t *testing.T) { + buf := new(bytes.Buffer) + allServices := []string{objectName("bar", "service1"), objectName("bar", "service2")} + allServicesIDs := buildObjectIDs(allServices) + + allNetworks := []string{objectName("bar", "network1")} + allNetworksIDs := buildObjectIDs(allNetworks) + + allSecrets := []string{objectName("bar", "secret1")} + allSecretsIDs := buildObjectIDs(allSecrets) + + cli := &fakeClient{ + services: allServices, + networks: allNetworks, + secrets: allSecrets, + } + cmd := newRemoveCommand(test.NewFakeCli(cli, buf)) + cmd.SetArgs([]string{"foo", "bar"}) + + assert.NilError(t, cmd.Execute()) + assert.Contains(t, buf.String(), "Nothing found in stack: foo") + assert.DeepEqual(t, cli.removedServices, allServicesIDs) + assert.DeepEqual(t, cli.removedNetworks, allNetworksIDs) + assert.DeepEqual(t, cli.removedSecrets, allSecretsIDs) +} + +func TestContinueAfterError(t *testing.T) { + allServices := []string{objectName("foo", "service1"), objectName("bar", "service1")} + allServicesIDs := buildObjectIDs(allServices) + + allNetworks := []string{objectName("foo", "network1"), objectName("bar", "network1")} + allNetworksIDs := buildObjectIDs(allNetworks) + + allSecrets := []string{objectName("foo", "secret1"), objectName("bar", "secret1")} + allSecretsIDs := buildObjectIDs(allSecrets) + + removedServices := []string{} + cli := &fakeClient{ + services: allServices, + networks: allNetworks, + secrets: allSecrets, + + serviceRemoveFunc: func(serviceID string) error { + removedServices = append(removedServices, serviceID) + + if strings.Contains(serviceID, "foo") { + return errors.New("") + } + return nil + }, + } + cmd := newRemoveCommand(test.NewFakeCli(cli, &bytes.Buffer{})) + cmd.SetArgs([]string{"foo", "bar"}) + + assert.Error(t, cmd.Execute(), "Failed to remove some resources from stack: foo") + assert.DeepEqual(t, removedServices, allServicesIDs) + assert.DeepEqual(t, cli.removedNetworks, allNetworksIDs) + assert.DeepEqual(t, cli.removedSecrets, allSecretsIDs) +} From 2da2221b17e144ff160bbce64a96611a51450f38 Mon Sep 17 00:00:00 2001 From: David Sheets Date: Tue, 21 Feb 2017 12:07:45 -0800 Subject: [PATCH 710/978] build: accept -f - to read Dockerfile from stdin Heavily based on implementation by David Sheets Signed-off-by: David Sheets Signed-off-by: Tonis Tiigi Upstream-commit: 924af54d98c46d3a5d521d1854def994a8064536 Component: cli --- components/cli/command/image/build.go | 66 +++++++++++++++++-- components/cli/command/image/build/context.go | 31 +++++---- 2 files changed, 80 insertions(+), 17 deletions(-) diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go index b14b0356ca..f6984619c1 100644 --- a/components/cli/command/image/build.go +++ b/components/cli/command/image/build.go @@ -6,10 +6,12 @@ import ( "bytes" "fmt" "io" + "io/ioutil" "os" "path/filepath" "regexp" "runtime" + "time" "github.com/docker/distribution/reference" "github.com/docker/docker/api" @@ -25,6 +27,7 @@ import ( "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/pkg/progress" "github.com/docker/docker/pkg/streamformatter" + "github.com/docker/docker/pkg/stringid" "github.com/docker/docker/pkg/urlutil" runconfigopts "github.com/docker/docker/runconfig/opts" units "github.com/docker/go-units" @@ -141,6 +144,7 @@ func (out *lastProgressOutput) WriteProgress(prog progress.Progress) error { func runBuild(dockerCli *command.DockerCli, options buildOptions) error { var ( buildCtx io.ReadCloser + dockerfileCtx io.ReadCloser err error contextDir string tempDir string @@ -157,6 +161,13 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { buildBuff = bytes.NewBuffer(nil) } + if options.dockerfileName == "-" { + if specifiedContext == "-" { + return errors.New("invalid argument: can't use stdin for both build context and dockerfile") + } + dockerfileCtx = dockerCli.In() + } + switch { case specifiedContext == "-": buildCtx, relDockerfile, err = build.GetContextFromReader(dockerCli.In(), options.dockerfileName) @@ -214,11 +225,11 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { // removed. The daemon will remove them for us, if needed, after it // parses the Dockerfile. Ignore errors here, as they will have been // caught by validateContextDirectory above. - var includes = []string{"."} - keepThem1, _ := fileutils.Matches(".dockerignore", excludes) - keepThem2, _ := fileutils.Matches(relDockerfile, excludes) - if keepThem1 || keepThem2 { - includes = append(includes, ".dockerignore", relDockerfile) + if keep, _ := fileutils.Matches(".dockerignore", excludes); keep { + excludes = append(excludes, "!.dockerignore") + } + if keep, _ := fileutils.Matches(relDockerfile, excludes); keep && dockerfileCtx == nil { + excludes = append(excludes, "!"+relDockerfile) } compression := archive.Uncompressed @@ -228,13 +239,56 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { buildCtx, err = archive.TarWithOptions(contextDir, &archive.TarOptions{ Compression: compression, ExcludePatterns: excludes, - IncludeFiles: includes, }) if err != nil { return err } } + // replace Dockerfile if added dynamically + if dockerfileCtx != nil { + file, err := ioutil.ReadAll(dockerfileCtx) + dockerfileCtx.Close() + if err != nil { + return err + } + now := time.Now() + hdrTmpl := &tar.Header{ + Mode: 0600, + Uid: 0, + Gid: 0, + ModTime: now, + Typeflag: tar.TypeReg, + AccessTime: now, + ChangeTime: now, + } + randomName := ".dockerfile." + stringid.GenerateRandomID()[:20] + + buildCtx = archive.ReplaceFileTarWrapper(buildCtx, map[string]archive.TarModifierFunc{ + randomName: func(_ string, h *tar.Header, content io.Reader) (*tar.Header, []byte, error) { + return hdrTmpl, file, nil + }, + ".dockerignore": func(_ string, h *tar.Header, content io.Reader) (*tar.Header, []byte, error) { + if h == nil { + h = hdrTmpl + } + extraIgnore := randomName + "\n" + b := &bytes.Buffer{} + if content != nil { + _, err := b.ReadFrom(content) + if err != nil { + return nil, nil, err + } + } else { + extraIgnore += ".dockerignore\n" + } + b.Write([]byte("\n" + extraIgnore)) + return h, b.Bytes(), nil + }, + }) + relDockerfile = randomName + } + ctx := context.Background() var resolvedTags []*resolvedTag diff --git a/components/cli/command/image/build/context.go b/components/cli/command/image/build/context.go index 85d319e0b7..348c721931 100644 --- a/components/cli/command/image/build/context.go +++ b/components/cli/command/image/build/context.go @@ -89,6 +89,10 @@ func GetContextFromReader(r io.ReadCloser, dockerfileName string) (out io.ReadCl return ioutils.NewReadCloserWrapper(buf, func() error { return r.Close() }), dockerfileName, nil } + if dockerfileName == "-" { + return nil, "", errors.New("build context is not an archive") + } + // Input should be read as a Dockerfile. tmpDir, err := ioutil.TempDir("", "docker-build-context-") if err != nil { @@ -166,7 +170,7 @@ func GetContextFromLocalDir(localDir, dockerfileName string) (absContextDir, rel // When using a local context directory, when the Dockerfile is specified // with the `-f/--file` option then it is considered relative to the // current directory and not the context directory. - if dockerfileName != "" { + if dockerfileName != "" && dockerfileName != "-" { if dockerfileName, err = filepath.Abs(dockerfileName); err != nil { return "", "", errors.Errorf("unable to get absolute path to Dockerfile: %v", err) } @@ -220,6 +224,8 @@ func getDockerfileRelPath(givenContextDir, givenDockerfile string) (absContextDi absDockerfile = altPath } } + } else if absDockerfile == "-" { + absDockerfile = filepath.Join(absContextDir, DefaultDockerfileName) } // If not already an absolute path, the Dockerfile path should be joined to @@ -234,18 +240,21 @@ func getDockerfileRelPath(givenContextDir, givenDockerfile string) (absContextDi // an issue in golang. On Windows, EvalSymLinks does not work on UNC file // paths (those starting with \\). This hack means that when using links // on UNC paths, they will not be followed. - if !isUNC(absDockerfile) { - absDockerfile, err = filepath.EvalSymlinks(absDockerfile) - if err != nil { - return "", "", errors.Errorf("unable to evaluate symlinks in Dockerfile path: %v", err) - } - } + if givenDockerfile != "-" { + if !isUNC(absDockerfile) { + absDockerfile, err = filepath.EvalSymlinks(absDockerfile) + if err != nil { + return "", "", errors.Errorf("unable to evaluate symlinks in Dockerfile path: %v", err) - if _, err := os.Lstat(absDockerfile); err != nil { - if os.IsNotExist(err) { - return "", "", errors.Errorf("Cannot locate Dockerfile: %q", absDockerfile) + } + } + + if _, err := os.Lstat(absDockerfile); err != nil { + if os.IsNotExist(err) { + return "", "", errors.Errorf("Cannot locate Dockerfile: %q", absDockerfile) + } + return "", "", errors.Errorf("unable to stat Dockerfile: %v", err) } - return "", "", errors.Errorf("unable to stat Dockerfile: %v", err) } if relDockerfile, err = filepath.Rel(absContextDir, absDockerfile); err != nil { From a78cb013cac7a40e410c5d0a0a124e5d288fcd27 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 5 Apr 2017 12:09:26 -0400 Subject: [PATCH 711/978] Factor out adding dockerfile from stdin. Signed-off-by: Daniel Nephin Upstream-commit: 596cd38a6e3415bcf5ec12b7bec2dda36770c72c Component: cli --- components/cli/command/image/build.go | 83 +++++++++++++++------------ 1 file changed, 46 insertions(+), 37 deletions(-) diff --git a/components/cli/command/image/build.go b/components/cli/command/image/build.go index f6984619c1..965acb4b51 100644 --- a/components/cli/command/image/build.go +++ b/components/cli/command/image/build.go @@ -247,46 +247,10 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { // replace Dockerfile if added dynamically if dockerfileCtx != nil { - file, err := ioutil.ReadAll(dockerfileCtx) - dockerfileCtx.Close() + buildCtx, relDockerfile, err = addDockerfileToBuildContext(dockerfileCtx, buildCtx) if err != nil { return err } - now := time.Now() - hdrTmpl := &tar.Header{ - Mode: 0600, - Uid: 0, - Gid: 0, - ModTime: now, - Typeflag: tar.TypeReg, - AccessTime: now, - ChangeTime: now, - } - randomName := ".dockerfile." + stringid.GenerateRandomID()[:20] - - buildCtx = archive.ReplaceFileTarWrapper(buildCtx, map[string]archive.TarModifierFunc{ - randomName: func(_ string, h *tar.Header, content io.Reader) (*tar.Header, []byte, error) { - return hdrTmpl, file, nil - }, - ".dockerignore": func(_ string, h *tar.Header, content io.Reader) (*tar.Header, []byte, error) { - if h == nil { - h = hdrTmpl - } - extraIgnore := randomName + "\n" - b := &bytes.Buffer{} - if content != nil { - _, err := b.ReadFrom(content) - if err != nil { - return nil, nil, err - } - } else { - extraIgnore += ".dockerignore\n" - } - b.Write([]byte("\n" + extraIgnore)) - return h, b.Bytes(), nil - }, - }) - relDockerfile = randomName } ctx := context.Background() @@ -392,6 +356,51 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error { return nil } +func addDockerfileToBuildContext(dockerfileCtx io.ReadCloser, buildCtx io.ReadCloser) (io.ReadCloser, string, error) { + file, err := ioutil.ReadAll(dockerfileCtx) + dockerfileCtx.Close() + if err != nil { + return nil, "", err + } + now := time.Now() + hdrTmpl := &tar.Header{ + Mode: 0600, + Uid: 0, + Gid: 0, + ModTime: now, + Typeflag: tar.TypeReg, + AccessTime: now, + ChangeTime: now, + } + randomName := ".dockerfile." + stringid.GenerateRandomID()[:20] + + buildCtx = archive.ReplaceFileTarWrapper(buildCtx, map[string]archive.TarModifierFunc{ + // Add the dockerfile with a random filename + randomName: func(_ string, h *tar.Header, content io.Reader) (*tar.Header, []byte, error) { + return hdrTmpl, file, nil + }, + // Update .dockerignore to include the random filename + ".dockerignore": func(_ string, h *tar.Header, content io.Reader) (*tar.Header, []byte, error) { + if h == nil { + h = hdrTmpl + } + extraIgnore := randomName + "\n" + b := &bytes.Buffer{} + if content != nil { + _, err := b.ReadFrom(content) + if err != nil { + return nil, nil, err + } + } else { + extraIgnore += ".dockerignore\n" + } + b.Write([]byte("\n" + extraIgnore)) + return h, b.Bytes(), nil + }, + }) + return buildCtx, randomName, nil +} + func isLocalDir(c string) bool { _, err := os.Stat(c) return err == nil From 1a49893201e55250e5c964cecdef7ea9f3c787c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20Fax=C3=B6?= Date: Tue, 29 Nov 2016 10:58:47 +0100 Subject: [PATCH 712/978] Added start period option to health check. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Elias Faxö Upstream-commit: a58f798fdf87a6986e3c50538342e3b713421e9a Component: cli --- components/cli/command/container/opts.go | 16 ++++++++++---- components/cli/command/container/opts_test.go | 4 ++-- components/cli/command/service/opts.go | 18 +++++++++++----- components/cli/command/service/opts_test.go | 18 +++++++++------- components/cli/command/service/update.go | 8 +++++-- components/cli/command/service/update_test.go | 5 +++++ components/cli/compose/convert/service.go | 21 ++++++++++++------- components/cli/compose/types/types.go | 11 +++++----- 8 files changed, 68 insertions(+), 33 deletions(-) diff --git a/components/cli/command/container/opts.go b/components/cli/command/container/opts.go index fc4ac855d5..7480bfaced 100644 --- a/components/cli/command/container/opts.go +++ b/components/cli/command/container/opts.go @@ -113,6 +113,7 @@ type containerOptions struct { healthCmd string healthInterval time.Duration healthTimeout time.Duration + healthStartPeriod time.Duration healthRetries int runtime string autoRemove bool @@ -232,6 +233,8 @@ func addFlags(flags *pflag.FlagSet) *containerOptions { flags.DurationVar(&copts.healthInterval, "health-interval", 0, "Time between running the check (ns|us|ms|s|m|h) (default 0s)") flags.IntVar(&copts.healthRetries, "health-retries", 0, "Consecutive failures needed to report unhealthy") flags.DurationVar(&copts.healthTimeout, "health-timeout", 0, "Maximum time to allow one check to run (ns|us|ms|s|m|h) (default 0s)") + flags.DurationVar(&copts.healthStartPeriod, "health-start-period", 0, "Start period for the container to initialize before starting health-retries countdown (ns|us|ms|s|m|h) (default 0s)") + flags.SetAnnotation("health-start-period", "version", []string{"1.29"}) flags.BoolVar(&copts.noHealthcheck, "no-healthcheck", false, "Disable any container-specified HEALTHCHECK") // Resource management @@ -464,6 +467,7 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err haveHealthSettings := copts.healthCmd != "" || copts.healthInterval != 0 || copts.healthTimeout != 0 || + copts.healthStartPeriod != 0 || copts.healthRetries != 0 if copts.noHealthcheck { if haveHealthSettings { @@ -486,12 +490,16 @@ func parse(flags *pflag.FlagSet, copts *containerOptions) (*containerConfig, err if copts.healthRetries < 0 { return nil, errors.Errorf("--health-retries cannot be negative") } + if copts.healthStartPeriod < 0 { + return nil, fmt.Errorf("--health-start-period cannot be negative") + } healthConfig = &container.HealthConfig{ - Test: probe, - Interval: copts.healthInterval, - Timeout: copts.healthTimeout, - Retries: copts.healthRetries, + Test: probe, + Interval: copts.healthInterval, + Timeout: copts.healthTimeout, + StartPeriod: copts.healthStartPeriod, + Retries: copts.healthRetries, } } diff --git a/components/cli/command/container/opts_test.go b/components/cli/command/container/opts_test.go index b628c0b625..575b214edc 100644 --- a/components/cli/command/container/opts_test.go +++ b/components/cli/command/container/opts_test.go @@ -501,8 +501,8 @@ func TestParseHealth(t *testing.T) { checkError("--no-healthcheck conflicts with --health-* options", "--no-healthcheck", "--health-cmd=/check.sh -q", "img", "cmd") - health = checkOk("--health-timeout=2s", "--health-retries=3", "--health-interval=4.5s", "img", "cmd") - if health.Timeout != 2*time.Second || health.Retries != 3 || health.Interval != 4500*time.Millisecond { + health = checkOk("--health-timeout=2s", "--health-retries=3", "--health-interval=4.5s", "--health-start-period=5s", "img", "cmd") + if health.Timeout != 2*time.Second || health.Retries != 3 || health.Interval != 4500*time.Millisecond || health.StartPeriod != 5*time.Second { t.Fatalf("--health-*: got %#v", health) } } diff --git a/components/cli/command/service/opts.go b/components/cli/command/service/opts.go index cdfe513177..3300f34d83 100644 --- a/components/cli/command/service/opts.go +++ b/components/cli/command/service/opts.go @@ -282,6 +282,7 @@ type healthCheckOptions struct { interval PositiveDurationOpt timeout PositiveDurationOpt retries int + startPeriod PositiveDurationOpt noHealthcheck bool } @@ -301,18 +302,22 @@ func (opts *healthCheckOptions) toHealthConfig() (*container.HealthConfig, error if opts.cmd != "" { test = []string{"CMD-SHELL", opts.cmd} } - var interval, timeout time.Duration + var interval, timeout, startPeriod time.Duration if ptr := opts.interval.Value(); ptr != nil { interval = *ptr } if ptr := opts.timeout.Value(); ptr != nil { timeout = *ptr } + if ptr := opts.startPeriod.Value(); ptr != nil { + startPeriod = *ptr + } healthConfig = &container.HealthConfig{ - Test: test, - Interval: interval, - Timeout: timeout, - Retries: opts.retries, + Test: test, + Interval: interval, + Timeout: timeout, + Retries: opts.retries, + StartPeriod: startPeriod, } } return healthConfig, nil @@ -555,6 +560,8 @@ func addServiceFlags(flags *pflag.FlagSet, opts *serviceOptions) { flags.SetAnnotation(flagHealthTimeout, "version", []string{"1.25"}) flags.IntVar(&opts.healthcheck.retries, flagHealthRetries, 0, "Consecutive failures needed to report unhealthy") flags.SetAnnotation(flagHealthRetries, "version", []string{"1.25"}) + flags.Var(&opts.healthcheck.startPeriod, flagHealthStartPeriod, "Start period for the container to initialize before counting retries towards unstable (ns|us|ms|s|m|h)") + flags.SetAnnotation(flagHealthStartPeriod, "version", []string{"1.29"}) flags.BoolVar(&opts.healthcheck.noHealthcheck, flagNoHealthcheck, false, "Disable any container-specified HEALTHCHECK") flags.SetAnnotation(flagNoHealthcheck, "version", []string{"1.25"}) @@ -644,6 +651,7 @@ const ( flagHealthInterval = "health-interval" flagHealthRetries = "health-retries" flagHealthTimeout = "health-timeout" + flagHealthStartPeriod = "health-start-period" flagNoHealthcheck = "no-healthcheck" flagSecret = "secret" flagSecretAdd = "secret-add" diff --git a/components/cli/command/service/opts_test.go b/components/cli/command/service/opts_test.go index ac5106793b..46db5fc838 100644 --- a/components/cli/command/service/opts_test.go +++ b/components/cli/command/service/opts_test.go @@ -71,18 +71,20 @@ func TestUint64OptSetAndValue(t *testing.T) { func TestHealthCheckOptionsToHealthConfig(t *testing.T) { dur := time.Second opt := healthCheckOptions{ - cmd: "curl", - interval: PositiveDurationOpt{DurationOpt{value: &dur}}, - timeout: PositiveDurationOpt{DurationOpt{value: &dur}}, - retries: 10, + cmd: "curl", + interval: PositiveDurationOpt{DurationOpt{value: &dur}}, + timeout: PositiveDurationOpt{DurationOpt{value: &dur}}, + startPeriod: PositiveDurationOpt{DurationOpt{value: &dur}}, + retries: 10, } config, err := opt.toHealthConfig() assert.NilError(t, err) assert.Equal(t, reflect.DeepEqual(config, &container.HealthConfig{ - Test: []string{"CMD-SHELL", "curl"}, - Interval: time.Second, - Timeout: time.Second, - Retries: 10, + Test: []string{"CMD-SHELL", "curl"}, + Interval: time.Second, + Timeout: time.Second, + StartPeriod: time.Second, + Retries: 10, }), true) } diff --git a/components/cli/command/service/update.go b/components/cli/command/service/update.go index afa0f807e9..b2d77e6bad 100644 --- a/components/cli/command/service/update.go +++ b/components/cli/command/service/update.go @@ -897,7 +897,7 @@ func updateLogDriver(flags *pflag.FlagSet, taskTemplate *swarm.TaskSpec) error { } func updateHealthcheck(flags *pflag.FlagSet, containerSpec *swarm.ContainerSpec) error { - if !anyChanged(flags, flagNoHealthcheck, flagHealthCmd, flagHealthInterval, flagHealthRetries, flagHealthTimeout) { + if !anyChanged(flags, flagNoHealthcheck, flagHealthCmd, flagHealthInterval, flagHealthRetries, flagHealthTimeout, flagHealthStartPeriod) { return nil } if containerSpec.Healthcheck == nil { @@ -908,7 +908,7 @@ func updateHealthcheck(flags *pflag.FlagSet, containerSpec *swarm.ContainerSpec) return err } if noHealthcheck { - if !anyChanged(flags, flagHealthCmd, flagHealthInterval, flagHealthRetries, flagHealthTimeout) { + if !anyChanged(flags, flagHealthCmd, flagHealthInterval, flagHealthRetries, flagHealthTimeout, flagHealthStartPeriod) { containerSpec.Healthcheck = &container.HealthConfig{ Test: []string{"NONE"}, } @@ -927,6 +927,10 @@ func updateHealthcheck(flags *pflag.FlagSet, containerSpec *swarm.ContainerSpec) val := *flags.Lookup(flagHealthTimeout).Value.(*PositiveDurationOpt).Value() containerSpec.Healthcheck.Timeout = val } + if flags.Changed(flagHealthStartPeriod) { + val := *flags.Lookup(flagHealthStartPeriod).Value.(*PositiveDurationOpt).Value() + containerSpec.Healthcheck.StartPeriod = val + } if flags.Changed(flagHealthRetries) { containerSpec.Healthcheck.Retries, _ = flags.GetInt(flagHealthRetries) } diff --git a/components/cli/command/service/update_test.go b/components/cli/command/service/update_test.go index d71e065f91..7a588d7fef 100644 --- a/components/cli/command/service/update_test.go +++ b/components/cli/command/service/update_test.go @@ -311,6 +311,11 @@ func TestUpdateHealthcheckTable(t *testing.T) { initial: &container.HealthConfig{Test: []string{"CMD", "cmd1"}, Retries: 10}, expected: &container.HealthConfig{Test: []string{"CMD", "cmd1"}}, }, + { + flags: [][2]string{{"health-start-period", "1m"}}, + initial: &container.HealthConfig{Test: []string{"CMD", "cmd1"}}, + expected: &container.HealthConfig{Test: []string{"CMD", "cmd1"}, StartPeriod: time.Minute}, + }, { flags: [][2]string{{"health-cmd", "cmd1"}, {"no-healthcheck", "true"}}, err: "--no-healthcheck conflicts with --health-* options", diff --git a/components/cli/compose/convert/service.go b/components/cli/compose/convert/service.go index fe9c281ae9..7af24b2ec7 100644 --- a/components/cli/compose/convert/service.go +++ b/components/cli/compose/convert/service.go @@ -255,9 +255,9 @@ func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container return nil, nil } var ( - err error - timeout, interval time.Duration - retries int + err error + timeout, interval, startPeriod time.Duration + retries int ) if healthcheck.Disable { if len(healthcheck.Test) != 0 { @@ -280,14 +280,21 @@ func convertHealthcheck(healthcheck *composetypes.HealthCheckConfig) (*container return nil, err } } + if healthcheck.StartPeriod != "" { + startPeriod, err = time.ParseDuration(healthcheck.StartPeriod) + if err != nil { + return nil, err + } + } if healthcheck.Retries != nil { retries = int(*healthcheck.Retries) } return &container.HealthConfig{ - Test: healthcheck.Test, - Timeout: timeout, - Interval: interval, - Retries: retries, + Test: healthcheck.Test, + Timeout: timeout, + Interval: interval, + Retries: retries, + StartPeriod: startPeriod, }, nil } diff --git a/components/cli/compose/types/types.go b/components/cli/compose/types/types.go index 1b4c4015ec..1a3772dada 100644 --- a/components/cli/compose/types/types.go +++ b/components/cli/compose/types/types.go @@ -163,11 +163,12 @@ type DeployConfig struct { // HealthCheckConfig the healthcheck configuration for a service type HealthCheckConfig struct { - Test HealthCheckTest - Timeout string - Interval string - Retries *uint64 - Disable bool + Test HealthCheckTest + Timeout string + Interval string + Retries *uint64 + StartPeriod string + Disable bool } // HealthCheckTest is the command run to test the health of a service From e8b7dd6280bed955d1fc43a546daf7d599467365 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 6 Apr 2017 10:32:35 -0400 Subject: [PATCH 713/978] Support rw as a volume option in compose file. Signed-off-by: Daniel Nephin Upstream-commit: 95b81eb68467897fdfb3cf68187403bfe4e1aa5a Component: cli --- components/cli/compose/loader/volume.go | 2 ++ components/cli/compose/loader/volume_test.go | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/components/cli/compose/loader/volume.go b/components/cli/compose/loader/volume.go index 3f33492ea7..4dce1b2ef5 100644 --- a/components/cli/compose/loader/volume.go +++ b/components/cli/compose/loader/volume.go @@ -70,6 +70,8 @@ func populateFieldFromBuffer(char rune, buffer []rune, volume *types.ServiceVolu switch option { case "ro": volume.ReadOnly = true + case "rw": + volume.ReadOnly = false case "nocopy": volume.Volume = &types.ServiceVolumeVolume{NoCopy: true} default: diff --git a/components/cli/compose/loader/volume_test.go b/components/cli/compose/loader/volume_test.go index 0735d5a54a..19d19f2306 100644 --- a/components/cli/compose/loader/volume_test.go +++ b/components/cli/compose/loader/volume_test.go @@ -132,3 +132,17 @@ func TestParseVolumeWithReadOnly(t *testing.T) { assert.DeepEqual(t, volume, expected) } } + +func TestParseVolumeWithRW(t *testing.T) { + for _, path := range []string{"./foo", "/home/user"} { + volume, err := parseVolume(path + ":/target:rw") + expected := types.ServiceVolumeConfig{ + Type: "bind", + Source: path, + Target: "/target", + ReadOnly: false, + } + assert.NilError(t, err) + assert.DeepEqual(t, volume, expected) + } +} From da58db9db717d684882156623b38db16e77ebd47 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sat, 4 Feb 2017 09:10:05 -0800 Subject: [PATCH 714/978] 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 Upstream-commit: 41471dfe1cb0432871812deecde650888e401d26 Component: cli --- components/cli/command/container/prune.go | 2 +- components/cli/command/image/prune.go | 1 + components/cli/command/network/prune.go | 2 +- components/cli/command/prune/prune.go | 2 +- components/cli/command/utils.go | 32 +++++++++++++++++++++++ components/cli/command/volume/prune.go | 16 +++++++----- components/cli/config/configfile/file.go | 1 + 7 files changed, 47 insertions(+), 9 deletions(-) diff --git a/components/cli/command/container/prune.go b/components/cli/command/container/prune.go index ca50e2e159..cf12dc71fe 100644 --- a/components/cli/command/container/prune.go +++ b/components/cli/command/container/prune.go @@ -49,7 +49,7 @@ const warning = `WARNING! This will remove all stopped containers. Are you sure you want to continue?` func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed uint64, output string, err error) { - pruneFilters := opts.filter.Value() + pruneFilters := command.PruneFilters(dockerCli, opts.filter.Value()) if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) { return diff --git a/components/cli/command/image/prune.go b/components/cli/command/image/prune.go index f17aed7410..f86bae39cc 100644 --- a/components/cli/command/image/prune.go +++ b/components/cli/command/image/prune.go @@ -58,6 +58,7 @@ Are you sure you want to continue?` func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed uint64, output string, err error) { pruneFilters := opts.filter.Value() pruneFilters.Add("dangling", fmt.Sprintf("%v", !opts.all)) + pruneFilters = command.PruneFilters(dockerCli, pruneFilters) warning := danglingWarning if opts.all { diff --git a/components/cli/command/network/prune.go b/components/cli/command/network/prune.go index c5c5359926..ec363ab914 100644 --- a/components/cli/command/network/prune.go +++ b/components/cli/command/network/prune.go @@ -48,7 +48,7 @@ const warning = `WARNING! This will remove all networks not used by at least one Are you sure you want to continue?` func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (output string, err error) { - pruneFilters := opts.filter.Value() + pruneFilters := command.PruneFilters(dockerCli, opts.filter.Value()) if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) { return diff --git a/components/cli/command/prune/prune.go b/components/cli/command/prune/prune.go index 6314718c69..26153ed7c1 100644 --- a/components/cli/command/prune/prune.go +++ b/components/cli/command/prune/prune.go @@ -37,7 +37,7 @@ func RunContainerPrune(dockerCli *command.DockerCli, filter opts.FilterOpt) (uin // RunVolumePrune executes a prune command for volumes func RunVolumePrune(dockerCli *command.DockerCli, filter opts.FilterOpt) (uint64, string, error) { - return volume.RunPrune(dockerCli) + return volume.RunPrune(dockerCli, filter) } // RunImagePrune executes a prune command for images diff --git a/components/cli/command/utils.go b/components/cli/command/utils.go index 4c52ce61b2..853fe11c78 100644 --- a/components/cli/command/utils.go +++ b/components/cli/command/utils.go @@ -9,6 +9,7 @@ import ( "runtime" "strings" + "github.com/docker/docker/api/types/filters" "github.com/docker/docker/pkg/system" ) @@ -85,3 +86,34 @@ func PromptForConfirmation(ins *InStream, outs *OutStream, message string) bool answer, _, _ := reader.ReadLine() return strings.ToLower(string(answer)) == "y" } + +// PruneFilters returns consolidated prune filters obtained from config.json and cli +func PruneFilters(dockerCli Cli, pruneFilters filters.Args) filters.Args { + if dockerCli.ConfigFile() == nil { + return pruneFilters + } + for _, f := range dockerCli.ConfigFile().PruneFilters { + parts := strings.SplitN(f, "=", 2) + if len(parts) != 2 { + continue + } + if parts[0] == "label" { + // CLI label filter supersede config.json. + // If CLI label filter conflict with config.json, + // skip adding label! filter in config.json. + if pruneFilters.Include("label!") && pruneFilters.ExactMatch("label!", parts[1]) { + continue + } + } else if parts[0] == "label!" { + // CLI label! filter supersede config.json. + // If CLI label! filter conflict with config.json, + // skip adding label filter in config.json. + if pruneFilters.Include("label") && pruneFilters.ExactMatch("label", parts[1]) { + continue + } + } + pruneFilters.Add(parts[0], parts[1]) + } + + return pruneFilters +} diff --git a/components/cli/command/volume/prune.go b/components/cli/command/volume/prune.go index 7e78c66e07..f7d823ffac 100644 --- a/components/cli/command/volume/prune.go +++ b/components/cli/command/volume/prune.go @@ -3,21 +3,22 @@ package volume import ( "fmt" - "github.com/docker/docker/api/types/filters" "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" + "github.com/docker/docker/opts" units "github.com/docker/go-units" "github.com/spf13/cobra" "golang.org/x/net/context" ) type pruneOptions struct { - force bool + force bool + filter opts.FilterOpt } // NewPruneCommand returns a new cobra prune command for volumes func NewPruneCommand(dockerCli command.Cli) *cobra.Command { - var opts pruneOptions + opts := pruneOptions{filter: opts.NewFilterOpt()} cmd := &cobra.Command{ Use: "prune [OPTIONS]", @@ -39,6 +40,7 @@ func NewPruneCommand(dockerCli command.Cli) *cobra.Command { flags := cmd.Flags() flags.BoolVarP(&opts.force, "force", "f", false, "Do not prompt for confirmation") + flags.Var(&opts.filter, "filter", "Provide filter values (e.g. 'label=