From c023f818aa9f1c4bee81079a4ae1cef45555429a Mon Sep 17 00:00:00 2001 From: Tibor Vass Date: Tue, 5 May 2015 00:18:28 -0400 Subject: [PATCH 001/563] 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 --- cli.go | 200 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ client.go | 12 ++++ common.go | 20 ++++++ 3 files changed, 232 insertions(+) create mode 100644 cli.go create mode 100644 client.go create mode 100644 common.go diff --git a/cli.go b/cli.go new file mode 100644 index 000000000..8e559fc3f --- /dev/null +++ b/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/client.go b/client.go new file mode 100644 index 000000000..6a82eb52a --- /dev/null +++ b/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/common.go b/common.go new file mode 100644 index 000000000..85a02ac43 --- /dev/null +++ b/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 2734a5821ce831305eaf140e6b35d217966e7631 Mon Sep 17 00:00:00 2001 From: Lei Jitang Date: Thu, 8 Oct 2015 08:46:21 -0400 Subject: [PATCH 002/563] Use consistent command description Signed-off-by: Lei Jitang --- common.go | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/common.go b/common.go index 85a02ac43..d3aa391be 100644 --- a/common.go +++ b/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 22e3fabb45d855f2258ea81c81f242c4f6847f4d Mon Sep 17 00:00:00 2001 From: Madhu Venugopal Date: Thu, 15 Oct 2015 03:10:39 -0700 Subject: [PATCH 003/563] Added `network` to docker --help and help cleanup Fixes https://github.com/docker/docker/issues/16909 Signed-off-by: Madhu Venugopal --- common.go | 1 + 1 file changed, 1 insertion(+) diff --git a/common.go b/common.go index d3aa391be..c03d9a90e 100644 --- a/common.go +++ b/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 c9a59eb6440a098e587b1b77311c86f9735e7141 Mon Sep 17 00:00:00 2001 From: Qiang Huang Date: Mon, 28 Dec 2015 19:19:26 +0800 Subject: [PATCH 004/563] 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 --- common.go | 1 + 1 file changed, 1 insertion(+) diff --git a/common.go b/common.go index c03d9a90e..a1f3646d8 100644 --- a/common.go +++ b/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 96832973488c4860de407bb3c0b6226cf7a1c4c4 Mon Sep 17 00:00:00 2001 From: David Calavera Date: Tue, 29 Dec 2015 19:27:12 -0500 Subject: [PATCH 005/563] 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 --- common.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common.go b/common.go index a1f3646d8..1ece1fb61 100644 --- a/common.go +++ b/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 c73cd919b4de6e70f81654ec92a2bee3a4f3ab0a Mon Sep 17 00:00:00 2001 From: huqun Date: Fri, 12 Feb 2016 16:11:31 +0800 Subject: [PATCH 006/563] 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 --- common.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common.go b/common.go index 1ece1fb61..880ef6c80 100644 --- a/common.go +++ b/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 7a30e41b8437923d79d7f298aca76f3ac045f6d5 Mon Sep 17 00:00:00 2001 From: Zhang Wei Date: Mon, 4 Jan 2016 23:58:20 +0800 Subject: [PATCH 007/563] 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 --- common.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common.go b/common.go index 880ef6c80..d2fa93d88 100644 --- a/common.go +++ b/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 38f2513340ae07bb4834d0b0d3b2934f0a7ef812 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 1 Mar 2016 17:28:42 +0100 Subject: [PATCH 008/563] 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 --- common.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common.go b/common.go index d2fa93d88..df6a6ec11 100644 --- a/common.go +++ b/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 54e7de9b12677a607e3df5c8dee2cd69229697be Mon Sep 17 00:00:00 2001 From: Martin Mosegaard Amdisen Date: Mon, 21 Mar 2016 15:15:40 +0100 Subject: [PATCH 009/563] 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 --- common.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common.go b/common.go index df6a6ec11..0b2f0e6e2 100644 --- a/common.go +++ b/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 bcd0ac71aea666eca668cab98c883ac0703c9ae9 Mon Sep 17 00:00:00 2001 From: Martin Mosegaard Amdisen Date: Tue, 22 Mar 2016 08:16:52 +0100 Subject: [PATCH 010/563] Update 'save' command help Based on review feedback. Signed-off-by: Martin Mosegaard Amdisen --- common.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common.go b/common.go index 0b2f0e6e2..7f6a24ba1 100644 --- a/common.go +++ b/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 57171ee83caaef435bc20a95b9efe39b14e1dbe2 Mon Sep 17 00:00:00 2001 From: allencloud Date: Sat, 26 Mar 2016 22:06:45 +0800 Subject: [PATCH 011/563] fix typos Signed-off-by: allencloud --- cli.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli.go b/cli.go index 8e559fc3f..88c6e68c2 100644 --- a/cli.go +++ b/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 0c4f21fee36b73c54c9f514978e8927b8564d6c8 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 19 Feb 2016 17:42:51 -0500 Subject: [PATCH 012/563] 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 --- flags/common.go | 110 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 flags/common.go diff --git a/flags/common.go b/flags/common.go new file mode 100644 index 000000000..d23696979 --- /dev/null +++ b/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 a5c08fdbf0a32a39f047812025d3219ee01c8015 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 21 Apr 2016 17:51:28 -0400 Subject: [PATCH 013/563] 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 --- client.go => flags/client.go | 2 +- flags/common.go | 21 +++++++++++++++++---- common.go => usage.go | 19 ------------------- 3 files changed, 18 insertions(+), 24 deletions(-) rename client.go => flags/client.go (94%) rename common.go => usage.go (86%) diff --git a/client.go b/flags/client.go similarity index 94% rename from client.go rename to flags/client.go index 6a82eb52a..cc7309db4 100644 --- a/client.go +++ b/flags/client.go @@ -1,4 +1,4 @@ -package cli +package flags import flag "github.com/docker/docker/pkg/mflag" diff --git a/flags/common.go b/flags/common.go index d23696979..4726b04f2 100644 --- a/flags/common.go +++ b/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/common.go b/usage.go similarity index 86% rename from common.go rename to usage.go index 7f6a24ba1..4b0eb0e0c 100644 --- a/common.go +++ b/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 2bc929b019f5f1c2149c886e487c78a31123bf77 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 25 Apr 2016 12:05:42 -0400 Subject: [PATCH 014/563] Consolidate the files in client/ Signed-off-by: Daniel Nephin --- usage.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/usage.go b/usage.go index 4b0eb0e0c..1ef6a35de 100644 --- a/usage.go +++ b/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 89d78abcdc393e8008636c4fad7c4fad45e1a759 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Fri, 29 Apr 2016 11:16:34 -0400 Subject: [PATCH 015/563] 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 --- cli.go | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/cli.go b/cli.go index 88c6e68c2..12649df6d 100644 --- a/cli.go +++ b/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 79b8543b546aa40f6f1017eaf4b24d603a777c5c Mon Sep 17 00:00:00 2001 From: muge Date: Mon, 16 May 2016 09:38:04 +0800 Subject: [PATCH 016/563] cli: remove unnecessary initErr type Signed-off-by: ZhangHang Signed-off-by: Alexander Morozov --- cli.go | 55 ++++++++++++++++++++++++++----------------------------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/cli.go b/cli.go index 12649df6d..f6d48d6fa 100644 --- a/cli.go +++ b/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 4786ccd05c2785b2cc0ef4d7e3c33cbaeff8ef73 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 19 Apr 2016 12:59:48 -0400 Subject: [PATCH 017/563] Migrate volume commands to cobra. Signed-off-by: Daniel Nephin --- cobraadaptor/adaptor.go | 83 +++++++++++++++++++++++++++++++++++++++++ required.go | 23 ++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 cobraadaptor/adaptor.go create mode 100644 required.go diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go new file mode 100644 index 000000000..07ff8124b --- /dev/null +++ b/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/required.go b/required.go new file mode 100644 index 000000000..6b83fadde --- /dev/null +++ b/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 13cea4e58d0a0ab729849e5cd11ed48a30130341 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 16 May 2016 17:20:29 -0400 Subject: [PATCH 018/563] Update usage and help to (almost) match the existing docker behaviour Signed-off-by: Daniel Nephin --- cobraadaptor/adaptor.go | 28 ++++++++++++++++++---------- required.go | 14 ++++++++++++++ usage.go | 1 - 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index 07ff8124b..35b77b47e 100644 --- a/cobraadaptor/adaptor.go +++ b/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/required.go b/required.go index 6b83fadde..db1a98bd5 100644 --- a/required.go +++ b/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/usage.go b/usage.go index 1ef6a35de..9ddc17326 100644 --- a/usage.go +++ b/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 11ede593799f2c5ff6be8fca986c6122d274d7d8 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 25 May 2016 14:57:18 -0700 Subject: [PATCH 019/563] Support usage messages on bad flags. Signed-off-by: Daniel Nephin --- cobraadaptor/adaptor.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index 35b77b47e..cfa1b7f04 100644 --- a/cobraadaptor/adaptor.go +++ b/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 25892d27be5dd10dac694fd4d5ae317882c32fc1 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 26 May 2016 14:57:31 -0700 Subject: [PATCH 020/563] Use Args in cobra.Command to validate args. Also re-use context. Signed-off-by: Daniel Nephin --- required.go | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/required.go b/required.go index db1a98bd5..94710374e 100644 --- a/required.go +++ b/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 82c85e1e83f2cf040e69c1a3b3b1ab66538a06bd Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 31 May 2016 14:47:51 -0700 Subject: [PATCH 021/563] Make the -h flag deprecated. Signed-off-by: Daniel Nephin --- cobraadaptor/adaptor.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index cfa1b7f04..db16166f0 100644 --- a/cobraadaptor/adaptor.go +++ b/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 bbefa88a8c4014f368cd6e8cd16800fe8ca6f542 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 3 Jun 2016 19:50:01 +0200 Subject: [PATCH 022/563] Use spf13/cobra for docker search - Move image command search to `api/client/image/search.go` - Use cobra :) Signed-off-by: Vincent Demeester --- cobraadaptor/adaptor.go | 2 ++ required.go | 16 ++++++++++++++++ usage.go | 1 - 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index db16166f0..633844a12 100644 --- a/cobraadaptor/adaptor.go +++ b/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/required.go b/required.go index 94710374e..b3ebb9ba9 100644 --- a/required.go +++ b/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/usage.go b/usage.go index 9ddc17326..98d7fdf44 100644 --- a/usage.go +++ b/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 396c0660abcbd4d49db195c6b0cc14838e96a881 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 31 May 2016 16:49:32 -0700 Subject: [PATCH 023/563] 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 --- cobraadaptor/adaptor.go | 6 ++++-- usage.go | 1 - 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index 633844a12..7e6327ac2 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index 98d7fdf44..324d1d92b 100644 --- a/usage.go +++ b/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 69d30376353f1b86eed94135f47fb625be7182a5 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 31 May 2016 22:19:13 -0700 Subject: [PATCH 024/563] Convert 'docker create' to use cobra and pflag Return the correct status code on flag parsins errors. Signed-off-by: Daniel Nephin --- cobraadaptor/adaptor.go | 19 ++----------------- flagerrors.go | 21 +++++++++++++++++++++ usage.go | 1 - 3 files changed, 23 insertions(+), 18 deletions(-) create mode 100644 flagerrors.go diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index 7e6327ac2..719eaf4e1 100644 --- a/cobraadaptor/adaptor.go +++ b/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/flagerrors.go b/flagerrors.go new file mode 100644 index 000000000..aab8a9884 --- /dev/null +++ b/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/usage.go b/usage.go index 324d1d92b..d6c97c32f 100644 --- a/usage.go +++ b/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 4a7a5f3a57a967c0dd0658658c96c21b8c172d29 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Sat, 4 Jun 2016 16:19:54 +0200 Subject: [PATCH 025/563] Display "See 'docker cmd --help'." in error cases This brings back this message in case missing arguments. Signed-off-by: Vincent Demeester --- required.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/required.go b/required.go index b3ebb9ba9..1bbd55f52 100644 --- a/required.go +++ b/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 6ee903eea0678533d7c456704c6e87219737378d Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Sun, 5 Jun 2016 16:42:19 +0200 Subject: [PATCH 026/563] Migrate export command to cobra Signed-off-by: Vincent Demeester --- cobraadaptor/adaptor.go | 1 + usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index 719eaf4e1..a838f2817 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index d6c97c32f..4a22afaad 100644 --- a/usage.go +++ b/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 4770a4ba82d7e5ad26a845279f7f96cc585288c1 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 5 Jun 2016 10:25:21 -0700 Subject: [PATCH 027/563] 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 --- cobraadaptor/adaptor.go | 1 + usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index 719eaf4e1..40672bd8b 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index d6c97c32f..fb0de5cbe 100644 --- a/usage.go +++ b/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 894cc1f20183e266f3e687bba1c657157c5932ee Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Sun, 5 Jun 2016 22:40:35 +0200 Subject: [PATCH 028/563] Use spf13/cobra for docker rmi Moves image command rmi to `api/client/image/remove.go` and use cobra :) Signed-off-by: Vincent Demeester --- cobraadaptor/adaptor.go | 1 + usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index cc74cce34..139b9b1ec 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index 86bd1fcec..1bcce75a3 100644 --- a/usage.go +++ b/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 65fed1bca2a23d22ee7299bd45e47d4f50cd3269 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 5 Jun 2016 15:13:55 -0700 Subject: [PATCH 029/563] 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 --- cobraadaptor/adaptor.go | 1 + usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index cc74cce34..ab1b0c6cc 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index 86bd1fcec..99d7b6d70 100644 --- a/usage.go +++ b/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 a5c6af94b1e48dca2b94661f6258c5e70701faa9 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 5 Jun 2016 15:50:48 -0700 Subject: [PATCH 030/563] 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 --- cobraadaptor/adaptor.go | 1 + usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index 139b9b1ec..a5dc7de8d 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index 1bcce75a3..1235d45aa 100644 --- a/usage.go +++ b/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 316ab12eedcf450e84e7a64666ca3e094cea130a Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 5 Jun 2016 11:43:07 -0700 Subject: [PATCH 031/563] 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 --- cobraadaptor/adaptor.go | 1 + usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index 139b9b1ec..941b5e094 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index 1bcce75a3..97cfb9d48 100644 --- a/usage.go +++ b/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 217e98c710075a691fad7ef8278fc9d2e2482be5 Mon Sep 17 00:00:00 2001 From: Zhang Wei Date: Sun, 5 Jun 2016 23:03:58 +0800 Subject: [PATCH 032/563] Migrate start command to cobra Signed-off-by: Zhang Wei --- cobraadaptor/adaptor.go | 1 + usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index 139b9b1ec..bc497dfe9 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index 1bcce75a3..f62e462b1 100644 --- a/usage.go +++ b/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 fac425608a53091aa9033c37cac1eea127356e26 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 6 Jun 2016 10:28:52 +0200 Subject: [PATCH 033/563] 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 --- cobraadaptor/adaptor.go | 2 ++ usage.go | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index a3df870cb..4ceb57757 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index b5d9e904a..f6152e5c9 100644 --- a/usage.go +++ b/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 bbf4cd7b56f044ef43de212870e6be3bbd98ae22 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 6 Jun 2016 13:58:23 +0200 Subject: [PATCH 034/563] Migrate import command to cobra Signed-off-by: Vincent Demeester --- cobraadaptor/adaptor.go | 1 + usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index 4ceb57757..05d726dac 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index f6152e5c9..b60395073 100644 --- a/usage.go +++ b/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 b4421407a0417a20d72d0fc1cb3386c1360ca65f Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 5 Jun 2016 17:25:22 -0700 Subject: [PATCH 035/563] 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 --- cobraadaptor/adaptor.go | 1 + usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index 4ceb57757..ee51435b8 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index f6152e5c9..23c357e1c 100644 --- a/usage.go +++ b/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 c289179c99c9b66f798f19e8bc39152a6d4d0ed3 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 5 Jun 2016 16:48:26 -0700 Subject: [PATCH 036/563] 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 --- cobraadaptor/adaptor.go | 1 + required.go | 18 ++++++++++++++++++ usage.go | 1 - 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index 4ceb57757..b873e9cae 100644 --- a/cobraadaptor/adaptor.go +++ b/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/required.go b/required.go index 1bbd55f52..0cf35b3d6 100644 --- a/required.go +++ b/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/usage.go b/usage.go index f6152e5c9..fc232b5ec 100644 --- a/usage.go +++ b/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 6829d53a08940730ad681c473ec131e52ebafa2b Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Mon, 6 Jun 2016 06:38:43 -0700 Subject: [PATCH 037/563] 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 --- cobraadaptor/adaptor.go | 1 + usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index 4ceb57757..7c839c628 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index f6152e5c9..31eda11b9 100644 --- a/usage.go +++ b/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 096f7f72bf289b780381e5b858020d07f2cf9241 Mon Sep 17 00:00:00 2001 From: Zhang Wei Date: Mon, 6 Jun 2016 00:17:39 +0800 Subject: [PATCH 038/563] Move attach command to cobra. Signed-off-by: Zhang Wei --- cobraadaptor/adaptor.go | 1 + usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index 4ceb57757..cf3966f14 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index f6152e5c9..6a42f9281 100644 --- a/usage.go +++ b/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 0090463cabd9ebcb94539763ecd26ccbe471de7f Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 5 Jun 2016 19:54:33 -0700 Subject: [PATCH 039/563] 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 --- cobraadaptor/adaptor.go | 1 + usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index ee51435b8..f1d97002c 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index 23c357e1c..b9563cb15 100644 --- a/usage.go +++ b/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 55d46e8352713ef50a2f888fc642c8855bcc3362 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 6 Jun 2016 14:17:04 -0400 Subject: [PATCH 040/563] Fix a panic when the DOCKER_HOST was invalid using cobra commands. Signed-off-by: Daniel Nephin --- cobraadaptor/adaptor.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index ca7d4e54c..342c1f878 100644 --- a/cobraadaptor/adaptor.go +++ b/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 084a028e84e19580f8788e14224767239e8f399f Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 5 Jun 2016 16:12:59 -0700 Subject: [PATCH 041/563] 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 --- cobraadaptor/adaptor.go | 1 + usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index ca7d4e54c..2f245d8d3 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index f336d611f..4fbc5d08d 100644 --- a/usage.go +++ b/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 3ff6d507fe5ad73a58b2aa90cf1489533454dc94 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 5 Jun 2016 19:16:26 -0700 Subject: [PATCH 042/563] 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 --- cobraadaptor/adaptor.go | 1 + usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index ca7d4e54c..83b01a3f7 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index f336d611f..680795127 100644 --- a/usage.go +++ b/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 254cce44cd2dd7329ce60706d4bbffbba97971ac Mon Sep 17 00:00:00 2001 From: Zhang Wei Date: Mon, 6 Jun 2016 22:15:46 +0800 Subject: [PATCH 043/563] Migrate restart command to cobra Signed-off-by: Zhang Wei --- cobraadaptor/adaptor.go | 1 + usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index f371ce4ed..28b57e2bb 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index 787a87aae..34397ccd2 100644 --- a/usage.go +++ b/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 05c7e2e124cacc7add50733242ed34bad64c4f69 Mon Sep 17 00:00:00 2001 From: Zhang Wei Date: Mon, 6 Jun 2016 15:38:20 +0800 Subject: [PATCH 044/563] Migrate kill command to cobra Signed-off-by: Zhang Wei --- cobraadaptor/adaptor.go | 1 + usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index f371ce4ed..2cb64e054 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index 787a87aae..727a7fb77 100644 --- a/usage.go +++ b/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 50b375d189a31dd1582dedd378245ced8435ca32 Mon Sep 17 00:00:00 2001 From: Zhang Wei Date: Mon, 6 Jun 2016 23:32:38 +0800 Subject: [PATCH 045/563] Migrate rm command to cobra Signed-off-by: Zhang Wei --- cobraadaptor/adaptor.go | 1 + usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index 0d536f060..fbddc77f1 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index baf494893..85de1fa36 100644 --- a/usage.go +++ b/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 fcd9f9f7bd623d0d8a6ee823e463786ed408faa1 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 5 Jun 2016 20:55:47 -0700 Subject: [PATCH 046/563] 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 --- cobraadaptor/adaptor.go | 1 + required.go | 17 +++++++++++++++++ usage.go | 1 - 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index 0d536f060..4f1f77ca0 100644 --- a/cobraadaptor/adaptor.go +++ b/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/required.go b/required.go index 0cf35b3d6..0c08e64f1 100644 --- a/required.go +++ b/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/usage.go b/usage.go index baf494893..a144df1af 100644 --- a/usage.go +++ b/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 7e043735b6e71e759b9a1b34f67c919f87c7d2cd Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Mon, 6 Jun 2016 19:15:37 -0700 Subject: [PATCH 047/563] 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 --- cobraadaptor/adaptor.go | 2 ++ usage.go | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index 0d536f060..555f265ac 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index baf494893..1ad0a891e 100644 --- a/usage.go +++ b/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 4951a30626466f78e1ab20cf763f878038a69a1b Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 5 Jun 2016 11:05:35 -0700 Subject: [PATCH 048/563] 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 --- cobraadaptor/adaptor.go | 1 + usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index 0d536f060..8c73831f4 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index baf494893..b8314eb0a 100644 --- a/usage.go +++ b/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 15083c2e98b5067d93beb32a5fe6201388ba84ea Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Tue, 7 Jun 2016 18:15:44 +0200 Subject: [PATCH 049/563] Migrate docker build to cobra Signed-off-by: Vincent Demeester --- cobraadaptor/adaptor.go | 1 + usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index 0d536f060..51dc09bd8 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index baf494893..065debaae 100644 --- a/usage.go +++ b/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 3f5ac2f50f20a4b6212f0de95436c3b53dddd15e Mon Sep 17 00:00:00 2001 From: Zhang Wei Date: Tue, 7 Jun 2016 00:47:18 +0800 Subject: [PATCH 050/563] Migrate stats and events command to cobra. Signed-off-by: Zhang Wei --- cobraadaptor/adaptor.go | 2 ++ usage.go | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index 5b31a0bd6..2b7cd169e 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index 7b1573fc6..4749f20aa 100644 --- a/usage.go +++ b/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 43fdd6f2fa3dc514bf53338c375af2710c5b971f Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Mon, 6 Jun 2016 07:57:44 -0700 Subject: [PATCH 051/563] 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 --- cobraadaptor/adaptor.go | 1 + required.go | 4 ++-- usage.go | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index 11b821cc4..4d4ca433b 100644 --- a/cobraadaptor/adaptor.go +++ b/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/required.go b/required.go index 0c08e64f1..9276a5740 100644 --- a/required.go +++ b/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/usage.go b/usage.go index e9debf3be..32b344265 100644 --- a/usage.go +++ b/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 ca96b906bc6ca90c1bdef1832a316935950d870d Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Thu, 9 Jun 2016 17:28:33 +0200 Subject: [PATCH 052/563] Migrate load command to cobra Signed-off-by: Vincent Demeester --- cobraadaptor/adaptor.go | 1 + usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index 11b821cc4..c5fcac069 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index e9debf3be..b6d043905 100644 --- a/usage.go +++ b/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 171adb0b0fb19b7a5833703e86a92a464b2be6cf Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Thu, 9 Jun 2016 17:38:20 +0200 Subject: [PATCH 053/563] Migrate save command to cobra Signed-off-by: Vincent Demeester --- cobraadaptor/adaptor.go | 1 + usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index c5fcac069..a9bdd1a17 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index b6d043905..20eae80f7 100644 --- a/usage.go +++ b/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 d4fef62ce0171c219642d60a2b705bcff3cbbc10 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 10 Jun 2016 12:04:29 +0200 Subject: [PATCH 054/563] 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 --- cobraadaptor/adaptor.go | 3 +++ usage.go | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index a9bdd1a17..11edef6f2 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index 20eae80f7..1e6c3dc20 100644 --- a/usage.go +++ b/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 27fd1bffb02da5c5d951581bcbe3e7587352035d Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 10 Jun 2016 12:07:23 +0200 Subject: [PATCH 055/563] Migrate pull command to cobra Signed-off-by: Vincent Demeester --- cobraadaptor/adaptor.go | 1 + usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index a9bdd1a17..2f3d14821 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index 20eae80f7..c063dd0da 100644 --- a/usage.go +++ b/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 16dbf630a2fb98985d7b39b7c54090d211021e2e Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 10 Jun 2016 12:07:28 +0200 Subject: [PATCH 056/563] Migrate push command to cobra Signed-off-by: Vincent Demeester --- cobraadaptor/adaptor.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index 2f3d14821..ec0c6d39c 100644 --- a/cobraadaptor/adaptor.go +++ b/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 0679ae5bf4c34bb8d099770e85bb9023bfe23839 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 10 Jun 2016 12:07:32 +0200 Subject: [PATCH 057/563] Moving Image{Push,Pull}Privileged to trust.go Signed-off-by: Vincent Demeester --- usage.go | 1 - 1 file changed, 1 deletion(-) diff --git a/usage.go b/usage.go index c063dd0da..7ebbf2d12 100644 --- a/usage.go +++ b/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 92e6b85fa0c518a4ac44c71e1737d891d1b50892 Mon Sep 17 00:00:00 2001 From: Tianyi Wang Date: Wed, 8 Jun 2016 21:56:44 +0900 Subject: [PATCH 058/563] Migrate ps command to cobra Signed-off-by: Tianyi Wang --- cobraadaptor/adaptor.go | 1 + usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index b1980491a..4d2958b53 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index 9cd7acd24..73fa4f224 100644 --- a/usage.go +++ b/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 408531dafa6ae9d50e7ebd13253e07244e04ffe5 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 13 Jun 2016 19:56:23 -0700 Subject: [PATCH 059/563] 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 --- cobraadaptor/adaptor.go | 6 ++++++ usage.go | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index 4d2958b53..6f1a8876b 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index 73fa4f224..3c3b321be 100644 --- a/usage.go +++ b/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 50626d2b2bf15bf925dcd235a9079fab8fda7eb7 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Tue, 14 Jun 2016 17:16:59 +0200 Subject: [PATCH 060/563] Migrate cp command to cobra Signed-off-by: Vincent Demeester --- cobraadaptor/adaptor.go | 1 + usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index 6f1a8876b..55538dd98 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index 3c3b321be..0e2923740 100644 --- a/usage.go +++ b/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 3fe470656a2bd5c1002f328cd98aca91f52af2c4 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 8 Jun 2016 13:47:46 -0400 Subject: [PATCH 061/563] Add experimental docker stack commands Signed-off-by: Daniel Nephin --- cobraadaptor/adaptor.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index 55538dd98..44d0a5e00 100644 --- a/cobraadaptor/adaptor.go +++ b/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 11c8c6c8fcfa5ebf8d918962020d1b171328186f Mon Sep 17 00:00:00 2001 From: Tibor Vass Date: Mon, 16 May 2016 11:50:55 -0400 Subject: [PATCH 062/563] 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 --- cobraadaptor/adaptor.go | 2 ++ error.go | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 error.go diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index 55538dd98..a87c75206 100644 --- a/cobraadaptor/adaptor.go +++ b/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/error.go b/error.go new file mode 100644 index 000000000..902d1b6e4 --- /dev/null +++ b/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 63bccf7f313697ef76fb924feac741bf808340e1 Mon Sep 17 00:00:00 2001 From: Anusha Ragunathan Date: Wed, 15 Jun 2016 09:17:05 -0700 Subject: [PATCH 063/563] Avoid back and forth conversion between strings and bytes. Signed-off-by: Anusha Ragunathan --- error.go | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/error.go b/error.go index 902d1b6e4..e421c7f7c 100644 --- a/error.go +++ b/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 91b49f8538b423648d4c93855e0dc0ce326bdfc3 Mon Sep 17 00:00:00 2001 From: Tomasz Kopczynski Date: Wed, 8 Jun 2016 23:42:25 +0200 Subject: [PATCH 064/563] Migrate info command to cobra Signed-off-by: Tomasz Kopczynski --- cobraadaptor/adaptor.go | 1 + usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index e8a29c681..546684031 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index 0e2923740..451c5e775 100644 --- a/usage.go +++ b/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 396d11999f37dd49c60cdcc6e4394a407ee5d340 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 5 Jun 2016 13:55:00 -0700 Subject: [PATCH 065/563] 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 --- cobraadaptor/adaptor.go | 1 + usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index 546684031..589e46c16 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index 451c5e775..56e406e3a 100644 --- a/usage.go +++ b/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 5545165e0298e9198821a07aaeb49f1ac8973816 Mon Sep 17 00:00:00 2001 From: allencloud Date: Sun, 3 Jul 2016 20:47:39 +0800 Subject: [PATCH 066/563] fix typos Signed-off-by: allencloud --- required.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/required.go b/required.go index 9276a5740..8ee02c842 100644 --- a/required.go +++ b/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 6393b5fcc7506d6e3606c508a2d99ec28a4c3bf0 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sat, 16 Jul 2016 16:44:10 +0200 Subject: [PATCH 067/563] 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 --- cli.go | 7 +------ cobraadaptor/adaptor.go | 4 ++-- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/cli.go b/cli.go index f6d48d6fa..8d21cda69 100644 --- a/cli.go +++ b/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/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index 589e46c16..f63307cb8 100644 --- a/cobraadaptor/adaptor.go +++ b/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 6f66e15f990c58c46efaf73754ae94675e49b6dc Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 9 Jun 2016 11:33:28 -0400 Subject: [PATCH 068/563] 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 --- cobraadaptor/adaptor.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index f63307cb8..f2c64c9f4 100644 --- a/cobraadaptor/adaptor.go +++ b/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 b32ff5a1cd1b0db2207e17a5a2325cce819b4ce1 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Mon, 20 Jun 2016 13:27:56 +0000 Subject: [PATCH 069/563] Migrate exec command to cobra Signed-off-by: Akihiro Suda --- cobraadaptor/adaptor.go | 1 + usage.go | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index f2c64c9f4..6614f1fa6 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index 56e406e3a..0ad053d73 100644 --- a/usage.go +++ b/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 39c47a0e24d9de5e4048c9c9dc915f33382ee4c6 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 23 Jun 2016 11:09:49 -0400 Subject: [PATCH 070/563] Convert inspect to cobra. Signed-off-by: Daniel Nephin --- cobraadaptor/adaptor.go | 1 + usage.go | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index 6614f1fa6..2df553bea 100644 --- a/cobraadaptor/adaptor.go +++ b/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/usage.go b/usage.go index 0ad053d73..a8a032864 100644 --- a/usage.go +++ b/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 d08dd40a8882c2c9afb575b0f145fd29922153de Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 21 Jun 2016 16:42:47 -0400 Subject: [PATCH 071/563] Convert dockerd to use cobra and pflag Signed-off-by: Daniel Nephin --- flags/client.go | 8 ++++-- flags/common.go | 75 +++++++++++++++++++++++-------------------------- 2 files changed, 40 insertions(+), 43 deletions(-) diff --git a/flags/client.go b/flags/client.go index cc7309db4..eadbc143b 100644 --- a/flags/client.go +++ b/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/flags/common.go b/flags/common.go index 4726b04f2..a3579bff6 100644 --- a/flags/common.go +++ b/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 82a8cc1556c1aa5be2ceb0406d7828b8360d0be6 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 22 Jun 2016 13:08:04 -0400 Subject: [PATCH 072/563] Convert docker root command to use pflag and cobra Fix the daemon proxy for cobra commands. Signed-off-by: Daniel Nephin --- cobraadaptor/adaptor.go | 65 ++++------------------------------------- flags/client.go | 17 +++++------ 2 files changed, 14 insertions(+), 68 deletions(-) diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index 2df553bea..d7747351c 100644 --- a/cobraadaptor/adaptor.go +++ b/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/flags/client.go b/flags/client.go index eadbc143b..9b6940f6b 100644 --- a/flags/client.go +++ b/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 fc1a3d79f8a2e8b31532c3736a59f00b8035100e Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 22 Jun 2016 18:36:51 -0400 Subject: [PATCH 073/563] 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 --- cobraadaptor/adaptor.go | 86 +++++++++-------------------------------- flagerrors.go | 21 ---------- flags/common.go | 7 ++-- 3 files changed, 21 insertions(+), 93 deletions(-) delete mode 100644 flagerrors.go diff --git a/cobraadaptor/adaptor.go b/cobraadaptor/adaptor.go index d7747351c..67263e157 100644 --- a/cobraadaptor/adaptor.go +++ b/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/flagerrors.go b/flagerrors.go deleted file mode 100644 index aab8a9884..000000000 --- a/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/flags/common.go b/flags/common.go index a3579bff6..2318b9d97 100644 --- a/flags/common.go +++ b/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 38aca22dcdfe7a1c3d3628340f520b9ea3591115 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 23 Jun 2016 11:25:51 -0400 Subject: [PATCH 074/563] 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 --- cli.go | 191 ---------------------------- cobraadaptor/adaptor.go => cobra.go | 8 +- error.go | 15 ++- usage.go | 19 --- 4 files changed, 15 insertions(+), 218 deletions(-) delete mode 100644 cli.go rename cobraadaptor/adaptor.go => cobra.go (86%) delete mode 100644 usage.go diff --git a/cli.go b/cli.go deleted file mode 100644 index 8d21cda69..000000000 --- a/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/cobraadaptor/adaptor.go b/cobra.go similarity index 86% rename from cobraadaptor/adaptor.go rename to cobra.go index 67263e157..5e20c9600 100644 --- a/cobraadaptor/adaptor.go +++ b/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/error.go b/error.go index e421c7f7c..62f62433b 100644 --- a/error.go +++ b/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/usage.go b/usage.go deleted file mode 100644 index a8a032864..000000000 --- a/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 2791c2ec2812855760f01f5c50d9bb92d445ac62 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 3 Aug 2016 12:20:46 -0400 Subject: [PATCH 075/563] 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 --- cobra.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cobra.go b/cobra.go index 5e20c9600..f924e67b2 100644 --- a/cobra.go +++ b/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 efdd29abcf8ab0a0cf2d706f4e9baa77a2f3393b Mon Sep 17 00:00:00 2001 From: Cao Weiwei Date: Sun, 28 Aug 2016 21:30:14 +0800 Subject: [PATCH 076/563] Fix typo Signed-off-by: Cao Weiwei --- cobra.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cobra.go b/cobra.go index f924e67b2..836196d76 100644 --- a/cobra.go +++ b/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 3bd1eb4b762a5860f6fd552020d5a5305b1453e0 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 8 Sep 2016 13:11:39 -0400 Subject: [PATCH 077/563] 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 --- command/bundlefile/bundlefile.go | 71 +++ command/bundlefile/bundlefile_test.go | 79 ++++ command/cli.go | 164 +++++++ command/commands/commands.go | 71 +++ command/container/attach.go | 130 ++++++ command/container/commit.go | 76 ++++ command/container/cp.go | 303 +++++++++++++ command/container/create.go | 217 ++++++++++ command/container/diff.go | 58 +++ command/container/exec.go | 192 +++++++++ command/container/exec_test.go | 117 +++++ command/container/export.go | 59 +++ command/container/hijack.go | 121 ++++++ command/container/kill.go | 53 +++ command/container/logs.go | 87 ++++ command/container/pause.go | 48 +++ command/container/port.go | 78 ++++ command/container/ps.go | 142 ++++++ command/container/ps_test.go | 74 ++++ command/container/rename.go | 51 +++ command/container/restart.go | 55 +++ command/container/rm.go | 76 ++++ command/container/run.go | 288 +++++++++++++ command/container/start.go | 161 +++++++ command/container/stats.go | 233 ++++++++++ command/container/stats_helpers.go | 238 ++++++++++ command/container/stats_unit_test.go | 45 ++ command/container/stop.go | 56 +++ command/container/top.go | 58 +++ command/container/tty.go | 103 +++++ command/container/unpause.go | 49 +++ command/container/update.go | 157 +++++++ command/container/utils.go | 92 ++++ command/container/wait.go | 50 +++ command/credentials.go | 44 ++ command/formatter/container.go | 208 +++++++++ command/formatter/container_test.go | 404 +++++++++++++++++ command/formatter/custom.go | 50 +++ command/formatter/custom_test.go | 28 ++ command/formatter/formatter.go | 90 ++++ command/formatter/image.go | 229 ++++++++++ command/formatter/image_test.go | 345 +++++++++++++++ command/formatter/network.go | 129 ++++++ command/formatter/network_test.go | 201 +++++++++ command/formatter/volume.go | 114 +++++ command/formatter/volume_test.go | 183 ++++++++ command/idresolver/idresolver.go | 70 +++ command/image/build.go | 452 +++++++++++++++++++ command/image/history.go | 99 +++++ command/image/images.go | 103 +++++ command/image/import.go | 88 ++++ command/image/load.go | 67 +++ command/image/pull.go | 93 ++++ command/image/push.go | 61 +++ command/image/remove.go | 70 +++ command/image/save.go | 57 +++ command/image/search.go | 135 ++++++ command/image/tag.go | 41 ++ command/in.go | 75 ++++ command/inspect/inspector.go | 195 +++++++++ command/inspect/inspector_test.go | 221 ++++++++++ command/network/cmd.go | 31 ++ command/network/connect.go | 64 +++ command/network/create.go | 225 ++++++++++ command/network/disconnect.go | 41 ++ command/network/inspect.go | 45 ++ command/network/list.go | 96 +++++ command/network/remove.go | 43 ++ command/node/cmd.go | 47 ++ command/node/demote.go | 36 ++ command/node/inspect.go | 144 +++++++ command/node/list.go | 111 +++++ command/node/opts.go | 60 +++ command/node/promote.go | 36 ++ command/node/ps.go | 69 +++ command/node/remove.go | 46 ++ command/node/update.go | 121 ++++++ command/out.go | 69 +++ command/plugin/cmd.go | 12 + command/plugin/cmd_experimental.go | 36 ++ command/plugin/disable.go | 45 ++ command/plugin/enable.go | 45 ++ command/plugin/inspect.go | 59 +++ command/plugin/install.go | 103 +++++ command/plugin/list.go | 62 +++ command/plugin/push.go | 55 +++ command/plugin/remove.go | 69 +++ command/plugin/set.go | 42 ++ command/registry.go | 193 +++++++++ command/registry/login.go | 85 ++++ command/registry/logout.go | 77 ++++ command/service/cmd.go | 32 ++ command/service/create.go | 72 ++++ command/service/inspect.go | 188 ++++++++ command/service/inspect_test.go | 84 ++++ command/service/list.go | 133 ++++++ command/service/opts.go | 567 ++++++++++++++++++++++++ command/service/opts_test.go | 176 ++++++++ command/service/ps.go | 71 +++ command/service/remove.go | 47 ++ command/service/scale.go | 88 ++++ command/service/update.go | 504 ++++++++++++++++++++++ command/service/update_test.go | 198 +++++++++ command/stack/cmd.go | 39 ++ command/stack/cmd_stub.go | 18 + command/stack/common.go | 50 +++ command/stack/config.go | 41 ++ command/stack/deploy.go | 236 ++++++++++ command/stack/opts.go | 49 +++ command/stack/ps.go | 72 ++++ command/stack/remove.go | 75 ++++ command/stack/services.go | 87 ++++ command/swarm/cmd.go | 30 ++ command/swarm/init.go | 81 ++++ command/swarm/join.go | 75 ++++ command/swarm/join_token.go | 105 +++++ command/swarm/leave.go | 44 ++ command/swarm/opts.go | 179 ++++++++ command/swarm/opts_test.go | 37 ++ command/swarm/update.go | 82 ++++ command/system/events.go | 115 +++++ command/system/events_utils.go | 66 +++ command/system/info.go | 261 +++++++++++ command/system/inspect.go | 136 ++++++ command/system/version.go | 110 +++++ command/task/print.go | 100 +++++ command/trust.go | 598 ++++++++++++++++++++++++++ command/trust_test.go | 56 +++ command/utils.go | 59 +++ command/volume/cmd.go | 48 +++ command/volume/create.go | 110 +++++ command/volume/inspect.go | 55 +++ command/volume/list.go | 108 +++++ command/volume/remove.go | 68 +++ 134 files changed, 15221 insertions(+) create mode 100644 command/bundlefile/bundlefile.go create mode 100644 command/bundlefile/bundlefile_test.go create mode 100644 command/cli.go create mode 100644 command/commands/commands.go create mode 100644 command/container/attach.go create mode 100644 command/container/commit.go create mode 100644 command/container/cp.go create mode 100644 command/container/create.go create mode 100644 command/container/diff.go create mode 100644 command/container/exec.go create mode 100644 command/container/exec_test.go create mode 100644 command/container/export.go create mode 100644 command/container/hijack.go create mode 100644 command/container/kill.go create mode 100644 command/container/logs.go create mode 100644 command/container/pause.go create mode 100644 command/container/port.go create mode 100644 command/container/ps.go create mode 100644 command/container/ps_test.go create mode 100644 command/container/rename.go create mode 100644 command/container/restart.go create mode 100644 command/container/rm.go create mode 100644 command/container/run.go create mode 100644 command/container/start.go create mode 100644 command/container/stats.go create mode 100644 command/container/stats_helpers.go create mode 100644 command/container/stats_unit_test.go create mode 100644 command/container/stop.go create mode 100644 command/container/top.go create mode 100644 command/container/tty.go create mode 100644 command/container/unpause.go create mode 100644 command/container/update.go create mode 100644 command/container/utils.go create mode 100644 command/container/wait.go create mode 100644 command/credentials.go create mode 100644 command/formatter/container.go create mode 100644 command/formatter/container_test.go create mode 100644 command/formatter/custom.go create mode 100644 command/formatter/custom_test.go create mode 100644 command/formatter/formatter.go create mode 100644 command/formatter/image.go create mode 100644 command/formatter/image_test.go create mode 100644 command/formatter/network.go create mode 100644 command/formatter/network_test.go create mode 100644 command/formatter/volume.go create mode 100644 command/formatter/volume_test.go create mode 100644 command/idresolver/idresolver.go create mode 100644 command/image/build.go create mode 100644 command/image/history.go create mode 100644 command/image/images.go create mode 100644 command/image/import.go create mode 100644 command/image/load.go create mode 100644 command/image/pull.go create mode 100644 command/image/push.go create mode 100644 command/image/remove.go create mode 100644 command/image/save.go create mode 100644 command/image/search.go create mode 100644 command/image/tag.go create mode 100644 command/in.go create mode 100644 command/inspect/inspector.go create mode 100644 command/inspect/inspector_test.go create mode 100644 command/network/cmd.go create mode 100644 command/network/connect.go create mode 100644 command/network/create.go create mode 100644 command/network/disconnect.go create mode 100644 command/network/inspect.go create mode 100644 command/network/list.go create mode 100644 command/network/remove.go create mode 100644 command/node/cmd.go create mode 100644 command/node/demote.go create mode 100644 command/node/inspect.go create mode 100644 command/node/list.go create mode 100644 command/node/opts.go create mode 100644 command/node/promote.go create mode 100644 command/node/ps.go create mode 100644 command/node/remove.go create mode 100644 command/node/update.go create mode 100644 command/out.go create mode 100644 command/plugin/cmd.go create mode 100644 command/plugin/cmd_experimental.go create mode 100644 command/plugin/disable.go create mode 100644 command/plugin/enable.go create mode 100644 command/plugin/inspect.go create mode 100644 command/plugin/install.go create mode 100644 command/plugin/list.go create mode 100644 command/plugin/push.go create mode 100644 command/plugin/remove.go create mode 100644 command/plugin/set.go create mode 100644 command/registry.go create mode 100644 command/registry/login.go create mode 100644 command/registry/logout.go create mode 100644 command/service/cmd.go create mode 100644 command/service/create.go create mode 100644 command/service/inspect.go create mode 100644 command/service/inspect_test.go create mode 100644 command/service/list.go create mode 100644 command/service/opts.go create mode 100644 command/service/opts_test.go create mode 100644 command/service/ps.go create mode 100644 command/service/remove.go create mode 100644 command/service/scale.go create mode 100644 command/service/update.go create mode 100644 command/service/update_test.go create mode 100644 command/stack/cmd.go create mode 100644 command/stack/cmd_stub.go create mode 100644 command/stack/common.go create mode 100644 command/stack/config.go create mode 100644 command/stack/deploy.go create mode 100644 command/stack/opts.go create mode 100644 command/stack/ps.go create mode 100644 command/stack/remove.go create mode 100644 command/stack/services.go create mode 100644 command/swarm/cmd.go create mode 100644 command/swarm/init.go create mode 100644 command/swarm/join.go create mode 100644 command/swarm/join_token.go create mode 100644 command/swarm/leave.go create mode 100644 command/swarm/opts.go create mode 100644 command/swarm/opts_test.go create mode 100644 command/swarm/update.go create mode 100644 command/system/events.go create mode 100644 command/system/events_utils.go create mode 100644 command/system/info.go create mode 100644 command/system/inspect.go create mode 100644 command/system/version.go create mode 100644 command/task/print.go create mode 100644 command/trust.go create mode 100644 command/trust_test.go create mode 100644 command/utils.go create mode 100644 command/volume/cmd.go create mode 100644 command/volume/create.go create mode 100644 command/volume/inspect.go create mode 100644 command/volume/list.go create mode 100644 command/volume/remove.go diff --git a/command/bundlefile/bundlefile.go b/command/bundlefile/bundlefile.go new file mode 100644 index 000000000..75c2d0743 --- /dev/null +++ b/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/command/bundlefile/bundlefile_test.go b/command/bundlefile/bundlefile_test.go new file mode 100644 index 000000000..1ff8235ff --- /dev/null +++ b/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/command/cli.go b/command/cli.go new file mode 100644 index 000000000..6194c7fe9 --- /dev/null +++ b/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/command/commands/commands.go b/command/commands/commands.go new file mode 100644 index 000000000..3eb1828d5 --- /dev/null +++ b/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/command/container/attach.go b/command/container/attach.go new file mode 100644 index 000000000..a1fe58dea --- /dev/null +++ b/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/command/container/commit.go b/command/container/commit.go new file mode 100644 index 000000000..cf8d0102a --- /dev/null +++ b/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/command/container/cp.go b/command/container/cp.go new file mode 100644 index 000000000..17ab2accf --- /dev/null +++ b/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/command/container/create.go b/command/container/create.go new file mode 100644 index 000000000..95e8d95ed --- /dev/null +++ b/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/command/container/diff.go b/command/container/diff.go new file mode 100644 index 000000000..12d659101 --- /dev/null +++ b/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/command/container/exec.go b/command/container/exec.go new file mode 100644 index 000000000..1682a7ca6 --- /dev/null +++ b/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/command/container/exec_test.go b/command/container/exec_test.go new file mode 100644 index 000000000..2e122e738 --- /dev/null +++ b/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/command/container/export.go b/command/container/export.go new file mode 100644 index 000000000..8fa2e5d77 --- /dev/null +++ b/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/command/container/hijack.go b/command/container/hijack.go new file mode 100644 index 000000000..855a15290 --- /dev/null +++ b/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/command/container/kill.go b/command/container/kill.go new file mode 100644 index 000000000..8d9af6f7a --- /dev/null +++ b/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/command/container/logs.go b/command/container/logs.go new file mode 100644 index 000000000..3a37cedf4 --- /dev/null +++ b/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/command/container/pause.go b/command/container/pause.go new file mode 100644 index 000000000..0cc5b351b --- /dev/null +++ b/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/command/container/port.go b/command/container/port.go new file mode 100644 index 000000000..ea1529014 --- /dev/null +++ b/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/command/container/ps.go b/command/container/ps.go new file mode 100644 index 000000000..d7ae675f5 --- /dev/null +++ b/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/command/container/ps_test.go b/command/container/ps_test.go new file mode 100644 index 000000000..2af183cce --- /dev/null +++ b/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/command/container/rename.go b/command/container/rename.go new file mode 100644 index 000000000..346fb7b3b --- /dev/null +++ b/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/command/container/restart.go b/command/container/restart.go new file mode 100644 index 000000000..e370ef401 --- /dev/null +++ b/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/command/container/rm.go b/command/container/rm.go new file mode 100644 index 000000000..622a69b51 --- /dev/null +++ b/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/command/container/run.go b/command/container/run.go new file mode 100644 index 000000000..d36ab610c --- /dev/null +++ b/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/command/container/start.go b/command/container/start.go new file mode 100644 index 000000000..e72369177 --- /dev/null +++ b/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/command/container/stats.go b/command/container/stats.go new file mode 100644 index 000000000..ffd3fcae9 --- /dev/null +++ b/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/command/container/stats_helpers.go b/command/container/stats_helpers.go new file mode 100644 index 000000000..b5e8e0472 --- /dev/null +++ b/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/command/container/stats_unit_test.go b/command/container/stats_unit_test.go new file mode 100644 index 000000000..6f6a46806 --- /dev/null +++ b/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/command/container/stop.go b/command/container/stop.go new file mode 100644 index 000000000..dddb7efa2 --- /dev/null +++ b/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/command/container/top.go b/command/container/top.go new file mode 100644 index 000000000..160153ba7 --- /dev/null +++ b/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/command/container/tty.go b/command/container/tty.go new file mode 100644 index 000000000..5360c6b04 --- /dev/null +++ b/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/command/container/unpause.go b/command/container/unpause.go new file mode 100644 index 000000000..c3635db55 --- /dev/null +++ b/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/command/container/update.go b/command/container/update.go new file mode 100644 index 000000000..b5770c899 --- /dev/null +++ b/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/command/container/utils.go b/command/container/utils.go new file mode 100644 index 000000000..8c993dcce --- /dev/null +++ b/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/command/container/wait.go b/command/container/wait.go new file mode 100644 index 000000000..19ccf7ac2 --- /dev/null +++ b/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/command/credentials.go b/command/credentials.go new file mode 100644 index 000000000..06e9d1de2 --- /dev/null +++ b/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/command/formatter/container.go b/command/formatter/container.go new file mode 100644 index 000000000..f1c985791 --- /dev/null +++ b/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/command/formatter/container_test.go b/command/formatter/container_test.go new file mode 100644 index 000000000..deaa915a8 --- /dev/null +++ b/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/command/formatter/custom.go b/command/formatter/custom.go new file mode 100644 index 000000000..2aa2e7b55 --- /dev/null +++ b/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/command/formatter/custom_test.go b/command/formatter/custom_test.go new file mode 100644 index 000000000..da42039dc --- /dev/null +++ b/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/command/formatter/formatter.go b/command/formatter/formatter.go new file mode 100644 index 000000000..de71c3cdd --- /dev/null +++ b/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/command/formatter/image.go b/command/formatter/image.go new file mode 100644 index 000000000..0ffcfaf72 --- /dev/null +++ b/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/command/formatter/image_test.go b/command/formatter/image_test.go new file mode 100644 index 000000000..7c87f393f --- /dev/null +++ b/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/command/formatter/network.go b/command/formatter/network.go new file mode 100644 index 000000000..6eb820879 --- /dev/null +++ b/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/command/formatter/network_test.go b/command/formatter/network_test.go new file mode 100644 index 000000000..b5f826af6 --- /dev/null +++ b/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/command/formatter/volume.go b/command/formatter/volume.go new file mode 100644 index 000000000..ba24b06a4 --- /dev/null +++ b/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/command/formatter/volume_test.go b/command/formatter/volume_test.go new file mode 100644 index 000000000..2295eff3e --- /dev/null +++ b/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/command/idresolver/idresolver.go b/command/idresolver/idresolver.go new file mode 100644 index 000000000..ad0d96735 --- /dev/null +++ b/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/command/image/build.go b/command/image/build.go new file mode 100644 index 000000000..10ad413f2 --- /dev/null +++ b/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/command/image/history.go b/command/image/history.go new file mode 100644 index 000000000..a75403a45 --- /dev/null +++ b/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/command/image/images.go b/command/image/images.go new file mode 100644 index 000000000..f00fecf67 --- /dev/null +++ b/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/command/image/import.go b/command/image/import.go new file mode 100644 index 000000000..60024fb53 --- /dev/null +++ b/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/command/image/load.go b/command/image/load.go new file mode 100644 index 000000000..56145a8a3 --- /dev/null +++ b/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/command/image/pull.go b/command/image/pull.go new file mode 100644 index 000000000..88ccb4734 --- /dev/null +++ b/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/command/image/push.go b/command/image/push.go new file mode 100644 index 000000000..62b637f6e --- /dev/null +++ b/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/command/image/remove.go b/command/image/remove.go new file mode 100644 index 000000000..51a7b2164 --- /dev/null +++ b/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/command/image/save.go b/command/image/save.go new file mode 100644 index 000000000..bbe82d2a0 --- /dev/null +++ b/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/command/image/search.go b/command/image/search.go new file mode 100644 index 000000000..7c4ad03b9 --- /dev/null +++ b/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/command/image/tag.go b/command/image/tag.go new file mode 100644 index 000000000..b88789b0f --- /dev/null +++ b/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/command/in.go b/command/in.go new file mode 100644 index 000000000..c3ed70dc1 --- /dev/null +++ b/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/command/inspect/inspector.go b/command/inspect/inspector.go new file mode 100644 index 000000000..b0537e846 --- /dev/null +++ b/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/command/inspect/inspector_test.go b/command/inspect/inspector_test.go new file mode 100644 index 000000000..1ce1593ab --- /dev/null +++ b/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/command/network/cmd.go b/command/network/cmd.go new file mode 100644 index 000000000..a7c9b3fce --- /dev/null +++ b/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/command/network/connect.go b/command/network/connect.go new file mode 100644 index 000000000..c4b676e5f --- /dev/null +++ b/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/command/network/create.go b/command/network/create.go new file mode 100644 index 000000000..2ffd80548 --- /dev/null +++ b/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/command/network/disconnect.go b/command/network/disconnect.go new file mode 100644 index 000000000..c9d9c14a1 --- /dev/null +++ b/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/command/network/inspect.go b/command/network/inspect.go new file mode 100644 index 000000000..f1f677db9 --- /dev/null +++ b/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/command/network/list.go b/command/network/list.go new file mode 100644 index 000000000..19013a3b8 --- /dev/null +++ b/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/command/network/remove.go b/command/network/remove.go new file mode 100644 index 000000000..2034b8709 --- /dev/null +++ b/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/command/node/cmd.go b/command/node/cmd.go new file mode 100644 index 000000000..6aa4dfcb1 --- /dev/null +++ b/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/command/node/demote.go b/command/node/demote.go new file mode 100644 index 000000000..33f86c649 --- /dev/null +++ b/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/command/node/inspect.go b/command/node/inspect.go new file mode 100644 index 000000000..c73b83a87 --- /dev/null +++ b/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/command/node/list.go b/command/node/list.go new file mode 100644 index 000000000..bed4bc496 --- /dev/null +++ b/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/command/node/opts.go b/command/node/opts.go new file mode 100644 index 000000000..7e6c55d48 --- /dev/null +++ b/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/command/node/promote.go b/command/node/promote.go new file mode 100644 index 000000000..f47d783f4 --- /dev/null +++ b/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/command/node/ps.go b/command/node/ps.go new file mode 100644 index 000000000..84d4b375a --- /dev/null +++ b/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/command/node/remove.go b/command/node/remove.go new file mode 100644 index 000000000..696cd5871 --- /dev/null +++ b/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/command/node/update.go b/command/node/update.go new file mode 100644 index 000000000..65339e138 --- /dev/null +++ b/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/command/out.go b/command/out.go new file mode 100644 index 000000000..09375d07d --- /dev/null +++ b/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/command/plugin/cmd.go b/command/plugin/cmd.go new file mode 100644 index 000000000..67d0d5031 --- /dev/null +++ b/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/command/plugin/cmd_experimental.go b/command/plugin/cmd_experimental.go new file mode 100644 index 000000000..6c991937f --- /dev/null +++ b/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/command/plugin/disable.go b/command/plugin/disable.go new file mode 100644 index 000000000..704eb7528 --- /dev/null +++ b/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/command/plugin/enable.go b/command/plugin/enable.go new file mode 100644 index 000000000..c31258bbb --- /dev/null +++ b/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/command/plugin/inspect.go b/command/plugin/inspect.go new file mode 100644 index 000000000..b43e3e945 --- /dev/null +++ b/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/command/plugin/install.go b/command/plugin/install.go new file mode 100644 index 000000000..05dc8e826 --- /dev/null +++ b/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/command/plugin/list.go b/command/plugin/list.go new file mode 100644 index 000000000..b50b2066a --- /dev/null +++ b/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/command/plugin/push.go b/command/plugin/push.go new file mode 100644 index 000000000..9ef490796 --- /dev/null +++ b/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/command/plugin/remove.go b/command/plugin/remove.go new file mode 100644 index 000000000..3b6137400 --- /dev/null +++ b/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/command/plugin/set.go b/command/plugin/set.go new file mode 100644 index 000000000..188bd63cc --- /dev/null +++ b/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/command/registry.go b/command/registry.go new file mode 100644 index 000000000..4f72afa4a --- /dev/null +++ b/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/command/registry/login.go b/command/registry/login.go new file mode 100644 index 000000000..dccf53847 --- /dev/null +++ b/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/command/registry/logout.go b/command/registry/logout.go new file mode 100644 index 000000000..1e0c5170a --- /dev/null +++ b/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/command/service/cmd.go b/command/service/cmd.go new file mode 100644 index 000000000..282ce2b4b --- /dev/null +++ b/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/command/service/create.go b/command/service/create.go new file mode 100644 index 000000000..4ec8835b3 --- /dev/null +++ b/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/command/service/inspect.go b/command/service/inspect.go new file mode 100644 index 000000000..8facb1f28 --- /dev/null +++ b/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/command/service/inspect_test.go b/command/service/inspect_test.go new file mode 100644 index 000000000..0e0f2ae74 --- /dev/null +++ b/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/command/service/list.go b/command/service/list.go new file mode 100644 index 000000000..681acd3f2 --- /dev/null +++ b/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/command/service/opts.go b/command/service/opts.go new file mode 100644 index 000000000..7236980e8 --- /dev/null +++ b/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/command/service/opts_test.go b/command/service/opts_test.go new file mode 100644 index 000000000..30e261b8d --- /dev/null +++ b/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/command/service/ps.go b/command/service/ps.go new file mode 100644 index 000000000..23c3679d7 --- /dev/null +++ b/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/command/service/remove.go b/command/service/remove.go new file mode 100644 index 000000000..c3fbbabbc --- /dev/null +++ b/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/command/service/scale.go b/command/service/scale.go new file mode 100644 index 000000000..2e2982db4 --- /dev/null +++ b/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/command/service/update.go b/command/service/update.go new file mode 100644 index 000000000..a86f20e58 --- /dev/null +++ b/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/command/service/update_test.go b/command/service/update_test.go new file mode 100644 index 000000000..6e68e977a --- /dev/null +++ b/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/command/stack/cmd.go b/command/stack/cmd.go new file mode 100644 index 000000000..979e1a0b7 --- /dev/null +++ b/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/command/stack/cmd_stub.go b/command/stack/cmd_stub.go new file mode 100644 index 000000000..51cb2d1bc --- /dev/null +++ b/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/command/stack/common.go b/command/stack/common.go new file mode 100644 index 000000000..2afdb5147 --- /dev/null +++ b/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/command/stack/config.go b/command/stack/config.go new file mode 100644 index 000000000..696c0c3fc --- /dev/null +++ b/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/command/stack/deploy.go b/command/stack/deploy.go new file mode 100644 index 000000000..5c03dc3d3 --- /dev/null +++ b/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/command/stack/opts.go b/command/stack/opts.go new file mode 100644 index 000000000..345bdc38f --- /dev/null +++ b/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/command/stack/ps.go b/command/stack/ps.go new file mode 100644 index 000000000..9d9458d85 --- /dev/null +++ b/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/command/stack/remove.go b/command/stack/remove.go new file mode 100644 index 000000000..9ba91e5c2 --- /dev/null +++ b/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/command/stack/services.go b/command/stack/services.go new file mode 100644 index 000000000..819b1c675 --- /dev/null +++ b/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/command/swarm/cmd.go b/command/swarm/cmd.go new file mode 100644 index 000000000..db2b6a253 --- /dev/null +++ b/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/command/swarm/init.go b/command/swarm/init.go new file mode 100644 index 000000000..9a17224bd --- /dev/null +++ b/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/command/swarm/join.go b/command/swarm/join.go new file mode 100644 index 000000000..72f97c015 --- /dev/null +++ b/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/command/swarm/join_token.go b/command/swarm/join_token.go new file mode 100644 index 000000000..b41120208 --- /dev/null +++ b/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/command/swarm/leave.go b/command/swarm/leave.go new file mode 100644 index 000000000..922411340 --- /dev/null +++ b/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/command/swarm/opts.go b/command/swarm/opts.go new file mode 100644 index 000000000..7fcf25d13 --- /dev/null +++ b/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/command/swarm/opts_test.go b/command/swarm/opts_test.go new file mode 100644 index 000000000..568dc8730 --- /dev/null +++ b/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/command/swarm/update.go b/command/swarm/update.go new file mode 100644 index 000000000..9884b7916 --- /dev/null +++ b/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/command/system/events.go b/command/system/events.go new file mode 100644 index 000000000..456e81b4c --- /dev/null +++ b/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/command/system/events_utils.go b/command/system/events_utils.go new file mode 100644 index 000000000..71c1b0476 --- /dev/null +++ b/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/command/system/info.go b/command/system/info.go new file mode 100644 index 000000000..259b254bd --- /dev/null +++ b/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/command/system/inspect.go b/command/system/inspect.go new file mode 100644 index 000000000..e4f67cf64 --- /dev/null +++ b/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/command/system/version.go b/command/system/version.go new file mode 100644 index 000000000..e77719ec3 --- /dev/null +++ b/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/command/task/print.go b/command/task/print.go new file mode 100644 index 000000000..963aea95c --- /dev/null +++ b/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/command/trust.go b/command/trust.go new file mode 100644 index 000000000..329da5251 --- /dev/null +++ b/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/command/trust_test.go b/command/trust_test.go new file mode 100644 index 000000000..534815f37 --- /dev/null +++ b/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/command/utils.go b/command/utils.go new file mode 100644 index 000000000..bceb7b335 --- /dev/null +++ b/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/command/volume/cmd.go b/command/volume/cmd.go new file mode 100644 index 000000000..090a00643 --- /dev/null +++ b/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/command/volume/create.go b/command/volume/create.go new file mode 100644 index 000000000..4427ff1ea --- /dev/null +++ b/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/command/volume/inspect.go b/command/volume/inspect.go new file mode 100644 index 000000000..ab06e0380 --- /dev/null +++ b/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/command/volume/list.go b/command/volume/list.go new file mode 100644 index 000000000..75e77f828 --- /dev/null +++ b/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/command/volume/remove.go b/command/volume/remove.go new file mode 100644 index 000000000..213ad26ab --- /dev/null +++ b/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 8f3e3fb6e5db5be1d71e340c29cf161fb5c78e26 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 8 Sep 2016 14:54:01 -0400 Subject: [PATCH 078/563] 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 --- command/commands/commands.go | 2 +- command/plugin/cmd_experimental.go | 4 ++-- command/plugin/disable.go | 6 +++--- command/plugin/enable.go | 6 +++--- command/plugin/inspect.go | 8 ++++---- command/plugin/install.go | 10 +++++----- command/plugin/list.go | 6 +++--- command/plugin/push.go | 8 ++++---- command/plugin/remove.go | 6 +++--- command/plugin/set.go | 6 +++--- command/stack/cmd.go | 6 +++--- command/stack/config.go | 8 ++++---- command/stack/deploy.go | 12 ++++++------ command/stack/opts.go | 2 +- command/stack/ps.go | 10 +++++----- command/stack/remove.go | 6 +++--- command/stack/services.go | 8 ++++---- 17 files changed, 57 insertions(+), 57 deletions(-) diff --git a/command/commands/commands.go b/command/commands/commands.go index 3eb1828d5..35fd6860b 100644 --- a/command/commands/commands.go +++ b/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/command/plugin/cmd_experimental.go b/command/plugin/cmd_experimental.go index 6c991937f..cc779143f 100644 --- a/command/plugin/cmd_experimental.go +++ b/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/command/plugin/disable.go b/command/plugin/disable.go index 704eb7528..3b5c69a01 100644 --- a/command/plugin/disable.go +++ b/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/command/plugin/enable.go b/command/plugin/enable.go index c31258bbb..cfc3580f4 100644 --- a/command/plugin/enable.go +++ b/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/command/plugin/inspect.go b/command/plugin/inspect.go index b43e3e945..a1cf1f7b0 100644 --- a/command/plugin/inspect.go +++ b/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/command/plugin/install.go b/command/plugin/install.go index 05dc8e826..2867247a8 100644 --- a/command/plugin/install.go +++ b/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/command/plugin/list.go b/command/plugin/list.go index b50b2066a..b8f5e5e08 100644 --- a/command/plugin/list.go +++ b/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/command/plugin/push.go b/command/plugin/push.go index 9ef490796..5174828ea 100644 --- a/command/plugin/push.go +++ b/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/command/plugin/remove.go b/command/plugin/remove.go index 3b6137400..800fc1b97 100644 --- a/command/plugin/remove.go +++ b/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/command/plugin/set.go b/command/plugin/set.go index 188bd63cc..f2d3b082c 100644 --- a/command/plugin/set.go +++ b/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/command/stack/cmd.go b/command/stack/cmd.go index 979e1a0b7..d459e0a9a 100644 --- a/command/stack/cmd.go +++ b/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/command/stack/config.go b/command/stack/config.go index 696c0c3fc..bdcf7d483 100644 --- a/command/stack/config.go +++ b/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/command/stack/deploy.go b/command/stack/deploy.go index 5c03dc3d3..d72c2bd08 100644 --- a/command/stack/deploy.go +++ b/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/command/stack/opts.go b/command/stack/opts.go index 345bdc38f..eef4d0e45 100644 --- a/command/stack/opts.go +++ b/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/command/stack/ps.go b/command/stack/ps.go index 9d9458d85..c4683b68a 100644 --- a/command/stack/ps.go +++ b/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/command/stack/remove.go b/command/stack/remove.go index 9ba91e5c2..6ab005d71 100644 --- a/command/stack/remove.go +++ b/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/command/stack/services.go b/command/stack/services.go index 819b1c675..22906378d 100644 --- a/command/stack/services.go +++ b/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 e2f7387906ded9b495ac7be8e4d1fcca7d645609 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 8 Sep 2016 15:11:38 -0400 Subject: [PATCH 079/563] Fix a test that expects whitespace at the end of the line. Signed-off-by: Daniel Nephin --- command/container/hijack.go | 2 +- command/container/tty.go | 2 +- command/formatter/container_test.go | 29 ++++++++++++++--------------- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/command/container/hijack.go b/command/container/hijack.go index 855a15290..ea429245c 100644 --- a/command/container/hijack.go +++ b/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/command/container/tty.go b/command/container/tty.go index 5360c6b04..edb11592d 100644 --- a/command/container/tty.go +++ b/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/command/formatter/container_test.go b/command/formatter/container_test.go index deaa915a8..29b8450db 100644 --- a/command/formatter/container_test.go +++ b/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 c68bb5795901881bf1c42faca66c474e1445d3b3 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 4 Sep 2016 14:44:34 -0700 Subject: [PATCH 080/563] 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 --- command/container/stats_helpers.go | 4 ++-- command/container/stats_unit_test.go | 2 +- command/formatter/container.go | 4 ++-- command/formatter/image.go | 2 +- command/image/history.go | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/command/container/stats_helpers.go b/command/container/stats_helpers.go index b5e8e0472..54cc5589c 100644 --- a/command/container/stats_helpers.go +++ b/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/command/container/stats_unit_test.go b/command/container/stats_unit_test.go index 6f6a46806..182ab5b30 100644 --- a/command/container/stats_unit_test.go +++ b/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/command/formatter/container.go b/command/formatter/container.go index f1c985791..6f519e449 100644 --- a/command/formatter/container.go +++ b/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/command/formatter/image.go b/command/formatter/image.go index 0ffcfaf72..012860e04 100644 --- a/command/formatter/image.go +++ b/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/command/image/history.go b/command/image/history.go index a75403a45..91c8f75a6 100644 --- a/command/image/history.go +++ b/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 f0647193dc8b6a3d468d10e9c1fca10668836cb5 Mon Sep 17 00:00:00 2001 From: boucher Date: Thu, 12 May 2016 10:52:00 -0400 Subject: [PATCH 081/563] Initial implementation of containerd Checkpoint API. Signed-off-by: boucher --- command/checkpoint/cmd.go | 12 +++++ command/checkpoint/cmd_experimental.go | 31 +++++++++++ command/checkpoint/create.go | 54 +++++++++++++++++++ command/checkpoint/list.go | 47 ++++++++++++++++ command/checkpoint/remove.go | 28 ++++++++++ command/commands/commands.go | 2 + command/container/start.go | 19 ++++++- command/container/start_utils.go | 8 +++ command/container/start_utils_experimental.go | 9 ++++ 9 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 command/checkpoint/cmd.go create mode 100644 command/checkpoint/cmd_experimental.go create mode 100644 command/checkpoint/create.go create mode 100644 command/checkpoint/list.go create mode 100644 command/checkpoint/remove.go create mode 100644 command/container/start_utils.go create mode 100644 command/container/start_utils_experimental.go diff --git a/command/checkpoint/cmd.go b/command/checkpoint/cmd.go new file mode 100644 index 000000000..cbeb95179 --- /dev/null +++ b/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/command/checkpoint/cmd_experimental.go b/command/checkpoint/cmd_experimental.go new file mode 100644 index 000000000..b7e614ca6 --- /dev/null +++ b/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/command/checkpoint/create.go b/command/checkpoint/create.go new file mode 100644 index 000000000..42b316fe2 --- /dev/null +++ b/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/command/checkpoint/list.go b/command/checkpoint/list.go new file mode 100644 index 000000000..6d22531d4 --- /dev/null +++ b/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/command/checkpoint/remove.go b/command/checkpoint/remove.go new file mode 100644 index 000000000..6605c5e47 --- /dev/null +++ b/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/command/commands/commands.go b/command/commands/commands.go index 35fd6860b..0adf8e3f3 100644 --- a/command/commands/commands.go +++ b/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/command/container/start.go b/command/container/start.go index e72369177..9f414a7c6 100644 --- a/command/container/start.go +++ b/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/command/container/start_utils.go b/command/container/start_utils.go new file mode 100644 index 000000000..689d742f0 --- /dev/null +++ b/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/command/container/start_utils_experimental.go b/command/container/start_utils_experimental.go new file mode 100644 index 000000000..43c64f431 --- /dev/null +++ b/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 9524caa317df82c812a278393e9e5e1b6440e1ec Mon Sep 17 00:00:00 2001 From: boucher Date: Tue, 30 Aug 2016 10:10:09 -0400 Subject: [PATCH 082/563] Fix typo Signed-off-by: boucher --- command/checkpoint/create.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/checkpoint/create.go b/command/checkpoint/create.go index 42b316fe2..f21457455 100644 --- a/command/checkpoint/create.go +++ b/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 0cf85349f3810de9feafe615208621aa1d89178f Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 29 Aug 2016 14:45:29 -0400 Subject: [PATCH 083/563] Move image trust related cli methods into the image package. Signed-off-by: Daniel Nephin --- command/cli.go | 12 +- command/container/create.go | 5 +- command/container/hijack.go | 11 +- command/image/build.go | 7 +- command/image/pull.go | 4 +- command/image/push.go | 4 +- command/image/trust.go | 576 ++++++++++++++++++++++++++++++ command/{ => image}/trust_test.go | 2 +- command/trust.go | 563 +---------------------------- 9 files changed, 604 insertions(+), 580 deletions(-) create mode 100644 command/image/trust.go rename command/{ => image}/trust_test.go (99%) diff --git a/command/cli.go b/command/cli.go index 6194c7fe9..63397bf92 100644 --- a/command/cli.go +++ b/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/command/container/create.go b/command/container/create.go index 95e8d95ed..b80b6e1e5 100644 --- a/command/container/create.go +++ b/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/command/container/hijack.go b/command/container/hijack.go index ea429245c..ca136f0e4 100644 --- a/command/container/hijack.go +++ b/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/command/image/build.go b/command/image/build.go index 10ad413f2..06ee32ba8 100644 --- a/command/image/build.go +++ b/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/command/image/pull.go b/command/image/pull.go index 88ccb4734..3f3093a5d 100644 --- a/command/image/pull.go +++ b/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/command/image/push.go b/command/image/push.go index 62b637f6e..a98de9e70 100644 --- a/command/image/push.go +++ b/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/command/image/trust.go b/command/image/trust.go new file mode 100644 index 000000000..f0948cc80 --- /dev/null +++ b/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/command/trust_test.go b/command/image/trust_test.go similarity index 99% rename from command/trust_test.go rename to command/image/trust_test.go index 534815f37..ba6373f2d 100644 --- a/command/trust_test.go +++ b/command/image/trust_test.go @@ -1,4 +1,4 @@ -package command +package image import ( "os" diff --git a/command/trust.go b/command/trust.go index 329da5251..b4c8a84ee 100644 --- a/command/trust.go +++ b/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 272868566bb0bfaa5adcdba2b280cc89cb692fcb Mon Sep 17 00:00:00 2001 From: boucher Date: Fri, 9 Sep 2016 12:13:46 -0400 Subject: [PATCH 084/563] Update checkpoint comments to be more accurate Signed-off-by: boucher --- command/checkpoint/cmd.go | 2 +- command/checkpoint/cmd_experimental.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/command/checkpoint/cmd.go b/command/checkpoint/cmd.go index cbeb95179..bc8224a2f 100644 --- a/command/checkpoint/cmd.go +++ b/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/command/checkpoint/cmd_experimental.go b/command/checkpoint/cmd_experimental.go index b7e614ca6..7939678cd 100644 --- a/command/checkpoint/cmd_experimental.go +++ b/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 6df46463a9bcb850be5bb5f8ba6546e8a7105f80 Mon Sep 17 00:00:00 2001 From: allencloud Date: Sun, 4 Sep 2016 15:38:50 +0800 Subject: [PATCH 085/563] support docker node ps multiNodes Signed-off-by: allencloud --- command/node/ps.go | 66 +++++++++++++++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 21 deletions(-) diff --git a/command/node/ps.go b/command/node/ps.go index 84d4b375a..607488f35 100644 --- a/command/node/ps.go +++ b/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 ed55f00674b7ccfc6b0b03d60a88ad07f07144bf Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 9 Sep 2016 10:49:52 -0400 Subject: [PATCH 086/563] Remove RetrieveAuthConfigs Signed-off-by: Daniel Nephin --- command/cli.go | 9 +++++++ command/credentials.go | 7 ----- command/image/build.go | 3 ++- command/registry.go | 58 +++++++++++++++++++++--------------------- 4 files changed, 40 insertions(+), 37 deletions(-) diff --git a/command/cli.go b/command/cli.go index 63397bf92..9ca28765c 100644 --- a/command/cli.go +++ b/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/command/credentials.go b/command/credentials.go index 06e9d1de2..e4a498145 100644 --- a/command/credentials.go +++ b/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/command/image/build.go b/command/image/build.go index 06ee32ba8..7f59b5413 100644 --- a/command/image/build.go +++ b/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/command/registry.go b/command/registry.go index 4f72afa4a..65d8f3d8b 100644 --- a/command/registry.go +++ b/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 4ae4e66e3cfed8e8dfb11e97c2ae32b914b700c0 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 9 Sep 2016 15:24:10 -0400 Subject: [PATCH 087/563] Remove cli/command/credentials Signed-off-by: Daniel Nephin --- command/credentials.go | 37 ------------------------------------- command/registry.go | 5 ++--- command/registry/login.go | 2 +- command/registry/logout.go | 2 +- 4 files changed, 4 insertions(+), 42 deletions(-) delete mode 100644 command/credentials.go diff --git a/command/credentials.go b/command/credentials.go deleted file mode 100644 index e4a498145..000000000 --- a/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/command/registry.go b/command/registry.go index 65d8f3d8b..3c78d47d8 100644 --- a/command/registry.go +++ b/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/command/registry/login.go b/command/registry/login.go index dccf53847..f97bb557f 100644 --- a/command/registry/login.go +++ b/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/command/registry/logout.go b/command/registry/logout.go index 1e0c5170a..3fc59dea7 100644 --- a/command/registry/logout.go +++ b/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 a26ba0e7023040f6e94dc34d68e0280cc4669561 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 9 Sep 2016 15:38:00 -0400 Subject: [PATCH 088/563] Remove remaining registry methods from DockerCLI. Signed-off-by: Daniel Nephin --- command/container/create.go | 2 +- command/image/pull.go | 4 +-- command/image/push.go | 4 +-- command/image/search.go | 4 +-- command/image/trust.go | 2 +- command/plugin/install.go | 4 +-- command/plugin/push.go | 2 +- command/registry.go | 52 ++++++++++++++++--------------------- command/registry/login.go | 4 +-- command/registry/logout.go | 2 +- command/service/create.go | 2 +- command/service/update.go | 2 +- command/stack/deploy.go | 2 +- 13 files changed, 40 insertions(+), 46 deletions(-) diff --git a/command/container/create.go b/command/container/create.go index b80b6e1e5..7bd385697 100644 --- a/command/container/create.go +++ b/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/command/image/pull.go b/command/image/pull.go index 3f3093a5d..9116d4584 100644 --- a/command/image/pull.go +++ b/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/command/image/push.go b/command/image/push.go index a98de9e70..a8ce4945e 100644 --- a/command/image/push.go +++ b/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/command/image/search.go b/command/image/search.go index 7c4ad03b9..6f8308af8 100644 --- a/command/image/search.go +++ b/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/command/image/trust.go b/command/image/trust.go index f0948cc80..b08bd490c 100644 --- a/command/image/trust.go +++ b/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/command/plugin/install.go b/command/plugin/install.go index 2867247a8..e90e8d122 100644 --- a/command/plugin/install.go +++ b/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/command/plugin/push.go b/command/plugin/push.go index 5174828ea..360830902 100644 --- a/command/plugin/push.go +++ b/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/command/registry.go b/command/registry.go index 3c78d47d8..b70d6f444 100644 --- a/command/registry.go +++ b/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/command/registry/login.go b/command/registry/login.go index f97bb557f..d6f7f8f1d 100644 --- a/command/registry/login.go +++ b/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/command/registry/logout.go b/command/registry/logout.go index 3fc59dea7..5d80595ff 100644 --- a/command/registry/logout.go +++ b/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/command/service/create.go b/command/service/create.go index 4ec8835b3..bc5576b1a 100644 --- a/command/service/create.go +++ b/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/command/service/update.go b/command/service/update.go index a86f20e58..be3218ed6 100644 --- a/command/service/update.go +++ b/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/command/stack/deploy.go b/command/stack/deploy.go index d72c2bd08..6daf9500f 100644 --- a/command/stack/deploy.go +++ b/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 f39b39cccbb9b883567eda1c190b711703dd50b4 Mon Sep 17 00:00:00 2001 From: sakeven Date: Mon, 29 Aug 2016 21:10:32 +0800 Subject: [PATCH 089/563] validate build-arg Signed-off-by: sakeven --- command/image/build.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/image/build.go b/command/image/build.go index 06ee32ba8..9ea7c23e4 100644 --- a/command/image/build.go +++ b/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 285fef282f3e27295a1a1abadaf1674fea46766a Mon Sep 17 00:00:00 2001 From: Zhang Wei Date: Tue, 19 Jul 2016 00:02:41 +0800 Subject: [PATCH 090/563] 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 --- command/container/stop.go | 8 ++++++-- command/container/utils.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/command/container/stop.go b/command/container/stop.go index dddb7efa2..2f22fd09a 100644 --- a/command/container/stop.go +++ b/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/command/container/utils.go b/command/container/utils.go index 8c993dcce..7e895834f 100644 --- a/command/container/utils.go +++ b/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 4570bfe8de34dda519567efcf28cabaf098ba2c8 Mon Sep 17 00:00:00 2001 From: Zhang Wei Date: Mon, 12 Sep 2016 17:21:08 +0800 Subject: [PATCH 091/563] Add parallel operation support for pause/unpause Support parallel pause/unpause Signed-off-by: Zhang Wei --- command/container/pause.go | 3 ++- command/container/unpause.go | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/command/container/pause.go b/command/container/pause.go index 0cc5b351b..6817cf60e 100644 --- a/command/container/pause.go +++ b/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/command/container/unpause.go b/command/container/unpause.go index c3635db55..c4d8d4841 100644 --- a/command/container/unpause.go +++ b/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 9aba07679ff36312d1e91c137687f42704ae1a39 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Mon, 12 Sep 2016 23:08:19 -0700 Subject: [PATCH 092/563] 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 --- command/system/inspect.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/command/system/inspect.go b/command/system/inspect.go index e4f67cf64..015c1b5c6 100644 --- a/command/system/inspect.go +++ b/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 d9cb421d69c79d6e97cfbea72e8e18228a3885e7 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 13 Sep 2016 14:53:11 -0400 Subject: [PATCH 093/563] Use opts.FilterOpt for filter flags. Signed-off-by: Daniel Nephin --- command/container/ps.go | 24 +++++++---------------- command/container/ps_test.go | 37 +++++++++++++++--------------------- command/image/images.go | 25 ++++++------------------ command/image/search.go | 19 +++++------------- command/network/list.go | 22 +++++---------------- command/system/events.go | 22 +++++---------------- command/volume/list.go | 20 +++++-------------- 7 files changed, 48 insertions(+), 121 deletions(-) diff --git a/command/container/ps.go b/command/container/ps.go index d7ae675f5..3583ee109 100644 --- a/command/container/ps.go +++ b/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/command/container/ps_test.go b/command/container/ps_test.go index 2af183cce..dafdcdf90 100644 --- a/command/container/ps_test.go +++ b/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/command/image/images.go b/command/image/images.go index f00fecf67..648236dfe 100644 --- a/command/image/images.go +++ b/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/command/image/search.go b/command/image/search.go index 6f8308af8..93db7006a 100644 --- a/command/image/search.go +++ b/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/command/network/list.go b/command/network/list.go index 19013a3b8..a0f2e7f4f 100644 --- a/command/network/list.go +++ b/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/command/system/events.go b/command/system/events.go index 456e81b4c..b9d740f35 100644 --- a/command/system/events.go +++ b/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/command/volume/list.go b/command/volume/list.go index 75e77f828..6d32d2cbf 100644 --- a/command/volume/list.go +++ b/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 db0952ad22e9fc8e3241f0a2ed15e4f32cc70e15 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 12 Sep 2016 16:59:18 -0400 Subject: [PATCH 094/563] Refactor formatter. Signed-off-by: Daniel Nephin --- command/container/ps.go | 26 ++-- command/formatter/container.go | 107 +++++++-------- command/formatter/container_test.go | 200 +++++++++------------------- command/formatter/custom.go | 15 ++- command/formatter/formatter.go | 75 ++++++++--- command/formatter/image.go | 88 ++++++------ command/formatter/image_test.go | 68 ++++------ command/formatter/network.go | 84 +++++------- command/formatter/network_test.go | 81 +++-------- command/formatter/volume.go | 78 +++++------ command/formatter/volume_test.go | 81 +++-------- command/image/images.go | 19 +-- command/network/list.go | 26 ++-- command/volume/list.go | 23 ++-- 14 files changed, 381 insertions(+), 590 deletions(-) diff --git a/command/container/ps.go b/command/container/ps.go index 3583ee109..9d015fd70 100644 --- a/command/container/ps.go +++ b/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/command/formatter/container.go b/command/formatter/container.go index 6f519e449..6f3a162fe 100644 --- a/command/formatter/container.go +++ b/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/command/formatter/container_test.go b/command/formatter/container_test.go index 29b8450db..1ef48ae2d 100644 --- a/command/formatter/container_test.go +++ b/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/command/formatter/custom.go b/command/formatter/custom.go index 2aa2e7b55..df3268442 100644 --- a/command/formatter/custom.go +++ b/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/command/formatter/formatter.go b/command/formatter/formatter.go index de71c3cdd..32f9a4d35 100644 --- a/command/formatter/formatter.go +++ b/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/command/formatter/image.go b/command/formatter/image.go index 012860e04..54cb7b62f 100644 --- a/command/formatter/image.go +++ b/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/command/formatter/image_test.go b/command/formatter/image_test.go index 7c87f393f..6dc7f73db 100644 --- a/command/formatter/image_test.go +++ b/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/command/formatter/network.go b/command/formatter/network.go index 6eb820879..d808fdc22 100644 --- a/command/formatter/network.go +++ b/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/command/formatter/network_test.go b/command/formatter/network_test.go index b5f826af6..28f078548 100644 --- a/command/formatter/network_test.go +++ b/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/command/formatter/volume.go b/command/formatter/volume.go index ba24b06a4..2fec59d8f 100644 --- a/command/formatter/volume.go +++ b/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/command/formatter/volume_test.go b/command/formatter/volume_test.go index 2295eff3e..1d5f74e42 100644 --- a/command/formatter/volume_test.go +++ b/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/command/image/images.go b/command/image/images.go index 648236dfe..b7dbd0567 100644 --- a/command/image/images.go +++ b/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/command/network/list.go b/command/network/list.go index a0f2e7f4f..dd7b72fea 100644 --- a/command/network/list.go +++ b/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/command/volume/list.go b/command/volume/list.go index 6d32d2cbf..cdbbaafc6 100644 --- a/command/volume/list.go +++ b/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 2f8c4333fea63e4d5ae9158a2b13905c2e322e81 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 13 Sep 2016 14:21:07 -0400 Subject: [PATCH 095/563] Fix testcases that expect trailing whitespace and broken integration tests based of nil pointers Signed-off-by: Daniel Nephin --- command/container/ps.go | 4 ++-- command/formatter/container.go | 10 +++++++++- command/formatter/volume.go | 4 ++-- command/formatter/volume_test.go | 12 ++++++------ command/image/images.go | 2 +- command/network/list.go | 2 +- command/volume/list.go | 2 +- 7 files changed, 22 insertions(+), 14 deletions(-) diff --git a/command/container/ps.go b/command/container/ps.go index 9d015fd70..b5a3be06e 100644 --- a/command/container/ps.go +++ b/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/command/formatter/container.go b/command/formatter/container.go index 6f3a162fe..30a649247 100644 --- a/command/formatter/container.go +++ b/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/command/formatter/volume.go b/command/formatter/volume.go index 2fec59d8f..e41ee266b 100644 --- a/command/formatter/volume.go +++ b/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/command/formatter/volume_test.go b/command/formatter/volume_test.go index 1d5f74e42..8c715b343 100644 --- a/command/formatter/volume_test.go +++ b/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/command/image/images.go b/command/image/images.go index b7dbd0567..0229734ce 100644 --- a/command/image/images.go +++ b/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/command/network/list.go b/command/network/list.go index dd7b72fea..9ba803275 100644 --- a/command/network/list.go +++ b/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/command/volume/list.go b/command/volume/list.go index cdbbaafc6..77ce35977 100644 --- a/command/volume/list.go +++ b/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 824707ea494f279765e8d685f54c9bc0d45f7702 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Mon, 12 Sep 2016 21:06:04 -0700 Subject: [PATCH 096/563] 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 --- command/image/build.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/command/image/build.go b/command/image/build.go index 85f51f14c..17be405bd 100644 --- a/command/image/build.go +++ b/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 0ae2a02ce6e4d3f2e8d3ecf54358414bec7ff62d Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 2 Sep 2016 07:40:06 +0000 Subject: [PATCH 097/563] add `docker events --format` Signed-off-by: Akihiro Suda --- command/system/events.go | 54 ++++++++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/command/system/events.go b/command/system/events.go index b9d740f35..f2946b876 100644 --- a/command/system/events.go +++ b/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 d0e960f3b18331107b8087ba536bb5828bffe97c Mon Sep 17 00:00:00 2001 From: John Howard Date: Wed, 7 Sep 2016 11:14:49 -0700 Subject: [PATCH 098/563] Only output security options if there are any Signed-off-by: John Howard --- command/system/info.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/command/system/info.go b/command/system/info.go index 259b254bd..a2d0abad2 100644 --- a/command/system/info.go +++ b/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 c3238783319476c4bb26a9f80a67b151ad00ed73 Mon Sep 17 00:00:00 2001 From: John Howard Date: Wed, 7 Sep 2016 16:08:51 -0700 Subject: [PATCH 099/563] Windows: stats support Signed-off-by: John Howard --- command/container/stats.go | 10 +- command/container/stats_helpers.go | 144 ++++++++++++++++++++--------- 2 files changed, 110 insertions(+), 44 deletions(-) diff --git a/command/container/stats.go b/command/container/stats.go index ffd3fcae9..4c9788389 100644 --- a/command/container/stats.go +++ b/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/command/container/stats_helpers.go b/command/container/stats_helpers.go index 54cc5589c..b48d9c7c6 100644 --- a/command/container/stats_helpers.go +++ b/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 a4f3442403ec2cb89052f7fafb21bd6ab306f748 Mon Sep 17 00:00:00 2001 From: Boaz Shuster Date: Mon, 18 Jul 2016 21:30:15 +0300 Subject: [PATCH 100/563] Add the format switch to the stats command Signed-off-by: Boaz Shuster --- command/container/stats.go | 82 +++++++--------- command/container/stats_helpers.go | 95 ++++--------------- command/container/stats_unit_test.go | 25 ----- command/formatter/stats.go | 135 +++++++++++++++++++++++++++ 4 files changed, 188 insertions(+), 149 deletions(-) create mode 100644 command/formatter/stats.go diff --git a/command/container/stats.go b/command/container/stats.go index 4c9788389..2bd5e3db7 100644 --- a/command/container/stats.go +++ b/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/command/container/stats_helpers.go b/command/container/stats_helpers.go index b48d9c7c6..2039d2ade 100644 --- a/command/container/stats_helpers.go +++ b/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/command/container/stats_unit_test.go b/command/container/stats_unit_test.go index 182ab5b30..fc6563c4d 100644 --- a/command/container/stats_unit_test.go +++ b/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/command/formatter/stats.go b/command/formatter/stats.go new file mode 100644 index 000000000..939431da1 --- /dev/null +++ b/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 accc5d5bd46de3c0d90d84eacb2d589290b497da Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 23 Jun 2016 13:03:40 -0400 Subject: [PATCH 101/563] 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 --- command/commands/commands.go | 75 ++++++++++++++++------------ command/container/cmd.go | 49 ++++++++++++++++++ command/container/inspect.go | 47 +++++++++++++++++ command/container/{ps.go => list.go} | 7 +++ command/image/cmd.go | 37 ++++++++++++++ command/image/inspect.go | 44 ++++++++++++++++ command/image/{images.go => list.go} | 7 +++ command/image/remove.go | 7 +++ command/stack/cmd.go | 4 +- 9 files changed, 242 insertions(+), 35 deletions(-) create mode 100644 command/container/cmd.go create mode 100644 command/container/inspect.go rename command/container/{ps.go => list.go} (94%) create mode 100644 command/image/cmd.go create mode 100644 command/image/inspect.go rename command/image/{images.go => list.go} (91%) diff --git a/command/commands/commands.go b/command/commands/commands.go index 0adf8e3f3..95615fe0b 100644 --- a/command/commands/commands.go +++ b/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/command/container/cmd.go b/command/container/cmd.go new file mode 100644 index 000000000..6d71e53eb --- /dev/null +++ b/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/command/container/inspect.go b/command/container/inspect.go new file mode 100644 index 000000000..0bef51a61 --- /dev/null +++ b/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/command/container/ps.go b/command/container/list.go similarity index 94% rename from command/container/ps.go rename to command/container/list.go index b5a3be06e..7f10ce8bd 100644 --- a/command/container/ps.go +++ b/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/command/image/cmd.go b/command/image/cmd.go new file mode 100644 index 000000000..e04c4c23f --- /dev/null +++ b/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/command/image/inspect.go b/command/image/inspect.go new file mode 100644 index 000000000..11c528ef2 --- /dev/null +++ b/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/command/image/images.go b/command/image/list.go similarity index 91% rename from command/image/images.go rename to command/image/list.go index 0229734ce..587869fdf 100644 --- a/command/image/images.go +++ b/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/command/image/remove.go b/command/image/remove.go index 51a7b2164..c79ceba7a 100644 --- a/command/image/remove.go +++ b/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/command/stack/cmd.go b/command/stack/cmd.go index d459e0a9a..22f076c4d 100644 --- a/command/stack/cmd.go +++ b/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 68b7f55a456b4970850c72a8b9d3381950bf5e20 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 29 Aug 2016 09:59:41 -0400 Subject: [PATCH 102/563] Move the search command to the registry package. And move it back to the top-level command. Signed-off-by: Daniel Nephin --- command/commands/commands.go | 2 +- command/image/cmd.go | 1 - command/{image => registry}/search.go | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) rename command/{image => registry}/search.go (99%) diff --git a/command/commands/commands.go b/command/commands/commands.go index 95615fe0b..cace1b152 100644 --- a/command/commands/commands.go +++ b/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/command/image/cmd.go b/command/image/cmd.go index e04c4c23f..4a7d2b3fd 100644 --- a/command/image/cmd.go +++ b/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/command/image/search.go b/command/registry/search.go similarity index 99% rename from command/image/search.go rename to command/registry/search.go index 93db7006a..124b4ae6c 100644 --- a/command/image/search.go +++ b/command/registry/search.go @@ -1,4 +1,4 @@ -package image +package registry import ( "fmt" From 1f0f7ecb5a943602aa582ab5a7ae05214b995f85 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 12 Sep 2016 11:37:00 -0400 Subject: [PATCH 103/563] 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 --- cobra.go | 79 +++++++++++++++++++++++--- command/checkpoint/cmd_experimental.go | 2 +- command/commands/commands.go | 5 ++ command/container/cmd.go | 2 +- command/image/cmd.go | 2 +- command/network/cmd.go | 2 +- command/node/cmd.go | 2 +- command/plugin/cmd_experimental.go | 2 +- command/service/cmd.go | 2 +- command/stack/cmd.go | 2 +- command/swarm/cmd.go | 2 +- command/volume/cmd.go | 2 +- 12 files changed, 86 insertions(+), 18 deletions(-) diff --git a/cobra.go b/cobra.go index 836196d76..324c0d7b2 100644 --- a/cobra.go +++ b/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/command/checkpoint/cmd_experimental.go b/command/checkpoint/cmd_experimental.go index 7939678cd..c05d3ded4 100644 --- a/command/checkpoint/cmd_experimental.go +++ b/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/command/commands/commands.go b/command/commands/commands.go index cace1b152..d61823399 100644 --- a/command/commands/commands.go +++ b/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/command/container/cmd.go b/command/container/cmd.go index 6d71e53eb..da9ea6d41 100644 --- a/command/container/cmd.go +++ b/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/command/image/cmd.go b/command/image/cmd.go index 4a7d2b3fd..f60ffeeb8 100644 --- a/command/image/cmd.go +++ b/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/command/network/cmd.go b/command/network/cmd.go index a7c9b3fce..b33f98cd3 100644 --- a/command/network/cmd.go +++ b/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/command/node/cmd.go b/command/node/cmd.go index 6aa4dfcb1..c7d0cf818 100644 --- a/command/node/cmd.go +++ b/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/command/plugin/cmd_experimental.go b/command/plugin/cmd_experimental.go index cc779143f..33c1c93ac 100644 --- a/command/plugin/cmd_experimental.go +++ b/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/command/service/cmd.go b/command/service/cmd.go index 282ce2b4b..9f342e134 100644 --- a/command/service/cmd.go +++ b/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/command/stack/cmd.go b/command/stack/cmd.go index 22f076c4d..22a233441 100644 --- a/command/stack/cmd.go +++ b/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/command/swarm/cmd.go b/command/swarm/cmd.go index db2b6a253..9f9df5395 100644 --- a/command/swarm/cmd.go +++ b/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/command/volume/cmd.go b/command/volume/cmd.go index 090a00643..caf6afcaa 100644 --- a/command/volume/cmd.go +++ b/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 f14f7711e7753a60cab182674e4ea064be5f9159 Mon Sep 17 00:00:00 2001 From: John Howard Date: Wed, 14 Sep 2016 11:55:07 -0700 Subject: [PATCH 104/563] Windows: OCI process struct convergence Signed-off-by: John Howard --- command/container/run.go | 2 +- command/container/tty.go | 2 +- command/out.go | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/command/container/run.go b/command/container/run.go index d36ab610c..a167e78f9 100644 --- a/command/container/run.go +++ b/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/command/container/tty.go b/command/container/tty.go index edb11592d..6af8e2bec 100644 --- a/command/container/tty.go +++ b/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/command/out.go b/command/out.go index 09375d07d..85718d7ac 100644 --- a/command/out.go +++ b/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 3e1b9350f58802cbfd5074e855252f09ef63267b Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 19 Sep 2016 14:31:53 -0400 Subject: [PATCH 105/563] Make all the experimental subcommand consistent. Signed-off-by: Daniel Nephin --- command/checkpoint/cmd.go | 5 ++-- command/checkpoint/cmd_experimental.go | 7 ++--- command/commands/commands.go | 4 +-- command/plugin/cmd.go | 3 +- command/plugin/cmd_experimental.go | 5 ++-- command/stack/cmd.go | 31 ++++---------------- command/stack/cmd_experimental.go | 39 ++++++++++++++++++++++++++ command/stack/cmd_stub.go | 18 ------------ 8 files changed, 56 insertions(+), 56 deletions(-) create mode 100644 command/stack/cmd_experimental.go delete mode 100644 command/stack/cmd_stub.go diff --git a/command/checkpoint/cmd.go b/command/checkpoint/cmd.go index bc8224a2f..7c3950bba 100644 --- a/command/checkpoint/cmd.go +++ b/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/command/checkpoint/cmd_experimental.go b/command/checkpoint/cmd_experimental.go index c05d3ded4..3c8954577 100644 --- a/command/checkpoint/cmd_experimental.go +++ b/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/command/commands/commands.go b/command/commands/commands.go index d61823399..3e8aa25af 100644 --- a/command/commands/commands.go +++ b/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/command/plugin/cmd.go b/command/plugin/cmd.go index 67d0d5031..10074218d 100644 --- a/command/plugin/cmd.go +++ b/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/command/plugin/cmd_experimental.go b/command/plugin/cmd_experimental.go index 33c1c93ac..8bb341609 100644 --- a/command/plugin/cmd_experimental.go +++ b/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/command/stack/cmd.go b/command/stack/cmd.go index 22a233441..51cb2d1bc 100644 --- a/command/stack/cmd.go +++ b/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/command/stack/cmd_experimental.go b/command/stack/cmd_experimental.go new file mode 100644 index 000000000..d459e0a9a --- /dev/null +++ b/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/command/stack/cmd_stub.go b/command/stack/cmd_stub.go deleted file mode 100644 index 51cb2d1bc..000000000 --- a/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 20c5a9448d3d16bd473359db375796a15e9f74a9 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Mon, 25 Jul 2016 15:24:34 -0400 Subject: [PATCH 106/563] 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 --- command/formatter/formatter.go | 8 +- command/formatter/service.go | 285 ++++++++++++++++++++++++++++++++ command/service/inspect.go | 148 +++-------------- command/service/inspect_test.go | 14 +- 4 files changed, 324 insertions(+), 131 deletions(-) create mode 100644 command/formatter/service.go diff --git a/command/formatter/formatter.go b/command/formatter/formatter.go index 32f9a4d35..e859a1ca2 100644 --- a/command/formatter/formatter.go +++ b/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/command/formatter/service.go b/command/formatter/service.go new file mode 100644 index 000000000..2ce18aba5 --- /dev/null +++ b/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/command/service/inspect.go b/command/service/inspect.go index 8facb1f28..054c24383 100644 --- a/command/service/inspect.go +++ b/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/command/service/inspect_test.go b/command/service/inspect_test.go index 0e0f2ae74..8e73a70ef 100644 --- a/command/service/inspect_test.go +++ b/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 1136c3458bd9b9de95a90107e871940883bd07cc Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 19 Sep 2016 13:38:58 -0400 Subject: [PATCH 107/563] Create a system subcommand for events and info. Signed-off-by: Daniel Nephin --- command/commands/commands.go | 5 +++-- command/system/cmd.go | 27 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 command/system/cmd.go diff --git a/command/commands/commands.go b/command/commands/commands.go index d61823399..a25abf0c5 100644 --- a/command/commands/commands.go +++ b/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/command/system/cmd.go b/command/system/cmd.go new file mode 100644 index 000000000..8ce9d93ae --- /dev/null +++ b/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 1385ad8b008cbd1e0e22f915ad420bad360885d5 Mon Sep 17 00:00:00 2001 From: John Howard Date: Tue, 20 Sep 2016 12:01:04 -0700 Subject: [PATCH 108/563] Revert Box from HostConfig Signed-off-by: John Howard --- command/container/run.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/container/run.go b/command/container/run.go index a167e78f9..d36ab610c 100644 --- a/command/container/run.go +++ b/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 bfbdb15f555956fea7a377be30db31a4e231fb8d Mon Sep 17 00:00:00 2001 From: Misty Stanley-Jones Date: Thu, 1 Sep 2016 15:38:25 -0700 Subject: [PATCH 109/563] Clarify usage of --force when used on a swarm manager Fixes #26125 Signed-off-by: Misty Stanley-Jones --- command/swarm/leave.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/command/swarm/leave.go b/command/swarm/leave.go index 922411340..ae1388415 100644 --- a/command/swarm/leave.go +++ b/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 d700b905768687e556e8a6e3060ab2e98016f95e Mon Sep 17 00:00:00 2001 From: Josh Horwitz Date: Tue, 9 Aug 2016 10:34:07 -1000 Subject: [PATCH 110/563] Refactor to new events api Signed-off-by: Josh Horwitz --- command/container/run.go | 5 +--- command/container/start.go | 8 ++---- command/container/stats.go | 24 ++++++++--------- command/container/utils.go | 42 ++++++++++++++++++------------ command/system/events.go | 47 +++++++++++++++++++--------------- command/system/events_utils.go | 21 ++------------- 6 files changed, 67 insertions(+), 80 deletions(-) diff --git a/command/container/run.go b/command/container/run.go index d36ab610c..2f1181659 100644 --- a/command/container/run.go +++ b/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/command/container/start.go b/command/container/start.go index 9f414a7c6..4c31f9bf9 100644 --- a/command/container/start.go +++ b/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/command/container/stats.go b/command/container/stats.go index 2bd5e3db7..394302d08 100644 --- a/command/container/stats.go +++ b/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/command/container/utils.go b/command/container/utils.go index 7e895834f..9df1d115e 100644 --- a/command/container/utils.go +++ b/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/command/system/events.go b/command/system/events.go index f2946b876..7b5fb592c 100644 --- a/command/system/events.go +++ b/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/command/system/events_utils.go b/command/system/events_utils.go index 71c1b0476..b0dd909d1 100644 --- a/command/system/events_utils.go +++ b/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 b06f3f27a46bdab60e9a9eafaa7aa7c76ecca1fc Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 23 Jun 2016 05:00:21 +0000 Subject: [PATCH 111/563] add `docker stack ls` Signed-off-by: Akihiro Suda --- command/stack/cmd_experimental.go | 1 + command/stack/list.go | 119 ++++++++++++++++++++++++++++++ command/stack/services.go | 4 - 3 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 command/stack/list.go diff --git a/command/stack/cmd_experimental.go b/command/stack/cmd_experimental.go index d459e0a9a..b32d92533 100644 --- a/command/stack/cmd_experimental.go +++ b/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/command/stack/list.go b/command/stack/list.go new file mode 100644 index 000000000..9fe626d96 --- /dev/null +++ b/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/command/stack/services.go b/command/stack/services.go index 22906378d..60f52c30c 100644 --- a/command/stack/services.go +++ b/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 fe4cc3fd77576a6c87c862ac6898cca15242aacd 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 112/563] Implement build cache based on history array Based on work by KJ Tsanaktsidis Signed-off-by: Tonis Tiigi Signed-off-by: KJ Tsanaktsidis --- command/image/build.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/command/image/build.go b/command/image/build.go index 17be405bd..51d0ea9f0 100644 --- a/command/image/build.go +++ b/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 266b7564a5e52794217c54b1cc213f29f3dcffe0 Mon Sep 17 00:00:00 2001 From: John Howard Date: Wed, 7 Sep 2016 15:10:00 -0700 Subject: [PATCH 113/563] Add isolation to info Signed-off-by: John Howard --- command/system/info.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/command/system/info.go b/command/system/info.go index a2d0abad2..e82661d4e 100644 --- a/command/system/info.go +++ b/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 56d92bfdff47bc356528372427798f4460db0e7c Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Mon, 26 Sep 2016 10:12:24 +0100 Subject: [PATCH 114/563] 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 --- command/formatter/service.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/command/formatter/service.go b/command/formatter/service.go index 2ce18aba5..a1872e91b 100644 --- a/command/formatter/service.go +++ b/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 a16fed83af4716a80c79ad44712dfafff559e3df Mon Sep 17 00:00:00 2001 From: allencloud Date: Mon, 19 Sep 2016 16:40:44 +0800 Subject: [PATCH 115/563] validate service parameter in client side to avoid api call Signed-off-by: allencloud --- command/service/scale.go | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/command/service/scale.go b/command/service/scale.go index 2e2982db4..61b73bc35 100644 --- a/command/service/scale.go +++ b/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 2d844ea5c8834b93239bd330f53cc43b90e060e0 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sat, 23 Jul 2016 09:58:58 -0700 Subject: [PATCH 116/563] 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 --- command/task/print.go | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/command/task/print.go b/command/task/print.go index 963aea95c..b9d6b3eaf 100644 --- a/command/task/print.go +++ b/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 cc375fafd04f4149a75185a85e1b6f1fad50ae6e Mon Sep 17 00:00:00 2001 From: allencloud Date: Sun, 25 Sep 2016 16:47:45 +0800 Subject: [PATCH 117/563] add endpoint mode in service pretty Signed-off-by: allencloud --- command/formatter/service.go | 15 ++++++++++++--- command/inspect/inspector.go | 4 ++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/command/formatter/service.go b/command/formatter/service.go index a1872e91b..a92326e75 100644 --- a/command/formatter/service.go +++ b/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/command/inspect/inspector.go b/command/inspect/inspector.go index b0537e846..1d81643fb 100644 --- a/command/inspect/inspector.go +++ b/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 e0f229d2caa4a46f06a327deffcfea80fceff915 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Thu, 25 Aug 2016 21:08:53 -0700 Subject: [PATCH 118/563] 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 --- command/swarm/init.go | 2 +- command/swarm/opts.go | 19 ++++++++++++++----- command/swarm/update.go | 3 ++- command/system/info.go | 6 +++++- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/command/swarm/init.go b/command/swarm/init.go index 9a17224bd..60fb8e8fe 100644 --- a/command/swarm/init.go +++ b/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/command/swarm/opts.go b/command/swarm/opts.go index 7fcf25d13..58330b7f8 100644 --- a/command/swarm/opts.go +++ b/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/command/swarm/update.go b/command/swarm/update.go index 9884b7916..71451e450 100644 --- a/command/swarm/update.go +++ b/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/command/system/info.go b/command/system/info.go index a2d0abad2..1debf755f 100644 --- a/command/system/info.go +++ b/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 2126d8160d69e2d577cb12f8ad6a6602baa8e717 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Tue, 27 Sep 2016 15:27:02 +0000 Subject: [PATCH 119/563] 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 --- command/service/opts.go | 4 ---- command/service/opts_test.go | 31 +++++++++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/command/service/opts.go b/command/service/opts.go index 7236980e8..1e966f90c 100644 --- a/command/service/opts.go +++ b/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/command/service/opts_test.go b/command/service/opts_test.go index 30e261b8d..8ef3cacb4 100644 --- a/command/service/opts_test.go +++ b/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 f612b93d336fc9430d6780a84ad2667ebd61d0ef Mon Sep 17 00:00:00 2001 From: Zhang Wei Date: Wed, 21 Sep 2016 22:06:43 +0800 Subject: [PATCH 120/563] Support parallel kill Signed-off-by: Zhang Wei --- command/container/kill.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/command/container/kill.go b/command/container/kill.go index 8d9af6f7a..6da91a40e 100644 --- a/command/container/kill.go +++ b/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 5c1362ce59a25d0fefb3daaeec601afc66fe334c Mon Sep 17 00:00:00 2001 From: Zhang Wei Date: Wed, 21 Sep 2016 22:35:08 +0800 Subject: [PATCH 121/563] Support parallel rm Signed-off-by: Zhang Wei --- command/container/rm.go | 29 +++++++++++++---------------- command/container/utils.go | 10 +++++----- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/command/container/rm.go b/command/container/rm.go index 622a69b51..60724f194 100644 --- a/command/container/rm.go +++ b/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/command/container/utils.go b/command/container/utils.go index 7e895834f..2e129f032 100644 --- a/command/container/utils.go +++ b/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 65b1e54c73989d237cf6ef8522b88673b14c4d69 Mon Sep 17 00:00:00 2001 From: allencloud Date: Thu, 29 Sep 2016 15:35:00 +0800 Subject: [PATCH 122/563] add \n in engine labels display in docker node inspect xxx --pretty Signed-off-by: allencloud --- command/node/inspect.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/command/node/inspect.go b/command/node/inspect.go index c73b83a87..a11182f08 100644 --- a/command/node/inspect.go +++ b/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 82dc15836b93b2fb528583e67170806a746a48db Mon Sep 17 00:00:00 2001 From: Kenfe-Mickael Laventure Date: Tue, 23 Aug 2016 16:19:37 -0700 Subject: [PATCH 123/563] 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 --- command/formatter/image.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/command/formatter/image.go b/command/formatter/image.go index 54cb7b62f..39e05378c 100644 --- a/command/formatter/image.go +++ b/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 6f8bb41ecbaf076c96493a00de1ab87c748ad373 Mon Sep 17 00:00:00 2001 From: Kenfe-Mickael Laventure Date: Thu, 22 Sep 2016 14:04:34 -0700 Subject: [PATCH 124/563] Add subcommand prune to the container, volume, image and system commands Signed-off-by: Kenfe-Mickael Laventure --- command/container/cmd.go | 1 + command/container/prune.go | 74 +++++++++++++++++++++++ command/container/stats.go | 3 +- command/{system => }/events_utils.go | 2 +- command/image/cmd.go | 2 + command/image/prune.go | 90 ++++++++++++++++++++++++++++ command/prune/prune.go | 39 ++++++++++++ command/system/cmd.go | 1 + command/system/prune.go | 90 ++++++++++++++++++++++++++++ command/utils.go | 22 +++++++ command/volume/cmd.go | 1 + command/volume/prune.go | 74 +++++++++++++++++++++++ 12 files changed, 396 insertions(+), 3 deletions(-) create mode 100644 command/container/prune.go rename command/{system => }/events_utils.go (98%) create mode 100644 command/image/prune.go create mode 100644 command/prune/prune.go create mode 100644 command/system/prune.go create mode 100644 command/volume/prune.go diff --git a/command/container/cmd.go b/command/container/cmd.go index da9ea6d41..f06b863b5 100644 --- a/command/container/cmd.go +++ b/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/command/container/prune.go b/command/container/prune.go new file mode 100644 index 000000000..13e283a8b --- /dev/null +++ b/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/command/container/stats.go b/command/container/stats.go index 394302d08..2e3714486 100644 --- a/command/container/stats.go +++ b/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/command/system/events_utils.go b/command/events_utils.go similarity index 98% rename from command/system/events_utils.go rename to command/events_utils.go index b0dd909d1..e710c9757 100644 --- a/command/system/events_utils.go +++ b/command/events_utils.go @@ -1,4 +1,4 @@ -package system +package command import ( "sync" diff --git a/command/image/cmd.go b/command/image/cmd.go index f60ffeeb8..6f8e7b7d4 100644 --- a/command/image/cmd.go +++ b/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/command/image/prune.go b/command/image/prune.go new file mode 100644 index 000000000..6944664a5 --- /dev/null +++ b/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/command/prune/prune.go b/command/prune/prune.go new file mode 100644 index 000000000..0b1374eda --- /dev/null +++ b/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/command/system/cmd.go b/command/system/cmd.go index 8ce9d93ae..f967c1b72 100644 --- a/command/system/cmd.go +++ b/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/command/system/prune.go b/command/system/prune.go new file mode 100644 index 000000000..4a9e952ad --- /dev/null +++ b/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/command/utils.go b/command/utils.go index bceb7b335..e768cf770 100644 --- a/command/utils.go +++ b/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/command/volume/cmd.go b/command/volume/cmd.go index caf6afcaa..5f39d3cf3 100644 --- a/command/volume/cmd.go +++ b/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/command/volume/prune.go b/command/volume/prune.go new file mode 100644 index 000000000..59f3c9463 --- /dev/null +++ b/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 d6b5a807d7a89738e6d44d4053669cc40549ab6a Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 29 Sep 2016 17:59:52 -0400 Subject: [PATCH 125/563] Use ListOpt for labels. Signed-off-by: Daniel Nephin --- command/image/build.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/command/image/build.go b/command/image/build.go index 51d0ea9f0..fa7abe402 100644 --- a/command/image/build.go +++ b/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 3f8c4be283305f9654235e66f1d60b25deb0f898 Mon Sep 17 00:00:00 2001 From: Kenfe-Mickael Laventure Date: Tue, 23 Aug 2016 16:37:37 -0700 Subject: [PATCH 126/563] 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 --- command/formatter/container.go | 14 ++ command/formatter/disk_usage.go | 331 ++++++++++++++++++++++++++++++++ command/formatter/image.go | 30 +++ command/formatter/image_test.go | 2 +- command/formatter/volume.go | 18 ++ command/system/cmd.go | 1 + command/system/df.go | 55 ++++++ 7 files changed, 450 insertions(+), 1 deletion(-) create mode 100644 command/formatter/disk_usage.go create mode 100644 command/system/df.go diff --git a/command/formatter/container.go b/command/formatter/container.go index 30a649247..ceef75890 100644 --- a/command/formatter/container.go +++ b/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/command/formatter/disk_usage.go b/command/formatter/disk_usage.go new file mode 100644 index 000000000..866e9bd04 --- /dev/null +++ b/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/command/formatter/image.go b/command/formatter/image.go index 39e05378c..1e71bda3a 100644 --- a/command/formatter/image.go +++ b/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/command/formatter/image_test.go b/command/formatter/image_test.go index 6dc7f73db..73b3c3f2e 100644 --- a/command/formatter/image_test.go +++ b/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/command/formatter/volume.go b/command/formatter/volume.go index e41ee266b..8fb11732e 100644 --- a/command/formatter/volume.go +++ b/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/command/system/cmd.go b/command/system/cmd.go index f967c1b72..46caa2491 100644 --- a/command/system/cmd.go +++ b/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/command/system/df.go b/command/system/df.go new file mode 100644 index 000000000..085d680fe --- /dev/null +++ b/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 e25646bbc039d5b428be77a7bf9742279fec9180 Mon Sep 17 00:00:00 2001 From: Paul Kehrer Date: Thu, 18 Aug 2016 16:35:23 +0800 Subject: [PATCH 127/563] 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 --- command/image/build.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/command/image/build.go b/command/image/build.go index 51d0ea9f0..2bd80f708 100644 --- a/command/image/build.go +++ b/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 e307da732af29d6b96950290697567c758bdf11c Mon Sep 17 00:00:00 2001 From: John Howard Date: Tue, 7 Jun 2016 12:15:50 -0700 Subject: [PATCH 128/563] Windows: Support credential specs Signed-off-by: John Howard --- command/image/build.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/command/image/build.go b/command/image/build.go index ccfebb983..19fd4aa70 100644 --- a/command/image/build.go +++ b/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 80bc9172264426bae397d9f5b449c231bd5f4aa4 Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Mon, 10 Oct 2016 23:07:32 +0800 Subject: [PATCH 129/563] Add the OPTIONS and Fix the links for contain prune Signed-off-by: yuexiao-wang --- command/container/prune.go | 2 +- command/image/prune.go | 2 +- command/system/prune.go | 2 +- command/volume/prune.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/command/container/prune.go b/command/container/prune.go index 13e283a8b..708803861 100644 --- a/command/container/prune.go +++ b/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/command/image/prune.go b/command/image/prune.go index 6944664a5..e5ad57313 100644 --- a/command/image/prune.go +++ b/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/command/system/prune.go b/command/system/prune.go index 4a9e952ad..6a36fdd89 100644 --- a/command/system/prune.go +++ b/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/command/volume/prune.go b/command/volume/prune.go index 59f3c9463..dc2d3e25b 100644 --- a/command/volume/prune.go +++ b/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 871b6928337440a692ee45a91f701fbceb3f41f4 Mon Sep 17 00:00:00 2001 From: allencloud Date: Sat, 8 Oct 2016 18:38:25 +0800 Subject: [PATCH 130/563] better prune and system df Signed-off-by: allencloud --- command/container/prune.go | 2 +- command/image/prune.go | 2 +- command/prune/prune.go | 12 ++++++------ command/system/df.go | 2 +- command/system/prune.go | 4 ++-- command/volume/prune.go | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/command/container/prune.go b/command/container/prune.go index 708803861..be67fe4ca 100644 --- a/command/container/prune.go +++ b/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/command/image/prune.go b/command/image/prune.go index e5ad57313..46bd56cb1 100644 --- a/command/image/prune.go +++ b/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/command/prune/prune.go b/command/prune/prune.go index 0b1374eda..fd04c590b 100644 --- a/command/prune/prune.go +++ b/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/command/system/df.go b/command/system/df.go index 085d680fe..293946c18 100644 --- a/command/system/df.go +++ b/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/command/system/prune.go b/command/system/prune.go index 6a36fdd89..ea8a41380 100644 --- a/command/system/prune.go +++ b/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/command/volume/prune.go b/command/volume/prune.go index dc2d3e25b..a4bb0092d 100644 --- a/command/volume/prune.go +++ b/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 3bc50c45ba84c4d177a2a58afec8538268ce08fd Mon Sep 17 00:00:00 2001 From: Boaz Shuster Date: Thu, 22 Sep 2016 15:54:41 +0300 Subject: [PATCH 131/563] 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 --- command/container/stats.go | 24 +++--- command/container/stats_helpers.go | 57 +++++-------- command/formatter/stats.go | 132 +++++++++++++++++++++-------- 3 files changed, 132 insertions(+), 81 deletions(-) diff --git a/command/container/stats.go b/command/container/stats.go index 2bd5e3db7..f60224796 100644 --- a/command/container/stats.go +++ b/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/command/container/stats_helpers.go b/command/container/stats_helpers.go index 2039d2ade..32ad84841 100644 --- a/command/container/stats_helpers.go +++ b/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/command/formatter/stats.go b/command/formatter/stats.go index 939431da1..c7b30c9f3 100644 --- a/command/formatter/stats.go +++ b/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 bed046666a4efa8e675f41bd4287ded917e440f2 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sun, 25 Sep 2016 16:26:46 +0200 Subject: [PATCH 132/563] 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 --- flags/common.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flags/common.go b/flags/common.go index 2318b9d97..758e0a66c 100644 --- a/flags/common.go +++ b/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 6ef1c7deaf031579a692d30d7601559a9c6e6109 Mon Sep 17 00:00:00 2001 From: allencloud Date: Sun, 9 Oct 2016 10:29:58 +0800 Subject: [PATCH 133/563] return nil when no node or service to avoid additional api call Signed-off-by: allencloud --- command/node/list.go | 22 +++++++++++++--------- command/service/list.go | 13 +++++++++---- command/utils.go | 6 +++--- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/command/node/list.go b/command/node/list.go index bed4bc496..d028d1914 100644 --- a/command/node/list.go +++ b/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/command/service/list.go b/command/service/list.go index 681acd3f2..2278643fb 100644 --- a/command/service/list.go +++ b/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/command/utils.go b/command/utils.go index e768cf770..9f9a1ee80 100644 --- a/command/utils.go +++ b/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 f4267969c796bcacea13b0a293eed89423e1c58c Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Tue, 11 Oct 2016 19:35:12 +0800 Subject: [PATCH 134/563] Modify function name from SetDaemonLogLevel to SetLogLevel Signed-off-by: yuexiao-wang --- flags/common.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/flags/common.go b/flags/common.go index 2318b9d97..074d53e31 100644 --- a/flags/common.go +++ b/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 5018781cab7b7458588ad28b639d6d0749a4da67 Mon Sep 17 00:00:00 2001 From: Kenfe-Mickael Laventure Date: Tue, 11 Oct 2016 11:49:26 -0700 Subject: [PATCH 135/563] 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 --- command/formatter/disk_usage.go | 14 +++++++------- command/formatter/volume.go | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/command/formatter/disk_usage.go b/command/formatter/disk_usage.go index 866e9bd04..acb210dbf 100644 --- a/command/formatter/disk_usage.go +++ b/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/command/formatter/volume.go b/command/formatter/volume.go index 8fb11732e..7bc353757 100644 --- a/command/formatter/volume.go +++ b/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 49e49e8e0069fd589c67d5538cd674ea15aa09c6 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 12 Oct 2016 16:06:34 -0700 Subject: [PATCH 136/563] 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 --- command/network/create.go | 7 ++++--- command/volume/create.go | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/command/network/create.go b/command/network/create.go index 2ffd80548..abc494e1e 100644 --- a/command/network/create.go +++ b/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/command/volume/create.go b/command/volume/create.go index 4427ff1ea..fbf62a5ef 100644 --- a/command/volume/create.go +++ b/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 b6fbe832ac559d7a75862cfc8ed695fc64fc7764 Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Thu, 13 Oct 2016 19:35:10 +0800 Subject: [PATCH 137/563] Fix the incorrect description for NewInStream Signed-off-by: yuexiao-wang --- command/in.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/in.go b/command/in.go index c3ed70dc1..7204b7ad0 100644 --- a/command/in.go +++ b/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 0cb01799e925c334508e9abe25f65af727ca7cdf Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 12 Oct 2016 10:24:19 -0700 Subject: [PATCH 138/563] 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 --- command/stack/deploy.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/command/stack/deploy.go b/command/stack/deploy.go index 6daf9500f..bf31dd775 100644 --- a/command/stack/deploy.go +++ b/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 43d7c0ed9af3a1ecf13c09544d664fb4ca154bc8 Mon Sep 17 00:00:00 2001 From: cyli Date: Wed, 28 Sep 2016 12:49:47 -0700 Subject: [PATCH 139/563] 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 --- command/image/trust.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/command/image/trust.go b/command/image/trust.go index b08bd490c..b8de6a524 100644 --- a/command/image/trust.go +++ b/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 c11155f0d121a44c1c6793945e39251ff260d1e4 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Tue, 13 Sep 2016 07:01:31 +0000 Subject: [PATCH 140/563] 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 --- command/formatter/container.go | 4 ++ command/formatter/container_test.go | 47 ++++++++++++++++++++ command/formatter/network.go | 4 ++ command/formatter/network_test.go | 46 ++++++++++++++++++++ command/formatter/reflect.go | 65 ++++++++++++++++++++++++++++ command/formatter/reflect_test.go | 66 +++++++++++++++++++++++++++++ command/formatter/service.go | 4 ++ command/formatter/volume.go | 4 ++ command/formatter/volume_test.go | 45 ++++++++++++++++++++ command/service/inspect_test.go | 47 +++++++++++++++++--- 10 files changed, 325 insertions(+), 7 deletions(-) create mode 100644 command/formatter/reflect.go create mode 100644 command/formatter/reflect_test.go diff --git a/command/formatter/container.go b/command/formatter/container.go index ceef75890..094bc8544 100644 --- a/command/formatter/container.go +++ b/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/command/formatter/container_test.go b/command/formatter/container_test.go index 1ef48ae2d..4b520f94b 100644 --- a/command/formatter/container_test.go +++ b/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/command/formatter/network.go b/command/formatter/network.go index d808fdc22..7fbad7d2a 100644 --- a/command/formatter/network.go +++ b/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/command/formatter/network_test.go b/command/formatter/network_test.go index 28f078548..b40a534ee 100644 --- a/command/formatter/network_test.go +++ b/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/command/formatter/reflect.go b/command/formatter/reflect.go new file mode 100644 index 000000000..d1d8737d2 --- /dev/null +++ b/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/command/formatter/reflect_test.go b/command/formatter/reflect_test.go new file mode 100644 index 000000000..e547b1841 --- /dev/null +++ b/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/command/formatter/service.go b/command/formatter/service.go index a92326e75..71ee4d656 100644 --- a/command/formatter/service.go +++ b/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/command/formatter/volume.go b/command/formatter/volume.go index 7bc353757..b76c8ba03 100644 --- a/command/formatter/volume.go +++ b/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/command/formatter/volume_test.go b/command/formatter/volume_test.go index 8c715b343..4e1c8d3ab 100644 --- a/command/formatter/volume_test.go +++ b/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/command/service/inspect_test.go b/command/service/inspect_test.go index 8e73a70ef..04a65080c 100644 --- a/command/service/inspect_test.go +++ b/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 6c3c3a87ba4b3e80aeca4faa64a6eed057395c06 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 17 Oct 2016 20:03:31 +0200 Subject: [PATCH 141/563] Revert docker volume column name to VOLUME_NAME Signed-off-by: Vincent Demeester --- command/formatter/volume.go | 3 ++- command/formatter/volume_test.go | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/command/formatter/volume.go b/command/formatter/volume.go index 7bc353757..695885b2f 100644 --- a/command/formatter/volume.go +++ b/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/command/formatter/volume_test.go b/command/formatter/volume_test.go index 8c715b343..8b92e8e16 100644 --- a/command/formatter/volume_test.go +++ b/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 c424fb0e3dd38209c0a089a34bedc4bd3a1a9702 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Mon, 6 Jun 2016 20:29:05 -0700 Subject: [PATCH 142/563] 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 --- command/container/restart.go | 13 ++++++++++--- command/container/stop.go | 13 ++++++++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/command/container/restart.go b/command/container/restart.go index e370ef401..fc3ba93c8 100644 --- a/command/container/restart.go +++ b/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/command/container/stop.go b/command/container/stop.go index 2f22fd09a..c68ede536 100644 --- a/command/container/stop.go +++ b/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 093072cc187b2a38b48348fc6c142c8b296832f5 Mon Sep 17 00:00:00 2001 From: allencloud Date: Tue, 18 Oct 2016 14:20:12 +0800 Subject: [PATCH 143/563] wrap line in deleted containers when pruning Signed-off-by: allencloud --- command/container/prune.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/container/prune.go b/command/container/prune.go index be67fe4ca..679471398 100644 --- a/command/container/prune.go +++ b/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 24d0191a3a73506532b835e87ba3c7925aa6ef90 Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Tue, 18 Oct 2016 18:50:11 +0800 Subject: [PATCH 144/563] Fix typs from go to Go Signed-off-by: yuexiao-wang --- command/container/inspect.go | 2 +- command/image/inspect.go | 2 +- command/network/inspect.go | 2 +- command/node/inspect.go | 2 +- command/plugin/inspect.go | 2 +- command/service/inspect.go | 2 +- command/system/events.go | 2 +- command/system/info.go | 2 +- command/system/inspect.go | 2 +- command/system/version.go | 2 +- command/volume/inspect.go | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/command/container/inspect.go b/command/container/inspect.go index 0bef51a61..08a8d244d 100644 --- a/command/container/inspect.go +++ b/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/command/image/inspect.go b/command/image/inspect.go index 11c528ef2..217863c77 100644 --- a/command/image/inspect.go +++ b/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/command/network/inspect.go b/command/network/inspect.go index f1f677db9..1a86855f7 100644 --- a/command/network/inspect.go +++ b/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/command/node/inspect.go b/command/node/inspect.go index a11182f08..0812ec5ea 100644 --- a/command/node/inspect.go +++ b/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/command/plugin/inspect.go b/command/plugin/inspect.go index a1cf1f7b0..e5059629e 100644 --- a/command/plugin/inspect.go +++ b/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/command/service/inspect.go b/command/service/inspect.go index 054c24383..deb701bf6 100644 --- a/command/service/inspect.go +++ b/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/command/system/events.go b/command/system/events.go index 7b5fb592c..087523051 100644 --- a/command/system/events.go +++ b/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/command/system/info.go b/command/system/info.go index 8116ae524..fb5ea17d7 100644 --- a/command/system/info.go +++ b/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/command/system/inspect.go b/command/system/inspect.go index 015c1b5c6..d7a24854e 100644 --- a/command/system/inspect.go +++ b/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/command/system/version.go b/command/system/version.go index e77719ec3..7959bf564 100644 --- a/command/system/version.go +++ b/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/command/volume/inspect.go b/command/volume/inspect.go index ab06e0380..5eb8ad251 100644 --- a/command/volume/inspect.go +++ b/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 805f66951279850688ee9a4478ad76445dea2c15 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Tue, 12 Jul 2016 05:08:05 -0700 Subject: [PATCH 145/563] 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 --- command/system/info.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/command/system/info.go b/command/system/info.go index fb5ea17d7..300a3960b 100644 --- a/command/system/info.go +++ b/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 06ebd4517db1bdd4d4ca699d276a67f4de1071c3 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Fri, 2 Sep 2016 14:12:05 -0700 Subject: [PATCH 146/563] 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 --- command/formatter/service.go | 18 +++++- command/service/opts.go | 104 +++++++++++++++++++---------------- command/service/update.go | 34 ++++++++++-- 3 files changed, 103 insertions(+), 53 deletions(-) diff --git a/command/formatter/service.go b/command/formatter/service.go index 71ee4d656..1549047b7 100644 --- a/command/formatter/service.go +++ b/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/command/service/opts.go b/command/service/opts.go index 1e966f90c..cf25b7827 100644 --- a/command/service/opts.go +++ b/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/command/service/update.go b/command/service/update.go index be3218ed6..797c98927 100644 --- a/command/service/update.go +++ b/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 49512f901c0d2f340debc4e22cb3dbb55b2d9af1 Mon Sep 17 00:00:00 2001 From: allencloud Date: Wed, 19 Oct 2016 06:29:27 +0800 Subject: [PATCH 147/563] make every node and plugin removal call api Signed-off-by: allencloud --- command/node/remove.go | 12 +++++++++++- command/plugin/remove.go | 6 ++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/command/node/remove.go b/command/node/remove.go index 696cd5871..9ba21b44a 100644 --- a/command/node/remove.go +++ b/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/command/plugin/remove.go b/command/plugin/remove.go index 800fc1b97..4222690a4 100644 --- a/command/plugin/remove.go +++ b/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 f2a6d37388a66faf37ccf037fbf9a906b9e5167b Mon Sep 17 00:00:00 2001 From: allencloud Date: Wed, 19 Oct 2016 14:35:05 +0800 Subject: [PATCH 148/563] change join node role judge Signed-off-by: allencloud --- command/swarm/join.go | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/command/swarm/join.go b/command/swarm/join.go index 72f97c015..004313b4c 100644 --- a/command/swarm/join.go +++ b/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 a528b05dab7e47fa8667ab3c96e7d60bc679d3b5 Mon Sep 17 00:00:00 2001 From: Jonh Wendell Date: Wed, 13 Jul 2016 14:24:41 -0300 Subject: [PATCH 149/563] 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 --- command/container/exec.go | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/command/container/exec.go b/command/container/exec.go index 1682a7ca6..48964693b 100644 --- a/command/container/exec.go +++ b/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 08ac5a303969838178d344a457b0de60475cb73c Mon Sep 17 00:00:00 2001 From: Kenfe-Mickael Laventure Date: Wed, 19 Oct 2016 15:09:42 -0700 Subject: [PATCH 150/563] 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 --- command/container/list.go | 7 +++++++ command/formatter/container.go | 16 ++++++++++++++++ command/formatter/container_test.go | 4 ++-- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/command/container/list.go b/command/container/list.go index 7f10ce8bd..2d46b6604 100644 --- a/command/container/list.go +++ b/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/command/formatter/container.go b/command/formatter/container.go index 094bc8544..627345335 100644 --- a/command/formatter/container.go +++ b/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/command/formatter/container_test.go b/command/formatter/container_test.go index 4b520f94b..0a844efb6 100644 --- a/command/formatter/container_test.go +++ b/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 ef87035bbb83de19a4ca8118c2a8a10675ea3721 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 3 Oct 2016 15:17:39 -0400 Subject: [PATCH 151/563] Generate api/types:Image from the swagger spec and rename it to a more appropriate name ImageSummary. Signed-off-by: Daniel Nephin --- command/formatter/disk_usage.go | 4 ++-- command/formatter/image.go | 8 ++++---- command/formatter/image_test.go | 20 ++++++++++---------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/command/formatter/disk_usage.go b/command/formatter/disk_usage.go index acb210dbf..6f97d3b0f 100644 --- a/command/formatter/disk_usage.go +++ b/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/command/formatter/image.go b/command/formatter/image.go index 1e71bda3a..594b2f392 100644 --- a/command/formatter/image.go +++ b/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/command/formatter/image_test.go b/command/formatter/image_test.go index 73b3c3f2e..ffe77f667 100644 --- a/command/formatter/image_test.go +++ b/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 dfed71a6ddf76afde84d3eb79e9c8cf3ab8635e3 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Thu, 20 Oct 2016 12:04:01 -0700 Subject: [PATCH 152/563] 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 --- command/service/update.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/command/service/update.go b/command/service/update.go index 797c98927..6034979a6 100644 --- a/command/service/update.go +++ b/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 15bbb6171119aee288bd898d49f50d4ad560f692 Mon Sep 17 00:00:00 2001 From: Amit Krishnan Date: Mon, 24 Oct 2016 15:06:33 -0700 Subject: [PATCH 153/563] 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 --- command/formatter/stats.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/formatter/stats.go b/command/formatter/stats.go index c7b30c9f3..212a1b4f5 100644 --- a/command/formatter/stats.go +++ b/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 66bd963b766062e328e38519a6c2279fc8299efa Mon Sep 17 00:00:00 2001 From: Kenfe-Mickael Laventure Date: Thu, 6 Oct 2016 07:09:54 -0700 Subject: [PATCH 154/563] Make experimental a runtime flag Signed-off-by: Kenfe-Mickael Laventure --- command/bundlefile/bundlefile.go | 2 - command/bundlefile/bundlefile_test.go | 2 - command/checkpoint/cmd.go | 20 ++++++++-- command/checkpoint/cmd_experimental.go | 30 -------------- command/checkpoint/create.go | 2 - command/checkpoint/list.go | 2 - command/checkpoint/remove.go | 2 - command/cli.go | 28 ++++++++++--- command/commands/commands.go | 14 +++++-- command/container/start.go | 4 +- command/container/start_utils.go | 8 ---- command/container/start_utils_experimental.go | 9 ----- command/plugin/cmd.go | 26 ++++++++++-- command/plugin/cmd_experimental.go | 35 ---------------- command/plugin/disable.go | 2 - command/plugin/enable.go | 2 - command/plugin/inspect.go | 2 - command/plugin/install.go | 2 - command/plugin/list.go | 2 - command/plugin/push.go | 2 - command/plugin/remove.go | 2 - command/plugin/set.go | 2 - command/stack/cmd.go | 32 ++++++++++++--- command/stack/cmd_experimental.go | 40 ------------------- command/stack/common.go | 2 - command/stack/config.go | 2 - command/stack/deploy.go | 2 - command/stack/list.go | 2 - command/stack/opts.go | 2 - command/stack/ps.go | 2 - command/stack/remove.go | 2 - command/stack/services.go | 2 - command/system/info.go | 2 +- command/system/version.go | 23 +++++------ 34 files changed, 112 insertions(+), 201 deletions(-) delete mode 100644 command/checkpoint/cmd_experimental.go delete mode 100644 command/container/start_utils.go delete mode 100644 command/container/start_utils_experimental.go delete mode 100644 command/plugin/cmd_experimental.go delete mode 100644 command/stack/cmd_experimental.go diff --git a/command/bundlefile/bundlefile.go b/command/bundlefile/bundlefile.go index 75c2d0743..7fd1e4f6c 100644 --- a/command/bundlefile/bundlefile.go +++ b/command/bundlefile/bundlefile.go @@ -1,5 +1,3 @@ -// +build experimental - package bundlefile import ( diff --git a/command/bundlefile/bundlefile_test.go b/command/bundlefile/bundlefile_test.go index 1ff8235ff..c343410df 100644 --- a/command/bundlefile/bundlefile_test.go +++ b/command/bundlefile/bundlefile_test.go @@ -1,5 +1,3 @@ -// +build experimental - package bundlefile import ( diff --git a/command/checkpoint/cmd.go b/command/checkpoint/cmd.go index 7c3950bba..84084ab71 100644 --- a/command/checkpoint/cmd.go +++ b/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/command/checkpoint/cmd_experimental.go b/command/checkpoint/cmd_experimental.go deleted file mode 100644 index 3c8954577..000000000 --- a/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/command/checkpoint/create.go b/command/checkpoint/create.go index f21457455..d36971811 100644 --- a/command/checkpoint/create.go +++ b/command/checkpoint/create.go @@ -1,5 +1,3 @@ -// +build experimental - package checkpoint import ( diff --git a/command/checkpoint/list.go b/command/checkpoint/list.go index 6d22531d4..7ba035890 100644 --- a/command/checkpoint/list.go +++ b/command/checkpoint/list.go @@ -1,5 +1,3 @@ -// +build experimental - package checkpoint import ( diff --git a/command/checkpoint/remove.go b/command/checkpoint/remove.go index 6605c5e47..82ce62312 100644 --- a/command/checkpoint/remove.go +++ b/command/checkpoint/remove.go @@ -1,5 +1,3 @@ -// +build experimental - package checkpoint import ( diff --git a/command/cli.go b/command/cli.go index 9ca28765c..be82ecf6f 100644 --- a/command/cli.go +++ b/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/command/commands/commands.go b/command/commands/commands.go index 6d0deb1d9..425f90ba7 100644 --- a/command/commands/commands.go +++ b/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/command/container/start.go b/command/container/start.go index 4c31f9bf9..8693b3a55 100644 --- a/command/container/start.go +++ b/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/command/container/start_utils.go b/command/container/start_utils.go deleted file mode 100644 index 689d742f0..000000000 --- a/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/command/container/start_utils_experimental.go b/command/container/start_utils_experimental.go deleted file mode 100644 index 43c64f431..000000000 --- a/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/command/plugin/cmd.go b/command/plugin/cmd.go index 10074218d..80fa61cb1 100644 --- a/command/plugin/cmd.go +++ b/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/command/plugin/cmd_experimental.go b/command/plugin/cmd_experimental.go deleted file mode 100644 index 8bb341609..000000000 --- a/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/command/plugin/disable.go b/command/plugin/disable.go index 3b5c69a01..9089a3cf6 100644 --- a/command/plugin/disable.go +++ b/command/plugin/disable.go @@ -1,5 +1,3 @@ -// +build experimental - package plugin import ( diff --git a/command/plugin/enable.go b/command/plugin/enable.go index cfc3580f4..0fd8f469d 100644 --- a/command/plugin/enable.go +++ b/command/plugin/enable.go @@ -1,5 +1,3 @@ -// +build experimental - package plugin import ( diff --git a/command/plugin/inspect.go b/command/plugin/inspect.go index e5059629e..13c7fa72d 100644 --- a/command/plugin/inspect.go +++ b/command/plugin/inspect.go @@ -1,5 +1,3 @@ -// +build experimental - package plugin import ( diff --git a/command/plugin/install.go b/command/plugin/install.go index e90e8d122..3989a35ce 100644 --- a/command/plugin/install.go +++ b/command/plugin/install.go @@ -1,5 +1,3 @@ -// +build experimental - package plugin import ( diff --git a/command/plugin/list.go b/command/plugin/list.go index b8f5e5e08..9d4b46d12 100644 --- a/command/plugin/list.go +++ b/command/plugin/list.go @@ -1,5 +1,3 @@ -// +build experimental - package plugin import ( diff --git a/command/plugin/push.go b/command/plugin/push.go index 360830902..4e176bea3 100644 --- a/command/plugin/push.go +++ b/command/plugin/push.go @@ -1,5 +1,3 @@ -// +build experimental - package plugin import ( diff --git a/command/plugin/remove.go b/command/plugin/remove.go index 4222690a4..7a51dce06 100644 --- a/command/plugin/remove.go +++ b/command/plugin/remove.go @@ -1,5 +1,3 @@ -// +build experimental - package plugin import ( diff --git a/command/plugin/set.go b/command/plugin/set.go index f2d3b082c..e58ea63bc 100644 --- a/command/plugin/set.go +++ b/command/plugin/set.go @@ -1,5 +1,3 @@ -// +build experimental - package plugin import ( diff --git a/command/stack/cmd.go b/command/stack/cmd.go index 51cb2d1bc..49fcedf20 100644 --- a/command/stack/cmd.go +++ b/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/command/stack/cmd_experimental.go b/command/stack/cmd_experimental.go deleted file mode 100644 index b32d92533..000000000 --- a/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/command/stack/common.go b/command/stack/common.go index 2afdb5147..3e3a35faa 100644 --- a/command/stack/common.go +++ b/command/stack/common.go @@ -1,5 +1,3 @@ -// +build experimental - package stack import ( diff --git a/command/stack/config.go b/command/stack/config.go index bdcf7d483..56e554a86 100644 --- a/command/stack/config.go +++ b/command/stack/config.go @@ -1,5 +1,3 @@ -// +build experimental - package stack import ( diff --git a/command/stack/deploy.go b/command/stack/deploy.go index bf31dd775..fcf55fb7d 100644 --- a/command/stack/deploy.go +++ b/command/stack/deploy.go @@ -1,5 +1,3 @@ -// +build experimental - package stack import ( diff --git a/command/stack/list.go b/command/stack/list.go index 9fe626d96..5d87cecb5 100644 --- a/command/stack/list.go +++ b/command/stack/list.go @@ -1,5 +1,3 @@ -// +build experimental - package stack import ( diff --git a/command/stack/opts.go b/command/stack/opts.go index eef4d0e45..5f2d8b5d0 100644 --- a/command/stack/opts.go +++ b/command/stack/opts.go @@ -1,5 +1,3 @@ -// +build experimental - package stack import ( diff --git a/command/stack/ps.go b/command/stack/ps.go index c4683b68a..2fff3de1f 100644 --- a/command/stack/ps.go +++ b/command/stack/ps.go @@ -1,5 +1,3 @@ -// +build experimental - package stack import ( diff --git a/command/stack/remove.go b/command/stack/remove.go index 6ab005d71..8137903d4 100644 --- a/command/stack/remove.go +++ b/command/stack/remove.go @@ -1,5 +1,3 @@ -// +build experimental - package stack import ( diff --git a/command/stack/services.go b/command/stack/services.go index 60f52c30c..50b50179d 100644 --- a/command/stack/services.go +++ b/command/stack/services.go @@ -1,5 +1,3 @@ -// +build experimental - package stack import ( diff --git a/command/system/info.go b/command/system/info.go index fb5ea17d7..0f94373d3 100644 --- a/command/system/info.go +++ b/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/command/system/version.go b/command/system/version.go index 7959bf564..0b484cb3b 100644 --- a/command/system/version.go +++ b/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 d5d520f0d739ae34355eb0483e5dac39ad8f8cea Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Tue, 18 Oct 2016 04:36:52 +0000 Subject: [PATCH 155/563] 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 --- command/network/cmd.go | 1 + command/network/prune.go | 72 ++++++++++++++++++++++++++++++++++++++++ command/prune/prune.go | 11 ++++++ command/system/prune.go | 6 ++-- 4 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 command/network/prune.go diff --git a/command/network/cmd.go b/command/network/cmd.go index b33f98cd3..77c8e4908 100644 --- a/command/network/cmd.go +++ b/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/command/network/prune.go b/command/network/prune.go new file mode 100644 index 000000000..00e05d3bd --- /dev/null +++ b/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/command/prune/prune.go b/command/prune/prune.go index fd04c590b..a022487fd 100644 --- a/command/prune/prune.go +++ b/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/command/system/prune.go b/command/system/prune.go index ea8a41380..c79bc6910 100644 --- a/command/system/prune.go +++ b/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 3c9dff2f758f7f18f0af4f327b7407f9923af53b Mon Sep 17 00:00:00 2001 From: sandyskies Date: Sun, 6 Mar 2016 20:29:23 +0800 Subject: [PATCH 156/563] add --network option for docker build Signed-off-by: sandyskies Signed-off-by: Tonis Tiigi --- command/image/build.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/command/image/build.go b/command/image/build.go index 19fd4aa70..7db76a649 100644 --- a/command/image/build.go +++ b/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 dff7842790555b7862f1bc32d2f932f8a36a18c4 Mon Sep 17 00:00:00 2001 From: John Howard Date: Tue, 25 Oct 2016 16:19:14 -0700 Subject: [PATCH 157/563] Windows: Fix stats CLI Signed-off-by: John Howard --- command/container/stats.go | 11 ++++++++++- command/formatter/stats.go | 33 ++++++++++++++++++--------------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/command/container/stats.go b/command/container/stats.go index 4d35e744c..e7954e4b9 100644 --- a/command/container/stats.go +++ b/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/command/formatter/stats.go b/command/formatter/stats.go index 212a1b4f5..cc2588c39 100644 --- a/command/formatter/stats.go +++ b/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 4f320d7c2a9fde514ae1ef4349087058a8b2dc83 Mon Sep 17 00:00:00 2001 From: "Erik St. Martin" Date: Tue, 7 Jun 2016 15:05:43 -0400 Subject: [PATCH 158/563] 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 --- command/container/update.go | 48 +++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/command/container/update.go b/command/container/update.go index b5770c899..5bacc9be7 100644 --- a/command/container/update.go +++ b/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 c1da6dc7ac95804d6dd66ef0f1230a3521d120c3 Mon Sep 17 00:00:00 2001 From: Boaz Shuster Date: Tue, 25 Oct 2016 01:55:29 +0300 Subject: [PATCH 159/563] Add unit tests to cli/command/formatter/stats.go Signed-off-by: Boaz Shuster --- command/formatter/stats.go | 2 +- command/formatter/stats_test.go | 228 ++++++++++++++++++++++++++++++++ 2 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 command/formatter/stats_test.go diff --git a/command/formatter/stats.go b/command/formatter/stats.go index cc2588c39..b2c972251 100644 --- a/command/formatter/stats.go +++ b/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/command/formatter/stats_test.go b/command/formatter/stats_test.go new file mode 100644 index 000000000..f1f449e71 --- /dev/null +++ b/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 6c80d2bb83ed05d98b65591b5eb64c46934a86f4 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Wed, 28 Sep 2016 12:34:31 +0200 Subject: [PATCH 160/563] 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 --- command/service/create.go | 2 ++ command/service/opts.go | 1 - command/service/update.go | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/command/service/create.go b/command/service/create.go index bc5576b1a..bb7af41f9 100644 --- a/command/service/create.go +++ b/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/command/service/opts.go b/command/service/opts.go index cf25b7827..d0d383a7a 100644 --- a/command/service/opts.go +++ b/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/command/service/update.go b/command/service/update.go index 6034979a6..e1f7cad66 100644 --- a/command/service/update.go +++ b/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 1250d2afae8d0196f1a3d6c77c001f3e6c3f55a3 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Tue, 19 Jul 2016 23:58:32 -0700 Subject: [PATCH 161/563] 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 --- command/service/create.go | 1 + command/service/opts.go | 24 +++++++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/command/service/create.go b/command/service/create.go index bc5576b1a..59e838ca8 100644 --- a/command/service/create.go +++ b/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/command/service/opts.go b/command/service/opts.go index cf25b7827..87968fd1b 100644 --- a/command/service/opts.go +++ b/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 4c1560e9879673b0a9159fb9049e20768e390eff Mon Sep 17 00:00:00 2001 From: Ce Gao Date: Fri, 28 Oct 2016 08:02:57 +0800 Subject: [PATCH 162/563] fixes #27643 Signed-off-by: Ce Gao --- command/service/ps.go | 5 +++++ command/task/print.go | 29 ++++++++++++++++++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/command/service/ps.go b/command/service/ps.go index 23c3679d7..55f837ba8 100644 --- a/command/service/ps.go +++ b/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/command/task/print.go b/command/task/print.go index b9d6b3eaf..b3cdcbe53 100644 --- a/command/task/print.go +++ b/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 5ddcbc3c0058d1982f73305db1ceb372bcf1b229 Mon Sep 17 00:00:00 2001 From: boucher Date: Mon, 19 Sep 2016 12:01:16 -0400 Subject: [PATCH 163/563] Allow providing a custom storage directory for docker checkpoints Signed-off-by: boucher --- command/checkpoint/create.go | 13 ++++++++----- command/checkpoint/list.go | 25 +++++++++++++++++++++---- command/checkpoint/remove.go | 26 ++++++++++++++++++++++---- command/container/start.go | 16 ++++++++++------ 4 files changed, 61 insertions(+), 19 deletions(-) diff --git a/command/checkpoint/create.go b/command/checkpoint/create.go index d36971811..646901ccd 100644 --- a/command/checkpoint/create.go +++ b/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/command/checkpoint/list.go b/command/checkpoint/list.go index 7ba035890..fef91a4cc 100644 --- a/command/checkpoint/list.go +++ b/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/command/checkpoint/remove.go b/command/checkpoint/remove.go index 82ce62312..c6ec56df8 100644 --- a/command/checkpoint/remove.go +++ b/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/command/container/start.go b/command/container/start.go index 8693b3a55..8e0654da3 100644 --- a/command/container/start.go +++ b/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 87e916a171eca346506a735d7b604150e5ecbad0 Mon Sep 17 00:00:00 2001 From: Cezar Sa Espinola Date: Thu, 13 Oct 2016 15:28:32 -0300 Subject: [PATCH 164/563] 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 --- command/service/opts.go | 80 ++++++++++++++++++++++++++++++++++ command/service/opts_test.go | 49 +++++++++++++++++++++ command/service/update.go | 50 +++++++++++++++++++++ command/service/update_test.go | 79 +++++++++++++++++++++++++++++++++ 4 files changed, 258 insertions(+) diff --git a/command/service/opts.go b/command/service/opts.go index 5fdc56de0..0c91360c6 100644 --- a/command/service/opts.go +++ b/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/command/service/opts_test.go b/command/service/opts_test.go index 8ef3cacb4..52016cbfc 100644 --- a/command/service/opts_test.go +++ b/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/command/service/update.go b/command/service/update.go index e1f7cad66..356c27a5c 100644 --- a/command/service/update.go +++ b/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/command/service/update_test.go b/command/service/update_test.go index 6e68e977a..731358753 100644 --- a/command/service/update_test.go +++ b/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 908aa5b4087ec5a4a257b9403e62e2850526f7a0 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 28 Oct 2016 11:48:25 -0700 Subject: [PATCH 165/563] 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 --- command/container/stats.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/command/container/stats.go b/command/container/stats.go index e7954e4b9..5e743a483 100644 --- a/command/container/stats.go +++ b/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 378ae7234a303310f3bc1a6b9e814aefad1dad86 Mon Sep 17 00:00:00 2001 From: Lily Guo Date: Wed, 26 Oct 2016 12:46:40 -0700 Subject: [PATCH 166/563] 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 --- command/service/create.go | 1 + command/service/opts.go | 2 +- command/service/update.go | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/command/service/create.go b/command/service/create.go index d2925b42d..28790ec8e 100644 --- a/command/service/create.go +++ b/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/command/service/opts.go b/command/service/opts.go index 0c91360c6..43b7b671c 100644 --- a/command/service/opts.go +++ b/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/command/service/update.go b/command/service/update.go index 356c27a5c..b76a20e97 100644 --- a/command/service/update.go +++ b/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 faac17728583bfd6342603addb0937d86cd430ca Mon Sep 17 00:00:00 2001 From: Qiang Huang Date: Sat, 29 Oct 2016 15:03:26 +0800 Subject: [PATCH 167/563] Fix bunch of typos Signed-off-by: Qiang Huang --- command/registry/logout.go | 2 +- command/swarm/join_token.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/command/registry/logout.go b/command/registry/logout.go index 5d80595ff..a73581804 100644 --- a/command/registry/logout.go +++ b/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/command/swarm/join_token.go b/command/swarm/join_token.go index b41120208..3a17a8020 100644 --- a/command/swarm/join_token.go +++ b/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 6027424adfec69183e04cb6e6cd33652dc9a8fa5 Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Sun, 18 Sep 2016 13:11:02 +0800 Subject: [PATCH 168/563] Modify short and flags for docker inspect Signed-off-by: yuexiao-wang --- command/system/inspect.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/command/system/inspect.go b/command/system/inspect.go index 015c1b5c6..06f1f1abf 100644 --- a/command/system/inspect.go +++ b/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 fdbf29e1fa69decd3676d225fee2879ea9b5e61a Mon Sep 17 00:00:00 2001 From: Boaz Shuster Date: Mon, 31 Oct 2016 14:12:10 +0200 Subject: [PATCH 169/563] 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 --- command/image/load.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/command/image/load.go b/command/image/load.go index 56145a8a3..4f88faf09 100644 --- a/command/image/load.go +++ b/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 120c5f99644441bfef1c2cf2b4ee39f0fdcc842a Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 6 Oct 2016 12:57:17 -0400 Subject: [PATCH 170/563] Generate VolumesCreateRequest from the swagger spec. Signed-off-by: Daniel Nephin --- command/volume/create.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/command/volume/create.go b/command/volume/create.go index fbf62a5ef..f16e650bb 100644 --- a/command/volume/create.go +++ b/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 010023c3c6a594aa9ec62985b19fa1c048c49944 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 14 Oct 2016 16:20:13 -0400 Subject: [PATCH 171/563] Use a config to generate swagger api types Moves the resposne types to a package under api/types Signed-off-by: Daniel Nephin --- command/volume/create.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/volume/create.go b/command/volume/create.go index f16e650bb..7b2a7e331 100644 --- a/command/volume/create.go +++ b/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 e7e083770259c805cc58b059ac2988e855c6559e Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 14 Oct 2016 16:28:47 -0400 Subject: [PATCH 172/563] Generate container create response from swagger spec. Signed-off-by: Daniel Nephin --- command/container/create.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/container/create.go b/command/container/create.go index 7bd385697..7dc644d28 100644 --- a/command/container/create.go +++ b/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 89db77511cce3518ea5f057c4da32c6dd919f816 Mon Sep 17 00:00:00 2001 From: yupeng Date: Tue, 1 Nov 2016 11:07:31 +0800 Subject: [PATCH 173/563] Align with other cli descriptions Signed-off-by: yupeng --- command/registry/login.go | 2 +- command/registry/logout.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/command/registry/login.go b/command/registry/login.go index d6f7f8f1d..7b29cfdb2 100644 --- a/command/registry/login.go +++ b/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/command/registry/logout.go b/command/registry/logout.go index a73581804..8e820dcc8 100644 --- a/command/registry/logout.go +++ b/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 9eceaa926f7111a4aaaeb99ca82bf2b2a83beacb Mon Sep 17 00:00:00 2001 From: Zhang Wei Date: Tue, 1 Nov 2016 22:01:16 +0800 Subject: [PATCH 174/563] 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 --- command/container/list.go | 8 ++++---- command/container/ps_test.go | 4 ++-- command/node/list.go | 2 +- command/node/ps.go | 2 +- command/service/list.go | 4 ++-- command/service/ps.go | 2 +- command/stack/common.go | 2 +- command/stack/list.go | 2 +- command/stack/ps.go | 2 +- command/stack/services.go | 4 ++-- 10 files changed, 16 insertions(+), 16 deletions(-) diff --git a/command/container/list.go b/command/container/list.go index 2d46b6604..80de7c5ff 100644 --- a/command/container/list.go +++ b/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/command/container/ps_test.go b/command/container/ps_test.go index dafdcdf90..9df4dfd5f 100644 --- a/command/container/ps_test.go +++ b/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/command/node/list.go b/command/node/list.go index d028d1914..9cacdcf44 100644 --- a/command/node/list.go +++ b/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/command/node/ps.go b/command/node/ps.go index 607488f35..a034721d2 100644 --- a/command/node/ps.go +++ b/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/command/service/list.go b/command/service/list.go index 2278643fb..4db561879 100644 --- a/command/service/list.go +++ b/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/command/service/ps.go b/command/service/ps.go index 55f837ba8..cf94ad737 100644 --- a/command/service/ps.go +++ b/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/command/stack/common.go b/command/stack/common.go index 3e3a35faa..4776ec1b4 100644 --- a/command/stack/common.go +++ b/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/command/stack/list.go b/command/stack/list.go index 5d87cecb5..f655b929a 100644 --- a/command/stack/list.go +++ b/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/command/stack/ps.go b/command/stack/ps.go index 2fff3de1f..7a5e069cb 100644 --- a/command/stack/ps.go +++ b/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/command/stack/services.go b/command/stack/services.go index 50b50179d..1ca1c8c12 100644 --- a/command/stack/services.go +++ b/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 ac7d79389afeaeb9db3992059af62a75764c0815 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 1 Nov 2016 09:12:27 -0700 Subject: [PATCH 175/563] 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 --- command/registry/login.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/command/registry/login.go b/command/registry/login.go index 7b29cfdb2..93e1b40e3 100644 --- a/command/registry/login.go +++ b/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 b90c048804d3390f746fcceec9d7c43ba6570b74 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Thu, 21 Apr 2016 12:08:37 -0400 Subject: [PATCH 176/563] 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 --- command/image/build.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/command/image/build.go b/command/image/build.go index 7db76a649..dc1860190 100644 --- a/command/image/build.go +++ b/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 3acdab83fb9f666b46777783f393657368101c58 Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Wed, 2 Nov 2016 03:11:38 +0800 Subject: [PATCH 177/563] Remove some redundant consts Signed-off-by: yuexiao-wang --- command/swarm/init.go | 9 +-------- command/swarm/opts.go | 2 +- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/command/swarm/init.go b/command/swarm/init.go index 60fb8e8fe..16f372f8d 100644 --- a/command/swarm/init.go +++ b/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/command/swarm/opts.go b/command/swarm/opts.go index 58330b7f8..3659b55f8 100644 --- a/command/swarm/opts.go +++ b/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 39e34ed1a39d491096b5dc3e29aa760e7f442bcb Mon Sep 17 00:00:00 2001 From: allencloud Date: Wed, 2 Nov 2016 15:53:18 +0800 Subject: [PATCH 178/563] add replicated in service scale command description Signed-off-by: allencloud --- command/service/scale.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/service/scale.go b/command/service/scale.go index 61b73bc35..ea30265bd 100644 --- a/command/service/scale.go +++ b/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 503053819eacd0c4ee7642ddb50561f40a8cbb49 Mon Sep 17 00:00:00 2001 From: allencloud Date: Wed, 2 Nov 2016 17:22:04 +0800 Subject: [PATCH 179/563] node rm can be applied on not only active node Signed-off-by: allencloud --- command/node/remove.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/node/remove.go b/command/node/remove.go index 9ba21b44a..3b89db866 100644 --- a/command/node/remove.go +++ b/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 557db1ea688f0f5a0ac0722070f3bc414b856a51 Mon Sep 17 00:00:00 2001 From: Antonio Murdaca Date: Fri, 2 Sep 2016 15:20:54 +0200 Subject: [PATCH 180/563] daemon: add a flag to override the default seccomp profile Signed-off-by: Antonio Murdaca --- command/system/info.go | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/command/system/info.go b/command/system/info.go index 0bfd9986d..dfbc83d90 100644 --- a/command/system/info.go +++ b/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 07f77b78eaf1474057142f4c9e63de04a57b9ab0 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Thu, 3 Nov 2016 07:20:46 +0100 Subject: [PATCH 181/563] 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 --- command/container/stats_helpers.go | 10 ++++++---- command/formatter/stats.go | 21 +++++++++++++++++---- command/formatter/stats_test.go | 10 +++++----- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/command/container/stats_helpers.go b/command/container/stats_helpers.go index 32ad84841..8bc537ad3 100644 --- a/command/container/stats_helpers.go +++ b/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/command/formatter/stats.go b/command/formatter/stats.go index b2c972251..7997f996d 100644 --- a/command/formatter/stats.go +++ b/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/command/formatter/stats_test.go b/command/formatter/stats_test.go index f1f449e71..d5a17cc70 100644 --- a/command/formatter/stats_test.go +++ b/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 ec7d9291b82d523533ad279fc1bbb4b463ed0e53 Mon Sep 17 00:00:00 2001 From: milindchawre Date: Tue, 1 Nov 2016 13:09:18 +0000 Subject: [PATCH 182/563] Fixes #27798 : Update help for --blkio-weight parameter Signed-off-by: milindchawre --- command/container/update.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/container/update.go b/command/container/update.go index 5bacc9be7..75765856c 100644 --- a/command/container/update.go +++ b/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 51cb4aa7b8fea5216e2dd5b8771a465ede42ac15 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Tue, 25 Oct 2016 03:26:54 +0000 Subject: [PATCH 183/563] cli: add `--mount` to `docker run` Signed-off-by: Akihiro Suda --- command/service/create.go | 2 +- command/service/opts.go | 141 +-------------------------------- command/service/opts_test.go | 146 ----------------------------------- command/service/update.go | 2 +- 4 files changed, 3 insertions(+), 288 deletions(-) diff --git a/command/service/create.go b/command/service/create.go index 28790ec8e..92cf969b4 100644 --- a/command/service/create.go +++ b/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/command/service/opts.go b/command/service/opts.go index 43b7b671c..358185c0b 100644 --- a/command/service/opts.go +++ b/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/command/service/opts_test.go b/command/service/opts_test.go index 52016cbfc..26534cf0f 100644 --- a/command/service/opts_test.go +++ b/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/command/service/update.go b/command/service/update.go index b76a20e97..a9aa9c998 100644 --- a/command/service/update.go +++ b/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 cd46f069343b4514f98936410a8d95121f395cef Mon Sep 17 00:00:00 2001 From: yupeng Date: Thu, 3 Nov 2016 15:47:58 +0800 Subject: [PATCH 184/563] Add for String Signed-off-by: yupeng --- flags/common.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flags/common.go b/flags/common.go index f40808ca0..690e8da4b 100644 --- a/flags/common.go +++ b/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 19b7bc17395a954841532206708cf319b5f9fbc0 Mon Sep 17 00:00:00 2001 From: Nikolay Milovanov Date: Thu, 27 Oct 2016 12:44:19 +0100 Subject: [PATCH 185/563] Adding the hostname option to docker service command Signed-off-by: Nikolay Milovanov --- command/service/create.go | 1 + command/service/opts.go | 3 +++ 2 files changed, 4 insertions(+) diff --git a/command/service/create.go b/command/service/create.go index 28790ec8e..430d44856 100644 --- a/command/service/create.go +++ b/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/command/service/opts.go b/command/service/opts.go index 43b7b671c..a13432735 100644 --- a/command/service/opts.go +++ b/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 816560ffe907a5af5a91e6e21077b6ef2118cc53 Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Tue, 1 Nov 2016 22:58:26 +0800 Subject: [PATCH 186/563] Update for docker volume create Signed-off-by: yuexiao-wang --- command/volume/cmd.go | 4 ++-- command/volume/list.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/command/volume/cmd.go b/command/volume/cmd.go index 5f39d3cf3..f35181ffa 100644 --- a/command/volume/cmd.go +++ b/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/command/volume/list.go b/command/volume/list.go index 77ce35977..d76006a1b 100644 --- a/command/volume/list.go +++ b/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 2cd40280245b3afe60b472253baeefd2004c2f3e Mon Sep 17 00:00:00 2001 From: Drew Erny Date: Mon, 24 Oct 2016 16:11:25 -0700 Subject: [PATCH 187/563] 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 --- command/node/inspect.go | 1 + 1 file changed, 1 insertion(+) diff --git a/command/node/inspect.go b/command/node/inspect.go index 0812ec5ea..fde70185f 100644 --- a/command/node/inspect.go +++ b/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 3014d36cd91d85c741b921926de9b8eb27e75d7d Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Thu, 3 Nov 2016 10:33:17 -0700 Subject: [PATCH 188/563] fix double [y/N] in container prune Signed-off-by: Victor Vieux --- command/container/prune.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/container/prune.go b/command/container/prune.go index 679471398..99a97f6cd 100644 --- a/command/container/prune.go +++ b/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 0e6f4e7cdaa1881faea242a6f85e7bc330c3ac4f Mon Sep 17 00:00:00 2001 From: Vincent Bernat Date: Thu, 3 Nov 2016 20:46:28 +0100 Subject: [PATCH 189/563] 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 --- command/system/inspect.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/command/system/inspect.go b/command/system/inspect.go index 8732c467e..a403685ee 100644 --- a/command/system/inspect.go +++ b/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 eb522dac241ce3b12af9293e7bfbfd10d65f2346 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Wed, 2 Nov 2016 17:43:32 -0700 Subject: [PATCH 190/563] always add but hide experimental cmds and flags Signed-off-by: Victor Vieux --- command/cli.go | 2 +- command/commands/commands.go | 13 ++++--------- command/container/start.go | 9 ++++----- command/image/build.go | 5 ++--- 4 files changed, 11 insertions(+), 18 deletions(-) diff --git a/command/cli.go b/command/cli.go index be82ecf6f..9b6149244 100644 --- a/command/cli.go +++ b/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/command/commands/commands.go b/command/commands/commands.go index 425f90ba7..fad709bca 100644 --- a/command/commands/commands.go +++ b/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/command/container/start.go b/command/container/start.go index 8e0654da3..e54402893 100644 --- a/command/container/start.go +++ b/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/command/image/build.go b/command/image/build.go index dc1860190..5cf36cfd5 100644 --- a/command/image/build.go +++ b/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 b8cf9a880eb1d66bc07ebc03557a02f4c1d32c1b Mon Sep 17 00:00:00 2001 From: Kunal Kushwaha Date: Wed, 24 Aug 2016 17:30:54 +0900 Subject: [PATCH 191/563] 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 --- command/service/update.go | 45 ++++++++++++++++++++++++++++++---- command/service/update_test.go | 28 +++++++++++++++++---- 2 files changed, 63 insertions(+), 10 deletions(-) diff --git a/command/service/update.go b/command/service/update.go index a9aa9c998..34cc9bc3d 100644 --- a/command/service/update.go +++ b/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/command/service/update_test.go b/command/service/update_test.go index 731358753..2123d1b79 100644 --- a/command/service/update_test.go +++ b/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 1cab3b32a68c0493843a0196fd50029553b160b6 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Tue, 1 Nov 2016 10:12:29 -0700 Subject: [PATCH 192/563] 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 --- command/service/opts.go | 32 ++------------------------------ command/service/opts_test.go | 5 +++-- 2 files changed, 5 insertions(+), 32 deletions(-) diff --git a/command/service/opts.go b/command/service/opts.go index c89d40a76..2199e9f36 100644 --- a/command/service/opts.go +++ b/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/command/service/opts_test.go b/command/service/opts_test.go index 26534cf0f..aa2d999dc 100644 --- a/command/service/opts_test.go +++ b/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 b338ab7c41955c8109711c2e31b9f94f67e9284c Mon Sep 17 00:00:00 2001 From: Antonio Murdaca Date: Fri, 4 Nov 2016 17:16:44 +0100 Subject: [PATCH 193/563] cli/info: fix seccomp warning also reword seccomp warning around default seccomp profile Signed-off-by: Antonio Murdaca --- command/system/info.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/command/system/info.go b/command/system/info.go index dfbc83d90..7ab658c13 100644 --- a/command/system/info.go +++ b/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 1e10649f55dbe8a8cd3a91ffae624c9eab067567 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Thu, 3 Nov 2016 17:12:15 -0700 Subject: [PATCH 194/563] update cobra and use Tags Signed-off-by: Victor Vieux --- command/checkpoint/cmd.go | 1 + command/cli.go | 26 ++++++++++---------------- command/container/start.go | 2 +- command/plugin/cmd.go | 1 + command/stack/cmd.go | 1 + command/stack/deploy.go | 1 + 6 files changed, 15 insertions(+), 17 deletions(-) diff --git a/command/checkpoint/cmd.go b/command/checkpoint/cmd.go index 84084ab71..7f9e53777 100644 --- a/command/checkpoint/cmd.go +++ b/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/command/cli.go b/command/cli.go index 9b6149244..33a26c4c6 100644 --- a/command/cli.go +++ b/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/command/container/start.go b/command/container/start.go index e54402893..87e815fed 100644 --- a/command/container/start.go +++ b/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/command/plugin/cmd.go b/command/plugin/cmd.go index 80fa61cb1..c78f43a8d 100644 --- a/command/plugin/cmd.go +++ b/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/command/stack/cmd.go b/command/stack/cmd.go index 49fcedf20..70afec9c6 100644 --- a/command/stack/cmd.go +++ b/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/command/stack/deploy.go b/command/stack/deploy.go index fcf55fb7d..b0f6b455a 100644 --- a/command/stack/deploy.go +++ b/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 21096cfc05294f7e34a37c25112b699f564999e5 Mon Sep 17 00:00:00 2001 From: Josh Horwitz Date: Thu, 3 Nov 2016 09:58:45 -0400 Subject: [PATCH 195/563] Add -a option to service/node ps Signed-off-by: Josh Horwitz --- command/node/ps.go | 7 +++++++ command/service/ps.go | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/command/node/ps.go b/command/node/ps.go index a034721d2..8591f0466 100644 --- a/command/node/ps.go +++ b/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/command/service/ps.go b/command/service/ps.go index cf94ad737..0028507c2 100644 --- a/command/service/ps.go +++ b/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 1491ae50e0656f9354a29e74a125549b1c826276 Mon Sep 17 00:00:00 2001 From: Alicia Lauerman Date: Thu, 3 Nov 2016 14:20:53 -0400 Subject: [PATCH 196/563] remove COMMAND column from service ls output. closes #27994 Signed-off-by: Alicia Lauerman --- command/service/list.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/command/service/list.go b/command/service/list.go index 4db561879..05b425a45 100644 --- a/command/service/list.go +++ b/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 713c7cd81ed199064ccc1c6226ddabeae8d739cf Mon Sep 17 00:00:00 2001 From: WangPing Date: Mon, 7 Nov 2016 16:17:04 +0800 Subject: [PATCH 197/563] modify to improve code readability Signed-off-by: WangPing align Signed-off-by: WangPing align Signed-off-by: WangPing --- command/image/build.go | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/command/image/build.go b/command/image/build.go index 5cf36cfd5..604888b6f 100644 --- a/command/image/build.go +++ b/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 3baa727ed100a46c27f159a4430508b0a4a1b02c Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Fri, 4 Nov 2016 11:31:44 -0700 Subject: [PATCH 198/563] 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 --- command/service/opts.go | 5 +++++ command/service/update.go | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/command/service/opts.go b/command/service/opts.go index 2199e9f36..989fd18b8 100644 --- a/command/service/opts.go +++ b/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/command/service/update.go b/command/service/update.go index 34cc9bc3d..c278ac1ba 100644 --- a/command/service/update.go +++ b/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 089b33edd8d297b2186f888967d5e6b45354a0a8 Mon Sep 17 00:00:00 2001 From: John Howard Date: Wed, 2 Nov 2016 10:04:39 -0700 Subject: [PATCH 199/563] Adds minimum API version to version Signed-off-by: John Howard --- command/system/version.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/command/system/version.go b/command/system/version.go index 0b484cb3b..6040c7936 100644 --- a/command/system/version.go +++ b/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 41513e30517b303e3f0b14ece4ac7ede273d5663 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Mon, 7 Nov 2016 17:43:11 -0800 Subject: [PATCH 200/563] support settings in docker plugins install Signed-off-by: Victor Vieux --- command/plugin/install.go | 9 +++++++-- command/plugin/set.go | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/command/plugin/install.go b/command/plugin/install.go index 3989a35ce..eae018367 100644 --- a/command/plugin/install.go +++ b/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/command/plugin/set.go b/command/plugin/set.go index e58ea63bc..5660523ed 100644 --- a/command/plugin/set.go +++ b/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 7113bbf2c6ac94b1f51e7f501512e4fb9273dc11 Mon Sep 17 00:00:00 2001 From: yupeng Date: Tue, 8 Nov 2016 14:51:17 +0800 Subject: [PATCH 201/563] context.Context should be the first parameter of a function Signed-off-by: yupeng --- command/container/start.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/command/container/start.go b/command/container/start.go index 87e815fed..77bb9ddb9 100644 --- a/command/container/start.go +++ b/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 7891d349b377b5fdc0869bf6f24828e46bb4b9ac Mon Sep 17 00:00:00 2001 From: allencloud Date: Tue, 25 Oct 2016 11:39:53 +0800 Subject: [PATCH 202/563] support show numbers of global service in service ls command Signed-off-by: allencloud --- command/service/list.go | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/command/service/list.go b/command/service/list.go index 05b425a45..f758808d1 100644 --- a/command/service/list.go +++ b/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 cd2269a456bbdbf9276493738b104f1c5d9075e3 Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Tue, 8 Nov 2016 16:15:09 +0800 Subject: [PATCH 203/563] Update for docker checkpoint Signed-off-by: yuexiao-wang --- command/checkpoint/create.go | 6 +++--- command/checkpoint/list.go | 4 ++-- command/checkpoint/remove.go | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/command/checkpoint/create.go b/command/checkpoint/create.go index 646901ccd..2377b5e2e 100644 --- a/command/checkpoint/create.go +++ b/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/command/checkpoint/list.go b/command/checkpoint/list.go index fef91a4cc..daf834999 100644 --- a/command/checkpoint/list.go +++ b/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/command/checkpoint/remove.go b/command/checkpoint/remove.go index c6ec56df8..ec39fa7b5 100644 --- a/command/checkpoint/remove.go +++ b/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 4ae7176ffb8d37c60d2152fb155678e659af5b99 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Wed, 2 Nov 2016 17:43:32 -0700 Subject: [PATCH 204/563] 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 --- command/checkpoint/cmd.go | 7 +++--- command/cli.go | 44 +++++++++++++++++++++++++++----------- command/container/cmd.go | 5 ++--- command/container/exec.go | 1 + command/container/prune.go | 1 + command/image/build.go | 3 ++- command/image/cmd.go | 6 ++---- command/image/prune.go | 1 + command/network/cmd.go | 5 ++--- command/network/prune.go | 1 + command/node/cmd.go | 5 ++--- command/plugin/cmd.go | 5 ++--- command/service/cmd.go | 5 ++--- command/stack/cmd.go | 7 +++--- command/stack/deploy.go | 2 +- command/swarm/cmd.go | 5 ++--- command/system/cmd.go | 6 +++--- command/system/df.go | 1 + command/system/prune.go | 1 + command/system/version.go | 8 ++++++- command/volume/cmd.go | 5 ++--- command/volume/prune.go | 1 + command/volume/remove.go | 2 +- 23 files changed, 75 insertions(+), 52 deletions(-) diff --git a/command/checkpoint/cmd.go b/command/checkpoint/cmd.go index 7f9e53777..f186232a4 100644 --- a/command/checkpoint/cmd.go +++ b/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/command/cli.go b/command/cli.go index 33a26c4c6..ef9de2edf 100644 --- a/command/cli.go +++ b/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/command/container/cmd.go b/command/container/cmd.go index f06b863b5..075f936bd 100644 --- a/command/container/cmd.go +++ b/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/command/container/exec.go b/command/container/exec.go index 48964693b..84eba113c 100644 --- a/command/container/exec.go +++ b/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/command/container/prune.go b/command/container/prune.go index 99a97f6cd..ec6b0e314 100644 --- a/command/container/prune.go +++ b/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/command/image/build.go b/command/image/build.go index 604888b6f..ebec87d64 100644 --- a/command/image/build.go +++ b/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/command/image/cmd.go b/command/image/cmd.go index 6f8e7b7d4..dc9825743 100644 --- a/command/image/cmd.go +++ b/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/command/image/prune.go b/command/image/prune.go index 46bd56cb1..ea84cda87 100644 --- a/command/image/prune.go +++ b/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/command/network/cmd.go b/command/network/cmd.go index 77c8e4908..c2a7e83dd 100644 --- a/command/network/cmd.go +++ b/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/command/network/prune.go b/command/network/prune.go index 00e05d3bd..f2f8cc20c 100644 --- a/command/network/prune.go +++ b/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/command/node/cmd.go b/command/node/cmd.go index c7d0cf818..d70ee8178 100644 --- a/command/node/cmd.go +++ b/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/command/plugin/cmd.go b/command/plugin/cmd.go index c78f43a8d..03d01c888 100644 --- a/command/plugin/cmd.go +++ b/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/command/service/cmd.go b/command/service/cmd.go index 9f342e134..f4f7d00f9 100644 --- a/command/service/cmd.go +++ b/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/command/stack/cmd.go b/command/stack/cmd.go index 70afec9c6..418950440 100644 --- a/command/stack/cmd.go +++ b/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/command/stack/deploy.go b/command/stack/deploy.go index b0f6b455a..435a9193b 100644 --- a/command/stack/deploy.go +++ b/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/command/swarm/cmd.go b/command/swarm/cmd.go index 9f9df5395..f0a6bcdeb 100644 --- a/command/swarm/cmd.go +++ b/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/command/system/cmd.go b/command/system/cmd.go index 46caa2491..9cd74b5d4 100644 --- a/command/system/cmd.go +++ b/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/command/system/df.go b/command/system/df.go index 293946c18..9f712484a 100644 --- a/command/system/df.go +++ b/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/command/system/prune.go b/command/system/prune.go index c79bc6910..92dddbdca 100644 --- a/command/system/prune.go +++ b/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/command/system/version.go b/command/system/version.go index 6040c7936..00a84a3cb 100644 --- a/command/system/version.go +++ b/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/command/volume/cmd.go b/command/volume/cmd.go index f35181ffa..39e4b7f46 100644 --- a/command/volume/cmd.go +++ b/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/command/volume/prune.go b/command/volume/prune.go index a4bb0092d..ac9c94451 100644 --- a/command/volume/prune.go +++ b/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/command/volume/remove.go b/command/volume/remove.go index 213ad26ab..f464bb3e1 100644 --- a/command/volume/remove.go +++ b/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 af8ebf69db3e5f80dbf8c17a79ee9503589365d9 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Mon, 7 Nov 2016 18:40:47 -0800 Subject: [PATCH 205/563] 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 --- command/service/update.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/command/service/update.go b/command/service/update.go index c278ac1ba..32a23f23f 100644 --- a/command/service/update.go +++ b/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 2af34ea285138573d793e3a18fe9dad43b31f69f Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Wed, 2 Nov 2016 12:29:51 -0700 Subject: [PATCH 206/563] 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 --- command/swarm/opts.go | 21 ++++++++++++++++++--- command/swarm/update.go | 33 +-------------------------------- command/system/info.go | 3 +++ 3 files changed, 22 insertions(+), 35 deletions(-) diff --git a/command/swarm/opts.go b/command/swarm/opts.go index 3659b55f8..af36a7167 100644 --- a/command/swarm/opts.go +++ b/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/command/swarm/update.go b/command/swarm/update.go index 71451e450..a39f34c88 100644 --- a/command/swarm/update.go +++ b/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/command/system/info.go b/command/system/info.go index 7ab658c13..5ea23ed43 100644 --- a/command/system/info.go +++ b/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 f40b12d0f73b7ecc670a3266344086dcda71742d Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 19 Oct 2016 17:07:44 -0700 Subject: [PATCH 207/563] 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 --- command/service/create.go | 3 +++ command/service/opts.go | 36 +++++++++++++++++++++++++----------- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/command/service/create.go b/command/service/create.go index e2c4c4d11..6aca4635a 100644 --- a/command/service/create.go +++ b/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/command/service/opts.go b/command/service/opts.go index 989fd18b8..5f1ca8634 100644 --- a/command/service/opts.go +++ b/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 49e528e18ac518c32c25dd67aa6c6f65f8e9a971 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 26 Oct 2016 20:05:39 -0700 Subject: [PATCH 208/563] 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 --- command/service/opts.go | 8 +++- command/service/update.go | 81 ++++++++++++++++++++++++++++++++++ command/service/update_test.go | 46 +++++++++++++++++++ 3 files changed, 134 insertions(+), 1 deletion(-) diff --git a/command/service/opts.go b/command/service/opts.go index 5f1ca8634..7a5db67b7 100644 --- a/command/service/opts.go +++ b/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/command/service/update.go b/command/service/update.go index 32a23f23f..d3088720a 100644 --- a/command/service/update.go +++ b/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/command/service/update_test.go b/command/service/update_test.go index 2123d1b79..91829b861 100644 --- a/command/service/update_test.go +++ b/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 5834d378e020b37c605b9da2014d1f22e3101fca Mon Sep 17 00:00:00 2001 From: Andrea Luzzardi Date: Fri, 4 Nov 2016 19:23:07 -0700 Subject: [PATCH 209/563] 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 --- command/idresolver/idresolver.go | 22 +++++++++++++++++++++- command/task/print.go | 24 +++++------------------- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/command/idresolver/idresolver.go b/command/idresolver/idresolver.go index ad0d96735..511b1a8f5 100644 --- a/command/idresolver/idresolver.go +++ b/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/command/task/print.go b/command/task/print.go index b3cdcbe53..45af178a4 100644 --- a/command/task/print.go +++ b/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 31c5b957e274222c037ccbf42c80e49c624104dc Mon Sep 17 00:00:00 2001 From: allencloud Date: Wed, 9 Nov 2016 14:22:06 +0800 Subject: [PATCH 210/563] add short flag for force Signed-off-by: allencloud --- command/node/remove.go | 2 +- command/swarm/leave.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/command/node/remove.go b/command/node/remove.go index 3b89db866..19b4a9663 100644 --- a/command/node/remove.go +++ b/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/command/swarm/leave.go b/command/swarm/leave.go index ae1388415..1ffaa3fcc 100644 --- a/command/swarm/leave.go +++ b/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 18caa28b669871adb11dff0f6914d3094d2d2c4e Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Wed, 9 Nov 2016 17:43:10 +0800 Subject: [PATCH 211/563] Update function name for TestCalculBlockIO Signed-off-by: yuexiao-wang --- command/container/stats_unit_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/container/stats_unit_test.go b/command/container/stats_unit_test.go index fc6563c4d..828d634c8 100644 --- a/command/container/stats_unit_test.go +++ b/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 e87262cc2dff9affb58ab984dc1ab7af7d615072 Mon Sep 17 00:00:00 2001 From: milindchawre Date: Tue, 25 Oct 2016 12:22:07 +0000 Subject: [PATCH 212/563] Fixes #24083 : Improving cli help for flags with duration option Signed-off-by: milindchawre --- command/service/opts.go | 4 ++-- command/swarm/opts.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/command/service/opts.go b/command/service/opts.go index 7a5db67b7..8de8b173e 100644 --- a/command/service/opts.go +++ b/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/command/swarm/opts.go b/command/swarm/opts.go index af36a7167..ce5a9b1de 100644 --- a/command/swarm/opts.go +++ b/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 071c746e5ef5e407d6769a0fd436e8cf50b371a9 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Tue, 8 Nov 2016 07:06:07 -0800 Subject: [PATCH 213/563] 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 --- command/service/create.go | 6 ++--- command/service/opts.go | 53 ++++++++++++++++++++++++++++----------- command/service/update.go | 34 +++++++++++++++---------- 3 files changed, 62 insertions(+), 31 deletions(-) diff --git a/command/service/create.go b/command/service/create.go index 6aca4635a..d6c3ebdb9 100644 --- a/command/service/create.go +++ b/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/command/service/opts.go b/command/service/opts.go index 8de8b173e..827c4e5cd 100644 --- a/command/service/opts.go +++ b/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/command/service/update.go b/command/service/update.go index d3088720a..4a7722949 100644 --- a/command/service/update.go +++ b/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 801167fcecd2d8cdeb94ee2ab7444c4bf71268eb Mon Sep 17 00:00:00 2001 From: Kenfe-Mickael Laventure Date: Mon, 24 Oct 2016 15:18:58 -0700 Subject: [PATCH 214/563] Add expected 3rd party binaries commit ids to info Signed-off-by: Kenfe-Mickael Laventure --- command/system/info.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/command/system/info.go b/command/system/info.go index 5ea23ed43..fceef5923 100644 --- a/command/system/info.go +++ b/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 1be644fbcf68872433e56914e6c4357920d084ca Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Wed, 19 Oct 2016 12:22:02 -0400 Subject: [PATCH 215/563] 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 --- command/commands/commands.go | 2 + command/secret/cmd.go | 29 ++++++++++++ command/secret/create.go | 57 ++++++++++++++++++++++ command/secret/inspect.go | 42 ++++++++++++++++ command/secret/ls.go | 62 ++++++++++++++++++++++++ command/secret/remove.go | 43 +++++++++++++++++ command/service/create.go | 7 +++ command/service/opts.go | 17 +++++++ command/service/parse.go | 92 ++++++++++++++++++++++++++++++++++++ 9 files changed, 351 insertions(+) create mode 100644 command/secret/cmd.go create mode 100644 command/secret/create.go create mode 100644 command/secret/inspect.go create mode 100644 command/secret/ls.go create mode 100644 command/secret/remove.go create mode 100644 command/service/parse.go diff --git a/command/commands/commands.go b/command/commands/commands.go index fad709bca..d64d5680c 100644 --- a/command/commands/commands.go +++ b/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/command/secret/cmd.go b/command/secret/cmd.go new file mode 100644 index 000000000..995300ad7 --- /dev/null +++ b/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/command/secret/create.go b/command/secret/create.go new file mode 100644 index 000000000..1c0e933f5 --- /dev/null +++ b/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/command/secret/inspect.go b/command/secret/inspect.go new file mode 100644 index 000000000..c8d5cd8f7 --- /dev/null +++ b/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/command/secret/ls.go b/command/secret/ls.go new file mode 100644 index 000000000..1befdad9d --- /dev/null +++ b/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/command/secret/remove.go b/command/secret/remove.go new file mode 100644 index 000000000..f336c6161 --- /dev/null +++ b/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/command/service/create.go b/command/service/create.go index d6c3ebdb9..8fb9070e6 100644 --- a/command/service/create.go +++ b/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/command/service/opts.go b/command/service/opts.go index 827c4e5cd..a4fd08881 100644 --- a/command/service/opts.go +++ b/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/command/service/parse.go b/command/service/parse.go new file mode 100644 index 000000000..41883fb44 --- /dev/null +++ b/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 3f9494f1d64c9ffe04c17ec976f5a2620c4e6ba9 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Wed, 26 Oct 2016 13:30:53 -0700 Subject: [PATCH 216/563] 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 --- command/service/parse.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/command/service/parse.go b/command/service/parse.go index 41883fb44..596d8e50d 100644 --- a/command/service/parse.go +++ b/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 4e8f1a7dd9926802a6bd9c292ea4bc3d2339a9eb Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 27 Oct 2016 00:41:32 -0700 Subject: [PATCH 217/563] 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 --- command/service/opts.go | 15 +-------------- command/service/parse.go | 12 +++++------- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/command/service/opts.go b/command/service/opts.go index a4fd08881..3ef24ee33 100644 --- a/command/service/opts.go +++ b/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/command/service/parse.go b/command/service/parse.go index 596d8e50d..f3061660a 100644 --- a/command/service/parse.go +++ b/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 8554b64b99bdc58c693d2ffe79dc2bfeb910bbd8 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 27 Oct 2016 15:51:02 -0700 Subject: [PATCH 218/563] add secret support for service update - add nosuid and noexec to tmpfs Signed-off-by: Evan Hazlett --- command/service/opts.go | 2 ++ command/service/update.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/command/service/opts.go b/command/service/opts.go index 3ef24ee33..37da5d114 100644 --- a/command/service/opts.go +++ b/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/command/service/update.go b/command/service/update.go index 4a7722949..a9f5ac9be 100644 --- a/command/service/update.go +++ b/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 ab5f829742a58c6d9f3b168e6724f351f788d6f3 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 27 Oct 2016 17:18:12 -0700 Subject: [PATCH 219/563] support the same secret with different targets on service create Signed-off-by: Evan Hazlett --- command/service/parse.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/command/service/parse.go b/command/service/parse.go index f3061660a..1a8e56b8c 100644 --- a/command/service/parse.go +++ b/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 6bbc35a7434744c70646ba5bc1f686f4faf9476c Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 27 Oct 2016 17:57:38 -0700 Subject: [PATCH 220/563] simplify secret lookup on service create Signed-off-by: Evan Hazlett --- command/service/parse.go | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/command/service/parse.go b/command/service/parse.go index 1a8e56b8c..71d6fb195 100644 --- a/command/service/parse.go +++ b/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 2b0fa52c0905f231edf71f3e097c1d8395ba0f60 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Tue, 1 Nov 2016 18:11:43 -0400 Subject: [PATCH 221/563] update to support new target in swarmkit Signed-off-by: Evan Hazlett --- command/service/parse.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/command/service/parse.go b/command/service/parse.go index 71d6fb195..5a22ed352 100644 --- a/command/service/parse.go +++ b/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 15b97a39d7668569a3bb5c018f593ad1bcf42b77 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Tue, 1 Nov 2016 22:28:32 -0400 Subject: [PATCH 222/563] secrets: use explicit format when using secrets Signed-off-by: Evan Hazlett --- command/service/create.go | 3 +- command/service/opts.go | 97 +++++++++++++++++++++++++++++++++++- command/service/opts_test.go | 45 +++++++++++++++++ command/service/parse.go | 54 ++++---------------- command/service/update.go | 7 +-- 5 files changed, 154 insertions(+), 52 deletions(-) diff --git a/command/service/create.go b/command/service/create.go index 8fb9070e6..e5b728d3e 100644 --- a/command/service/create.go +++ b/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/command/service/opts.go b/command/service/opts.go index 37da5d114..00cdecb67 100644 --- a/command/service/opts.go +++ b/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/command/service/opts_test.go b/command/service/opts_test.go index aa2d999dc..551dfc239 100644 --- a/command/service/opts_test.go +++ b/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/command/service/parse.go b/command/service/parse.go index 5a22ed352..73fa8a0cb 100644 --- a/command/service/parse.go +++ b/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/command/service/update.go b/command/service/update.go index a9f5ac9be..37f709e23 100644 --- a/command/service/update.go +++ b/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 d22e1a91f628d5acea0e3db54d23ed99832fffbf Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Tue, 1 Nov 2016 23:32:21 -0400 Subject: [PATCH 223/563] secrets: enable secret inspect and rm by secret name Signed-off-by: Evan Hazlett --- command/secret/inspect.go | 20 +++++++++++++++++--- command/secret/remove.go | 25 ++++++++++++++++++++++++- command/secret/utils.go | 21 +++++++++++++++++++++ 3 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 command/secret/utils.go diff --git a/command/secret/inspect.go b/command/secret/inspect.go index c8d5cd8f7..c5b0aa6a3 100644 --- a/command/secret/inspect.go +++ b/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/command/secret/remove.go b/command/secret/remove.go index f336c6161..9396b9b17 100644 --- a/command/secret/remove.go +++ b/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/command/secret/utils.go b/command/secret/utils.go new file mode 100644 index 000000000..40aa4a6d7 --- /dev/null +++ b/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 91c08eab93ac0158842313a1b3a5ec9ca58d493c Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 3 Nov 2016 11:08:22 -0400 Subject: [PATCH 224/563] move secretopt to opts pkg Signed-off-by: Evan Hazlett --- command/service/opts.go | 2 +- command/service/parse.go | 18 +++++++++--------- command/service/update.go | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/command/service/opts.go b/command/service/opts.go index 00cdecb67..45adb3767 100644 --- a/command/service/opts.go +++ b/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/command/service/parse.go b/command/service/parse.go index 73fa8a0cb..cbf2745dc 100644 --- a/command/service/parse.go +++ b/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/command/service/update.go b/command/service/update.go index 37f709e23..1bc72a8f1 100644 --- a/command/service/update.go +++ b/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 90743339570aea9001339aea628001387d8c1afe Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 3 Nov 2016 14:09:13 -0400 Subject: [PATCH 225/563] 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 --- command/secret/inspect.go | 2 +- command/secret/remove.go | 2 +- command/secret/utils.go | 4 ++-- command/service/opts_test.go | 34 +++++++++++++++++----------------- command/service/parse.go | 4 ++-- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/command/secret/inspect.go b/command/secret/inspect.go index c5b0aa6a3..25da79f16 100644 --- a/command/secret/inspect.go +++ b/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/command/secret/remove.go b/command/secret/remove.go index 9396b9b17..d277eceba 100644 --- a/command/secret/remove.go +++ b/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/command/secret/utils.go b/command/secret/utils.go index 40aa4a6d7..d1a7d97c4 100644 --- a/command/secret/utils.go +++ b/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/command/service/opts_test.go b/command/service/opts_test.go index 551dfc239..3df3a4fd5 100644 --- a/command/service/opts_test.go +++ b/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/command/service/parse.go b/command/service/parse.go index cbf2745dc..4728c773c 100644 --- a/command/service/parse.go +++ b/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 b3bbcc1ba62af66ec33932f4d94fead0f04a5dd4 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 3 Nov 2016 15:56:05 -0400 Subject: [PATCH 226/563] secrets: support simple syntax --secret foo Signed-off-by: Evan Hazlett --- command/service/opts_test.go | 46 ------------------------------------ 1 file changed, 46 deletions(-) diff --git a/command/service/opts_test.go b/command/service/opts_test.go index 3df3a4fd5..85de3ae88 100644 --- a/command/service/opts_test.go +++ b/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 0bda23ec2b53d4226c6fb5aab4c793f7bd6dfea7 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Thu, 3 Nov 2016 17:01:54 -0400 Subject: [PATCH 227/563] support labels for secrets upon creation; review updates Signed-off-by: Evan Hazlett --- command/secret/create.go | 29 +++++++++++++++++++---------- command/service/parse.go | 2 +- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/command/secret/create.go b/command/secret/create.go index 1c0e933f5..980004834 100644 --- a/command/secret/create.go +++ b/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/command/service/parse.go b/command/service/parse.go index 4728c773c..0e3a229f4 100644 --- a/command/service/parse.go +++ b/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 0dc91150065e4219a45edb3e713f50a644510182 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Fri, 4 Nov 2016 14:24:44 -0400 Subject: [PATCH 228/563] SecretRequestOptions -> SecretRequestOption Signed-off-by: Evan Hazlett --- command/service/parse.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/service/parse.go b/command/service/parse.go index 0e3a229f4..368bc6d44 100644 --- a/command/service/parse.go +++ b/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 c7d7b50003d23f6828cd5b5c8a6985bff8f123f8 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Tue, 8 Nov 2016 11:34:45 -0500 Subject: [PATCH 229/563] more review updates - return err instead of wrap for update secret - add omitempty for data in secret spec Signed-off-by: Evan Hazlett --- command/service/opts.go | 1 - command/service/opts_test.go | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/command/service/opts.go b/command/service/opts.go index 45adb3767..b81998ec0 100644 --- a/command/service/opts.go +++ b/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/command/service/opts_test.go b/command/service/opts_test.go index 85de3ae88..aa2d999dc 100644 --- a/command/service/opts_test.go +++ b/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 06666a5a23dcc004eb19af7f4b6028f481f74ab5 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Tue, 8 Nov 2016 22:40:46 -0500 Subject: [PATCH 230/563] use human readable units when listing secrets Signed-off-by: Evan Hazlett --- command/secret/ls.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/command/secret/ls.go b/command/secret/ls.go index 1befdad9d..67fc1daff 100644 --- a/command/secret/ls.go +++ b/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 b38ca0f4c3024fb78e86c7a36bc6e624f4c60e18 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Tue, 8 Nov 2016 18:29:10 -0800 Subject: [PATCH 231/563] 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 --- command/service/create.go | 2 +- command/service/opts.go | 22 +++++++++++----------- command/service/update.go | 20 ++++++++++---------- command/service/update_test.go | 4 ++-- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/command/service/create.go b/command/service/create.go index d6c3ebdb9..2b9a6df99 100644 --- a/command/service/create.go +++ b/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/command/service/opts.go b/command/service/opts.go index 827c4e5cd..6240fdc73 100644 --- a/command/service/opts.go +++ b/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/command/service/update.go b/command/service/update.go index 4a7722949..d4a672261 100644 --- a/command/service/update.go +++ b/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/command/service/update_test.go b/command/service/update_test.go index 91829b861..b99064352 100644 --- a/command/service/update_test.go +++ b/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 b825c58ff87acb942b83fdae4c9afa573baf73a4 Mon Sep 17 00:00:00 2001 From: Anusha Ragunathan Date: Tue, 4 Oct 2016 12:01:19 -0700 Subject: [PATCH 232/563] Add plugin create functionality. Signed-off-by: Anusha Ragunathan --- command/plugin/cmd.go | 1 + command/plugin/create.go | 125 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 command/plugin/create.go diff --git a/command/plugin/cmd.go b/command/plugin/cmd.go index c78f43a8d..2bb165db1 100644 --- a/command/plugin/cmd.go +++ b/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/command/plugin/create.go b/command/plugin/create.go new file mode 100644 index 000000000..3b18ed375 --- /dev/null +++ b/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 d006a04357b3503bb6550348e79b8ebf6acbb100 Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Fri, 21 Oct 2016 18:07:55 -0700 Subject: [PATCH 233/563] Add support for swarm init lock and swarm unlock Signed-off-by: Tonis Tiigi --- command/swarm/cmd.go | 1 + command/swarm/init.go | 46 +++++++++++++++++++++++++++++++++++++++++ command/swarm/opts.go | 1 + command/swarm/unlock.go | 35 +++++++++++++++++++++++++++++++ command/system/info.go | 2 +- 5 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 command/swarm/unlock.go diff --git a/command/swarm/cmd.go b/command/swarm/cmd.go index f0a6bcdeb..5ea973bb7 100644 --- a/command/swarm/cmd.go +++ b/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/command/swarm/init.go b/command/swarm/init.go index 16f372f8d..b2590e156 100644 --- a/command/swarm/init.go +++ b/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/command/swarm/opts.go b/command/swarm/opts.go index ce5a9b1de..a08c761a6 100644 --- a/command/swarm/opts.go +++ b/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/command/swarm/unlock.go b/command/swarm/unlock.go new file mode 100644 index 000000000..03a11da55 --- /dev/null +++ b/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/command/system/info.go b/command/system/info.go index 5ea23ed43..da5a396d6 100644 --- a/command/system/info.go +++ b/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 56b7ad90b1d0dcf61ed910eaf4ced22008284a28 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Thu, 27 Oct 2016 18:50:49 -0700 Subject: [PATCH 234/563] 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 --- command/swarm/cmd.go | 1 + command/swarm/init.go | 66 +++++++++---------------------------- command/swarm/opts.go | 6 ++++ command/swarm/unlock.go | 21 +++++++++++- command/swarm/unlock_key.go | 57 ++++++++++++++++++++++++++++++++ command/swarm/update.go | 13 ++++++++ 6 files changed, 112 insertions(+), 52 deletions(-) create mode 100644 command/swarm/unlock_key.go diff --git a/command/swarm/cmd.go b/command/swarm/cmd.go index 5ea973bb7..6c70459df 100644 --- a/command/swarm/cmd.go +++ b/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/command/swarm/init.go b/command/swarm/init.go index b2590e156..93c97c3a7 100644 --- a/command/swarm/init.go +++ b/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/command/swarm/opts.go b/command/swarm/opts.go index a08c761a6..8682375b1 100644 --- a/command/swarm/opts.go +++ b/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/command/swarm/unlock.go b/command/swarm/unlock.go index 03a11da55..51b06d626 100644 --- a/command/swarm/unlock.go +++ b/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/command/swarm/unlock_key.go b/command/swarm/unlock_key.go new file mode 100644 index 000000000..19caa0cc2 --- /dev/null +++ b/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/command/swarm/update.go b/command/swarm/update.go index a39f34c88..7c8876049 100644 --- a/command/swarm/update.go +++ b/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 65e1e166ee8dd6b2afd3d50072ecb0c06d3e2a5c Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Fri, 28 Oct 2016 16:35:49 -0700 Subject: [PATCH 235/563] Add unlock key rotation Signed-off-by: Aaron Lehmann --- command/swarm/unlock_key.go | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/command/swarm/unlock_key.go b/command/swarm/unlock_key.go index 19caa0cc2..96450f55b 100644 --- a/command/swarm/unlock_key.go +++ b/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 96c16101dd6e81eb9427aa970e599d2bb908972a Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Wed, 9 Nov 2016 16:59:01 -0800 Subject: [PATCH 236/563] fix manpages Signed-off-by: Victor Vieux --- command/secret/create.go | 2 +- command/secret/inspect.go | 3 +-- command/secret/ls.go | 2 +- command/secret/remove.go | 2 +- command/secret/utils.go | 3 +-- 5 files changed, 5 insertions(+), 7 deletions(-) diff --git a/command/secret/create.go b/command/secret/create.go index 980004834..da1cb9275 100644 --- a/command/secret/create.go +++ b/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/command/secret/inspect.go b/command/secret/inspect.go index 25da79f16..ad61706b3 100644 --- a/command/secret/inspect.go +++ b/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/command/secret/ls.go b/command/secret/ls.go index 67fc1daff..7471f08b1 100644 --- a/command/secret/ls.go +++ b/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/command/secret/remove.go b/command/secret/remove.go index d277eceba..0ee6d9f57 100644 --- a/command/secret/remove.go +++ b/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/command/secret/utils.go b/command/secret/utils.go index d1a7d97c4..621e60aaa 100644 --- a/command/secret/utils.go +++ b/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 b6fe99530cc885e9841f4ded90b409581886bbd4 Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Thu, 10 Nov 2016 10:39:23 +0800 Subject: [PATCH 237/563] Remove redundant parameter and fix typos Signed-off-by: yuexiao-wang --- command/container/exec.go | 5 ++--- command/container/exec_test.go | 7 +++---- command/stack/list.go | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/command/container/exec.go b/command/container/exec.go index 84eba113c..4bc8c5806 100644 --- a/command/container/exec.go +++ b/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/command/container/exec_test.go b/command/container/exec_test.go index 2e122e738..baeeaf190 100644 --- a/command/container/exec_test.go +++ b/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/command/stack/list.go b/command/stack/list.go index f655b929a..7be42525d 100644 --- a/command/stack/list.go +++ b/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 0ae9598f96597d13e961413b98160678a78dd062 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Mon, 7 Nov 2016 18:51:47 -0800 Subject: [PATCH 238/563] rename plugin manifest Signed-off-by: Victor Vieux --- command/plugin/create.go | 14 +++++++------- command/plugin/list.go | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/command/plugin/create.go b/command/plugin/create.go index 3b18ed375..94c0d2c36 100644 --- a/command/plugin/create.go +++ b/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/command/plugin/list.go b/command/plugin/list.go index 9d4b46d12..e402d44b3 100644 --- a/command/plugin/list.go +++ b/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 dc32cb6c772869d401a9af6f50ccefa77f555252 Mon Sep 17 00:00:00 2001 From: Andrew Hsu Date: Thu, 10 Nov 2016 08:23:19 -0800 Subject: [PATCH 239/563] use "golang.org/x/net/context" instead of "context" Signed-off-by: Andrew Hsu --- command/swarm/unlock.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/swarm/unlock.go b/command/swarm/unlock.go index 51b06d626..048fb56e3 100644 --- a/command/swarm/unlock.go +++ b/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 f702b722d8e7420820ab1eb1262829e0b590f57a Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 2 Nov 2016 14:57:40 -0400 Subject: [PATCH 240/563] Convert deploy to use a compose-file. Signed-off-by: Daniel Nephin --- command/service/opts.go | 5 +- command/service/update.go | 2 +- command/stack/cmd.go | 1 - command/stack/config.go | 39 ------ command/stack/deploy.go | 253 ++++++++++++++++++++++++++------------ command/stack/opts.go | 9 +- 6 files changed, 185 insertions(+), 124 deletions(-) delete mode 100644 command/stack/config.go diff --git a/command/service/opts.go b/command/service/opts.go index c48c952e0..2113fdfed 100644 --- a/command/service/opts.go +++ b/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/command/service/update.go b/command/service/update.go index 9741f67d5..d1c695d75 100644 --- a/command/service/update.go +++ b/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/command/stack/cmd.go b/command/stack/cmd.go index 418950440..ff71e0ddf 100644 --- a/command/stack/cmd.go +++ b/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/command/stack/config.go b/command/stack/config.go deleted file mode 100644 index 56e554a86..000000000 --- a/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/command/stack/deploy.go b/command/stack/deploy.go index 435a9193b..c1faa0521 100644 --- a/command/stack/deploy.go +++ b/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/command/stack/opts.go b/command/stack/opts.go index 5f2d8b5d0..c2cc0d1e7 100644 --- a/command/stack/opts.go +++ b/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 a9fc9b60feba5a6962e5583ca0a3a428c6c58b0e Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 25 Oct 2016 14:41:45 -0700 Subject: [PATCH 241/563] Add support for service-level 'volumes' key Support volume driver + options Support external volumes Support hostname in Compose file Signed-off-by: Aanand Prasad --- command/stack/deploy.go | 108 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 99 insertions(+), 9 deletions(-) diff --git a/command/stack/deploy.go b/command/stack/deploy.go index c1faa0521..96bd17545 100644 --- a/command/stack/deploy.go +++ b/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 e1b96b6447d3c64fce5d3b92508aac7bb504f3ea Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 28 Oct 2016 17:30:20 -0400 Subject: [PATCH 242/563] Add swarmkit fields to stack service. Signed-off-by: Daniel Nephin --- command/stack/deploy.go | 118 ++++++++++++++++++++++++++++++++-------- 1 file changed, 95 insertions(+), 23 deletions(-) diff --git a/command/stack/deploy.go b/command/stack/deploy.go index 96bd17545..e72abcc8c 100644 --- a/command/stack/deploy.go +++ b/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 dfab8f2bd42e6ca94fb3ad06b800402abd927198 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 31 Oct 2016 12:43:47 -0700 Subject: [PATCH 243/563] Handle unsupported, deprecated and forbidden properties Signed-off-by: Aanand Prasad --- command/stack/deploy.go | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/command/stack/deploy.go b/command/stack/deploy.go index e72abcc8c..6a1560923 100644 --- a/command/stack/deploy.go +++ b/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 25c93d4ebb906b29736b63dc5bf9aff53ff682b8 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 2 Nov 2016 13:10:34 +0000 Subject: [PATCH 244/563] Default to replicated mode Signed-off-by: Aanand Prasad --- command/stack/deploy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/stack/deploy.go b/command/stack/deploy.go index 6a1560923..83d55324d 100644 --- a/command/stack/deploy.go +++ b/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 ae8f00182973637c07c7bc852ee9c401b2c9ba91 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 2 Nov 2016 12:19:37 -0400 Subject: [PATCH 245/563] Send warnings to stderr. Signed-off-by: Daniel Nephin --- command/stack/deploy.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/command/stack/deploy.go b/command/stack/deploy.go index 83d55324d..b92662c3c 100644 --- a/command/stack/deploy.go +++ b/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 d89cb4c62fa3c7422bf9a7f0b81f8e1dbbfc512a Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 3 Nov 2016 17:40:48 -0600 Subject: [PATCH 246/563] Always use a default network if no other networks are set. also add network labels. Signed-off-by: Daniel Nephin --- command/stack/deploy.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/command/stack/deploy.go b/command/stack/deploy.go index b92662c3c..bb3e73e6e 100644 --- a/command/stack/deploy.go +++ b/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 ef845be6a52cdc93900885cffad48e62ccc4f385 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 3 Nov 2016 17:50:03 -0600 Subject: [PATCH 247/563] Remove duplication of name mangling. Signed-off-by: Daniel Nephin --- command/stack/common.go | 8 ++++++++ command/stack/deploy.go | 41 +++++++++++++++++++---------------------- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/command/stack/common.go b/command/stack/common.go index 4776ec1b4..b94c10866 100644 --- a/command/stack/common.go +++ b/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/command/stack/deploy.go b/command/stack/deploy.go index bb3e73e6e..fccd89eb5 100644 --- a/command/stack/deploy.go +++ b/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 3875355a3e593fac42bea3f88025c76acd3c18dc Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 4 Nov 2016 13:59:14 -0600 Subject: [PATCH 248/563] Remove bundlefile Signed-off-by: Daniel Nephin --- command/bundlefile/bundlefile.go | 69 ------------------------ command/bundlefile/bundlefile_test.go | 77 --------------------------- command/stack/opts.go | 39 +------------- 3 files changed, 1 insertion(+), 184 deletions(-) delete mode 100644 command/bundlefile/bundlefile.go delete mode 100644 command/bundlefile/bundlefile_test.go diff --git a/command/bundlefile/bundlefile.go b/command/bundlefile/bundlefile.go deleted file mode 100644 index 7fd1e4f6c..000000000 --- a/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/command/bundlefile/bundlefile_test.go b/command/bundlefile/bundlefile_test.go deleted file mode 100644 index c343410df..000000000 --- a/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/command/stack/opts.go b/command/stack/opts.go index c2cc0d1e7..a33e7707e 100644 --- a/command/stack/opts.go +++ b/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 d05510d954ea006ac985ce25d44e2dfcdf502c0c Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 4 Nov 2016 14:55:24 -0600 Subject: [PATCH 249/563] Add integration test for stack deploy. Signed-off-by: Daniel Nephin --- command/stack/deploy.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/command/stack/deploy.go b/command/stack/deploy.go index fccd89eb5..6201c2bd2 100644 --- a/command/stack/deploy.go +++ b/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 791b68784858c74def4a8648a962d35d80d88d9c Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 8 Nov 2016 17:05:23 +0000 Subject: [PATCH 250/563] Reinstate --bundle-file argument to 'docker deploy' Signed-off-by: Aanand Prasad --- command/bundlefile/bundlefile.go | 69 +++++++++ command/bundlefile/bundlefile_test.go | 77 ++++++++++ command/stack/deploy.go | 205 +++++++++++++++++++++----- command/stack/opts.go | 39 ++++- 4 files changed, 351 insertions(+), 39 deletions(-) create mode 100644 command/bundlefile/bundlefile.go create mode 100644 command/bundlefile/bundlefile_test.go diff --git a/command/bundlefile/bundlefile.go b/command/bundlefile/bundlefile.go new file mode 100644 index 000000000..7fd1e4f6c --- /dev/null +++ b/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/command/bundlefile/bundlefile_test.go b/command/bundlefile/bundlefile_test.go new file mode 100644 index 000000000..c343410df --- /dev/null +++ b/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/command/stack/deploy.go b/command/stack/deploy.go index 6201c2bd2..895442a04 100644 --- a/command/stack/deploy.go +++ b/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/command/stack/opts.go b/command/stack/opts.go index a33e7707e..c2cc0d1e7 100644 --- a/command/stack/opts.go +++ b/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 458ffcd2e66871e35b7ff0dc6fc5dad07cb3a2cf Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 8 Nov 2016 15:20:16 -0500 Subject: [PATCH 251/563] Restore stack deploy integration test with dab Signed-off-by: Daniel Nephin --- command/stack/deploy.go | 92 ++---------------------------- command/stack/deploy_bundlefile.go | 80 ++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 87 deletions(-) create mode 100644 command/stack/deploy_bundlefile.go diff --git a/command/stack/deploy.go b/command/stack/deploy.go index 895442a04..6a633c9a8 100644 --- a/command/stack/deploy.go +++ b/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/command/stack/deploy_bundlefile.go b/command/stack/deploy_bundlefile.go new file mode 100644 index 000000000..5ec8a2a05 --- /dev/null +++ b/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 0333117b88cd8fd43ff47b9d20feb62c1977e907 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 10 Nov 2016 11:33:00 -0500 Subject: [PATCH 252/563] Handle bind options and volume options Signed-off-by: Daniel Nephin --- command/stack/deploy.go | 154 +++++++++++++++++++++++++--------------- 1 file changed, 96 insertions(+), 58 deletions(-) diff --git a/command/stack/deploy.go b/command/stack/deploy.go index 6a633c9a8..f68ca8555 100644 --- a/command/stack/deploy.go +++ b/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 6f3ee9c5687d06306744fb619064791c11890399 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Thu, 3 Nov 2016 08:05:00 -0700 Subject: [PATCH 253/563] 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 --- command/service/create.go | 1 + command/service/opts.go | 20 ++++++++++++++ command/service/update.go | 49 ++++++++++++++++++++++++++++++++++ command/service/update_test.go | 20 ++++++++++++++ 4 files changed, 90 insertions(+) diff --git a/command/service/create.go b/command/service/create.go index 3efee10f4..17cf19625 100644 --- a/command/service/create.go +++ b/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/command/service/opts.go b/command/service/opts.go index c48c952e0..52971ae83 100644 --- a/command/service/opts.go +++ b/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/command/service/update.go b/command/service/update.go index 9741f67d5..f5acc2c51 100644 --- a/command/service/update.go +++ b/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/command/service/update_test.go b/command/service/update_test.go index b99064352..a3736090a 100644 --- a/command/service/update_test.go +++ b/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 fd5673eeb955f79eca9ce84469afb3dd688dde50 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Thu, 10 Nov 2016 12:05:19 -0800 Subject: [PATCH 254/563] 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 --- command/swarm/init.go | 1 + command/swarm/opts.go | 1 - command/swarm/update.go | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/command/swarm/init.go b/command/swarm/init.go index 93c97c3a7..2550feeb4 100644 --- a/command/swarm/init.go +++ b/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/command/swarm/opts.go b/command/swarm/opts.go index 8682375b1..885a3cd04 100644 --- a/command/swarm/opts.go +++ b/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/command/swarm/update.go b/command/swarm/update.go index 7c8876049..cb0d83ef2 100644 --- a/command/swarm/update.go +++ b/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 cb1783590c93a828fa1545a9cd3b8674ef2bdf67 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 10 Nov 2016 16:22:31 -0500 Subject: [PATCH 255/563] Implement ipamconfig.subnet and be more explicit about restart policy always. Signed-off-by: Daniel Nephin --- command/stack/deploy.go | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/command/stack/deploy.go b/command/stack/deploy.go index f68ca8555..33dd15e5a 100644 --- a/command/stack/deploy.go +++ b/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 b059cf5286f407ad4f7ce317d65391da290a72ed Mon Sep 17 00:00:00 2001 From: Andrea Luzzardi Date: Wed, 26 Oct 2016 01:19:32 -0700 Subject: [PATCH 256/563] cli: docker service logs support Signed-off-by: Andrea Luzzardi --- command/service/cmd.go | 1 + command/service/logs.go | 163 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 command/service/logs.go diff --git a/command/service/cmd.go b/command/service/cmd.go index f4f7d00f9..63f2db717 100644 --- a/command/service/cmd.go +++ b/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/command/service/logs.go b/command/service/logs.go new file mode 100644 index 000000000..19d3d9a48 --- /dev/null +++ b/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 f5cea67e3317fd7f439bd10fcd15f80b8a4b780b Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Wed, 9 Nov 2016 17:49:09 -0800 Subject: [PATCH 257/563] move plugins out of experimental Signed-off-by: Victor Vieux --- command/plugin/cmd.go | 1 - 1 file changed, 1 deletion(-) diff --git a/command/plugin/cmd.go b/command/plugin/cmd.go index e55f4ef32..6e1160fd9 100644 --- a/command/plugin/cmd.go +++ b/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 43bcd982cdee5a190b5b340d9852c1331e781331 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Thu, 10 Nov 2016 15:59:02 -0800 Subject: [PATCH 258/563] Update for distribution vendor Handle updates to reference package. Updates for refactoring of challenge manager. Signed-off-by: Derek McGowan (github: dmcgowan) --- command/image/trust.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/command/image/trust.go b/command/image/trust.go index b8de6a524..d1106b532 100644 --- a/command/image/trust.go +++ b/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 148dc157f6d4f947595b7a13c138276913cdd35a Mon Sep 17 00:00:00 2001 From: Jana Radhakrishnan Date: Thu, 10 Nov 2016 12:13:26 -0800 Subject: [PATCH 259/563] Add support for host port PublishMode in services Add api/cli support for adding host port PublishMode in services. Signed-off-by: Jana Radhakrishnan --- command/service/create.go | 4 ++- command/service/opts.go | 14 +++++---- command/service/update.go | 55 ++++++++++++++++++++++++++++++++-- command/service/update_test.go | 18 ++--------- command/task/print.go | 20 +++++++++++-- 5 files changed, 84 insertions(+), 27 deletions(-) diff --git a/command/service/create.go b/command/service/create.go index 17cf19625..335867186 100644 --- a/command/service/create.go +++ b/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/command/service/opts.go b/command/service/opts.go index 4ea78c6af..7da833851 100644 --- a/command/service/opts.go +++ b/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/command/service/update.go b/command/service/update.go index 1214b03a5..d2639a62d 100644 --- a/command/service/update.go +++ b/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/command/service/update_test.go b/command/service/update_test.go index a3736090a..998d06d3b 100644 --- a/command/service/update_test.go +++ b/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/command/task/print.go b/command/task/print.go index 45af178a4..2c5b2eecd 100644 --- a/command/task/print.go +++ b/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 356421b7dac6dabf0aa278f1ec5921004a63f7e5 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 11 Nov 2016 11:27:21 +0100 Subject: [PATCH 260/563] Add support for tty in composefile v3 Signed-off-by: Vincent Demeester --- command/stack/deploy.go | 1 + 1 file changed, 1 insertion(+) diff --git a/command/stack/deploy.go b/command/stack/deploy.go index 33dd15e5a..94ef6bac2 100644 --- a/command/stack/deploy.go +++ b/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 f24ff647e150dd1094d525b0bbcd2e994b75d45f Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 11 Nov 2016 15:15:10 +0100 Subject: [PATCH 261/563] Add support for stdin_open in composefile v3 Signed-off-by: Vincent Demeester --- command/stack/deploy.go | 1 + 1 file changed, 1 insertion(+) diff --git a/command/stack/deploy.go b/command/stack/deploy.go index 94ef6bac2..147df1a0b 100644 --- a/command/stack/deploy.go +++ b/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 84a795bf05c878d4929d8831f4e6d06af96d4454 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 11 Nov 2016 14:19:41 +0100 Subject: [PATCH 262/563] Add support for extra_hosts in composefile v3 Signed-off-by: Vincent Demeester --- command/stack/deploy.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/command/stack/deploy.go b/command/stack/deploy.go index 94ef6bac2..9996f128b 100644 --- a/command/stack/deploy.go +++ b/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 3c61af0f76bcf2ff0342f01c09bef771fbd567f6 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 11 Nov 2016 15:34:01 +0100 Subject: [PATCH 263/563] =?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 --- command/image/list.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/command/image/list.go b/command/image/list.go index 587869fdf..679604fc0 100644 --- a/command/image/list.go +++ b/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 885c5f174725bee8e3a6e1c42c8c06a18cfb5fa9 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Fri, 11 Nov 2016 09:56:25 -0500 Subject: [PATCH 264/563] only check secrets for service create if requested Signed-off-by: Evan Hazlett --- command/service/create.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/command/service/create.go b/command/service/create.go index 335867186..061a36f06 100644 --- a/command/service/create.go +++ b/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 cd71257cfd03e08df671040d7e60a937b8f8e073 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Thu, 3 Nov 2016 11:23:58 -0700 Subject: [PATCH 265/563] 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 --- command/system/info.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/command/system/info.go b/command/system/info.go index 36b09a9cc..b751bbff1 100644 --- a/command/system/info.go +++ b/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 4088f3bcff5c9704b33afa6345f41c02aa8409e8 Mon Sep 17 00:00:00 2001 From: John Howard Date: Fri, 23 Sep 2016 11:52:57 -0700 Subject: [PATCH 266/563] Planned 1.13 deprecation: email from login Signed-off-by: John Howard --- command/registry/login.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/command/registry/login.go b/command/registry/login.go index 93e1b40e3..f161f2d40 100644 --- a/command/registry/login.go +++ b/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 7e7c4eefa4a650af8ec08fa851cab0a2ec627439 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Fri, 11 Nov 2016 17:44:42 -0800 Subject: [PATCH 267/563] 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 --- command/service/opts.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/command/service/opts.go b/command/service/opts.go index 7da833851..90d0f9924 100644 --- a/command/service/opts.go +++ b/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 0f6af2074c64cc41a3d96520b8921291af1a4a48 Mon Sep 17 00:00:00 2001 From: yupeng Date: Sat, 12 Nov 2016 14:14:34 +0800 Subject: [PATCH 268/563] context.Context should be the first parameter of a function Signed-off-by: yupeng --- command/container/attach.go | 2 +- command/container/run.go | 2 +- command/container/start.go | 2 +- command/container/stats.go | 8 ++++---- command/container/stats_helpers.go | 2 +- command/container/utils.go | 4 ++-- command/secret/inspect.go | 2 +- command/secret/remove.go | 2 +- command/secret/utils.go | 2 +- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/command/container/attach.go b/command/container/attach.go index a1fe58dea..31bb10934 100644 --- a/command/container/attach.go +++ b/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/command/container/run.go b/command/container/run.go index 2f1181659..0fad93e68 100644 --- a/command/container/run.go +++ b/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/command/container/start.go b/command/container/start.go index 77bb9ddb9..3521a4194 100644 --- a/command/container/start.go +++ b/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/command/container/stats.go b/command/container/stats.go index 5e743a483..12d5c6852 100644 --- a/command/container/stats.go +++ b/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/command/container/stats_helpers.go b/command/container/stats_helpers.go index 8bc537ad3..4b57e3fe0 100644 --- a/command/container/stats_helpers.go +++ b/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/command/container/utils.go b/command/container/utils.go index d8a8ef355..6161e0714 100644 --- a/command/container/utils.go +++ b/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/command/secret/inspect.go b/command/secret/inspect.go index ad61706b3..04a5bd8a8 100644 --- a/command/secret/inspect.go +++ b/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/command/secret/remove.go b/command/secret/remove.go index 0ee6d9f57..44a71ef01 100644 --- a/command/secret/remove.go +++ b/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/command/secret/utils.go b/command/secret/utils.go index 621e60aaa..c6e3cb61a 100644 --- a/command/secret/utils.go +++ b/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 d3169abc3b0604a52d42549f1359686473b58d4f Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Sat, 12 Nov 2016 11:10:27 -0500 Subject: [PATCH 269/563] Fix issue with missing fields for `ps` template Signed-off-by: Brian Goff --- command/container/list.go | 13 ++++++------- command/formatter/container_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/command/container/list.go b/command/container/list.go index 80de7c5ff..b4cdfa2eb 100644 --- a/command/container/list.go +++ b/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/command/formatter/container_test.go b/command/formatter/container_test.go index 0a844efb6..cdfc911a9 100644 --- a/command/formatter/container_test.go +++ b/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 f1598f8b828b67ccfd3269bffd267c3d4dabb806 Mon Sep 17 00:00:00 2001 From: Anusha Ragunathan Date: Mon, 14 Nov 2016 08:38:06 -0800 Subject: [PATCH 270/563] Add docs for plugin push Signed-off-by: Anusha Ragunathan --- command/plugin/push.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/command/plugin/push.go b/command/plugin/push.go index 4e176bea3..e37a0483a 100644 --- a/command/plugin/push.go +++ b/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 5723c85b1de7bbbaf1a9f1f627629a21a299f71e Mon Sep 17 00:00:00 2001 From: John Howard Date: Tue, 1 Nov 2016 15:44:06 -0700 Subject: [PATCH 271/563] Windows: Use sequential file access Signed-off-by: John Howard --- command/image/load.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/command/image/load.go b/command/image/load.go index 4f88faf09..988f5106e 100644 --- a/command/image/load.go +++ b/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 3c564598013e4df79d210ef93b732c162d70ed65 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sat, 12 Nov 2016 16:44:51 -0800 Subject: [PATCH 272/563] 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 --- command/container/list.go | 32 +++++++++++----------- command/container/ps_test.go | 51 ++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 17 deletions(-) diff --git a/command/container/list.go b/command/container/list.go index b4cdfa2eb..60c246298 100644 --- a/command/container/list.go +++ b/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/command/container/ps_test.go b/command/container/ps_test.go index 9df4dfd5f..62b054527 100644 --- a/command/container/ps_test.go +++ b/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 4488d9f9fb416c9fe90acb774d1cd913fec80c22 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 16 Nov 2016 16:46:31 -0800 Subject: [PATCH 273/563] 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 --- command/formatter/service.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/command/formatter/service.go b/command/formatter/service.go index 1549047b7..aaa78386c 100644 --- a/command/formatter/service.go +++ b/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 d691bce8c9c849aeb52521c0c174d8aa3707dbaa Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 16 Nov 2016 22:17:40 -0800 Subject: [PATCH 274/563] 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 --- command/formatter/container_test.go | 4 ++-- command/stack/list.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/command/formatter/container_test.go b/command/formatter/container_test.go index cdfc911a9..16137897b 100644 --- a/command/formatter/container_test.go +++ b/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/command/stack/list.go b/command/stack/list.go index 7be42525d..f655b929a 100644 --- a/command/stack/list.go +++ b/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 55908f8a82d8fe88d3377df6d8b8dd2d03e46143 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Thu, 17 Nov 2016 10:54:10 -0800 Subject: [PATCH 275/563] refactor help func in CLI Signed-off-by: Victor Vieux --- command/checkpoint/cmd.go | 7 ++----- command/cli.go | 8 ++++++++ command/container/cmd.go | 5 +---- command/image/cmd.go | 5 +---- command/network/cmd.go | 5 +---- command/node/cmd.go | 5 +---- command/plugin/cmd.go | 5 +---- command/secret/cmd.go | 6 +----- command/service/cmd.go | 5 +---- command/stack/cmd.go | 7 ++----- command/swarm/cmd.go | 5 +---- command/system/cmd.go | 5 +---- command/volume/cmd.go | 5 +---- 13 files changed, 22 insertions(+), 51 deletions(-) diff --git a/command/checkpoint/cmd.go b/command/checkpoint/cmd.go index f186232a4..d5705a4da 100644 --- a/command/checkpoint/cmd.go +++ b/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/command/cli.go b/command/cli.go index ef9de2edf..99ea6331a 100644 --- a/command/cli.go +++ b/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/command/container/cmd.go b/command/container/cmd.go index 075f936bd..3e9b4880a 100644 --- a/command/container/cmd.go +++ b/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/command/image/cmd.go b/command/image/cmd.go index dc9825743..c3ca61f85 100644 --- a/command/image/cmd.go +++ b/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/command/network/cmd.go b/command/network/cmd.go index c2a7e83dd..ab8393cde 100644 --- a/command/network/cmd.go +++ b/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/command/node/cmd.go b/command/node/cmd.go index d70ee8178..e71b9199a 100644 --- a/command/node/cmd.go +++ b/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/command/plugin/cmd.go b/command/plugin/cmd.go index 6e1160fd9..2173943f8 100644 --- a/command/plugin/cmd.go +++ b/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/command/secret/cmd.go b/command/secret/cmd.go index 995300ad7..79e669858 100644 --- a/command/secret/cmd.go +++ b/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/command/service/cmd.go b/command/service/cmd.go index 63f2db717..796fe926c 100644 --- a/command/service/cmd.go +++ b/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/command/stack/cmd.go b/command/stack/cmd.go index ff71e0ddf..8626dc7fe 100644 --- a/command/stack/cmd.go +++ b/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/command/swarm/cmd.go b/command/swarm/cmd.go index 6c70459df..632679c4b 100644 --- a/command/swarm/cmd.go +++ b/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/command/system/cmd.go b/command/system/cmd.go index 9cd74b5d4..ab3beb895 100644 --- a/command/system/cmd.go +++ b/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/command/volume/cmd.go b/command/volume/cmd.go index 39e4b7f46..40862f29d 100644 --- a/command/volume/cmd.go +++ b/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 bc542f365c3534cbf086a55bf65dadcf7f87be91 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Tue, 15 Nov 2016 10:04:36 -0500 Subject: [PATCH 276/563] 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 --- command/service/parse.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/command/service/parse.go b/command/service/parse.go index 368bc6d44..ff3249e58 100644 --- a/command/service/parse.go +++ b/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 cc36bf62efbde2232d859e4ee08c78ae442fc0fc Mon Sep 17 00:00:00 2001 From: Boaz Shuster Date: Sun, 13 Nov 2016 10:28:25 +0200 Subject: [PATCH 277/563] Change the docker-tag usage text to be clearer Signed-off-by: Boaz Shuster --- command/image/tag.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/command/image/tag.go b/command/image/tag.go index b88789b0f..fb2b70385 100644 --- a/command/image/tag.go +++ b/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 e21f4f9996b332c3a723548ac8e246c6cb79c4a8 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 18 Nov 2016 15:09:13 +0100 Subject: [PATCH 278/563] 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 --- command/stack/deploy.go | 49 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/command/stack/deploy.go b/command/stack/deploy.go index 683f0cad3..13b43a78b 100644 --- a/command/stack/deploy.go +++ b/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 c682f10a8fda1544001d424e34a3356741a1a45f Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 18 Nov 2016 10:15:29 -0500 Subject: [PATCH 279/563] Default parallelism to 1. Signed-off-by: Daniel Nephin --- command/stack/deploy.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/command/stack/deploy.go b/command/stack/deploy.go index 13b43a78b..808cc7f4e 100644 --- a/command/stack/deploy.go +++ b/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 b866fa77f46dbe70cc87115938d209c3e03bc0e0 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Mon, 14 Nov 2016 18:08:24 -0800 Subject: [PATCH 280/563] 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 --- command/service/create.go | 4 ++++ command/service/scale.go | 6 +++++- command/service/update.go | 6 +++++- command/stack/deploy.go | 9 +++++++-- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/command/service/create.go b/command/service/create.go index 061a36f06..96c9f36da 100644 --- a/command/service/create.go +++ b/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/command/service/scale.go b/command/service/scale.go index ea30265bd..cf89e9027 100644 --- a/command/service/scale.go +++ b/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/command/service/update.go b/command/service/update.go index d2639a62d..20a4fc570 100644 --- a/command/service/update.go +++ b/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/command/stack/deploy.go b/command/stack/deploy.go index 683f0cad3..63adeacd6 100644 --- a/command/stack/deploy.go +++ b/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 5f1209bf4b2e2a3da74c5f86c742068aeb5983d6 Mon Sep 17 00:00:00 2001 From: Nishant Totla Date: Wed, 16 Nov 2016 22:21:18 -0800 Subject: [PATCH 281/563] Suppressing digest for docker service ls/ps Signed-off-by: Nishant Totla --- command/service/list.go | 13 ++++++++++++- command/task/print.go | 15 ++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/command/service/list.go b/command/service/list.go index f758808d1..724126079 100644 --- a/command/service/list.go +++ b/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/command/task/print.go b/command/task/print.go index 2c5b2eecd..2995e9afb 100644 --- a/command/task/print.go +++ b/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 82804cc8e510dea746c9c11365ec90890a4d88fe Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Fri, 18 Nov 2016 22:04:27 +0100 Subject: [PATCH 282/563] 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 --- command/stack/deploy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/stack/deploy.go b/command/stack/deploy.go index 63adeacd6..3075477b8 100644 --- a/command/stack/deploy.go +++ b/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 123d33d81dc037eebdb67a89dbce887f77ad7705 Mon Sep 17 00:00:00 2001 From: Antonio Murdaca Date: Wed, 16 Nov 2016 22:30:29 +0100 Subject: [PATCH 283/563] api: types: keep info.SecurityOptions a string slice Signed-off-by: Antonio Murdaca --- command/system/info.go | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/command/system/info.go b/command/system/info.go index b751bbff1..e0b876737 100644 --- a/command/system/info.go +++ b/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 2638cd6f3d38e6c580a479dcc399a7c4591788af Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 21 Nov 2016 17:59:29 +0100 Subject: [PATCH 284/563] 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 --- command/stack/deploy.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/command/stack/deploy.go b/command/stack/deploy.go index b0aaa290b..099f8c03a 100644 --- a/command/stack/deploy.go +++ b/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 e1b5bdd768312acca85d014f2fff85c53bf16f08 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 21 Nov 2016 15:30:25 -0500 Subject: [PATCH 285/563] Move docker stack out of experimental Signed-off-by: Daniel Nephin --- command/stack/cmd.go | 3 ++- command/stack/deploy.go | 1 - command/stack/opts.go | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/command/stack/cmd.go b/command/stack/cmd.go index 8626dc7fe..860bfedd1 100644 --- a/command/stack/cmd.go +++ b/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/command/stack/deploy.go b/command/stack/deploy.go index b0aaa290b..b7e1fc4fc 100644 --- a/command/stack/deploy.go +++ b/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/command/stack/opts.go b/command/stack/opts.go index c2cc0d1e7..440d6099e 100644 --- a/command/stack/opts.go +++ b/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 752a9a7c56837d6e9669db0447c828d1d0f77824 Mon Sep 17 00:00:00 2001 From: Anusha Ragunathan Date: Mon, 21 Nov 2016 09:24:01 -0800 Subject: [PATCH 286/563] Add HTTP client timeout. Signed-off-by: Anusha Ragunathan --- command/plugin/enable.go | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/command/plugin/enable.go b/command/plugin/enable.go index 0fd8f469d..d84da2466 100644 --- a/command/plugin/enable.go +++ b/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 4632a029d929ebb250e148799682dedc50f7777a Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Tue, 15 Nov 2016 12:10:02 -0500 Subject: [PATCH 287/563] 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 --- command/container/utils.go | 55 +++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/command/container/utils.go b/command/container/utils.go index 6161e0714..f4ad09b91 100644 --- a/command/container/utils.go +++ b/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 14770269e8527dae0510dc0146e50a0a59355d90 Mon Sep 17 00:00:00 2001 From: Reficul Date: Tue, 22 Nov 2016 10:42:55 +0800 Subject: [PATCH 288/563] fix incorrect ErrConnectFailed comparison Signed-off-by: Reficul --- command/container/exec.go | 2 +- command/container/utils.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/command/container/exec.go b/command/container/exec.go index 4bc8c5806..f0381494e 100644 --- a/command/container/exec.go +++ b/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/command/container/utils.go b/command/container/utils.go index 6161e0714..f42a8def1 100644 --- a/command/container/utils.go +++ b/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 46cd1fa87b2eecb9dba9141d5fe18215a7fb0fd3 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Tue, 22 Nov 2016 11:18:28 -0500 Subject: [PATCH 289/563] 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 --- command/secret/inspect.go | 24 +++++++----------------- command/secret/remove.go | 28 +++++----------------------- command/secret/utils.go | 27 +++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 40 deletions(-) diff --git a/command/secret/inspect.go b/command/secret/inspect.go index 04a5bd8a8..a82a26e4a 100644 --- a/command/secret/inspect.go +++ b/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/command/secret/remove.go b/command/secret/remove.go index 44a71ef01..75b4be622 100644 --- a/command/secret/remove.go +++ b/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/command/secret/utils.go b/command/secret/utils.go index c6e3cb61a..0134853e0 100644 --- a/command/secret/utils.go +++ b/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 357cabef2d1bbeae18ecd1b86cc1687d4484b420 Mon Sep 17 00:00:00 2001 From: cyli Date: Tue, 22 Nov 2016 16:37:02 -0500 Subject: [PATCH 290/563] Do not display the digest or size of swarm secrets Signed-off-by: cyli --- command/secret/ls.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/command/secret/ls.go b/command/secret/ls.go index 7471f08b1..e99f99e3d 100644 --- a/command/secret/ls.go +++ b/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 0171a79c56599d5b687dd046ac7295c5cacad779 Mon Sep 17 00:00:00 2001 From: erxian Date: Fri, 18 Nov 2016 14:28:21 +0800 Subject: [PATCH 291/563] update secret command Signed-off-by: erxian --- command/secret/create.go | 2 +- command/secret/inspect.go | 4 ++-- command/secret/ls.go | 2 +- command/secret/remove.go | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/command/secret/create.go b/command/secret/create.go index da1cb9275..faef32ef8 100644 --- a/command/secret/create.go +++ b/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/command/secret/inspect.go b/command/secret/inspect.go index a82a26e4a..1dda6f783 100644 --- a/command/secret/inspect.go +++ b/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/command/secret/ls.go b/command/secret/ls.go index e99f99e3d..d96b37786 100644 --- a/command/secret/ls.go +++ b/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/command/secret/remove.go b/command/secret/remove.go index 75b4be622..5026a437f 100644 --- a/command/secret/remove.go +++ b/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 5ead1cc4901027ced77944ff5eefe4b829ec1a14 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 21 Nov 2016 15:03:43 -0500 Subject: [PATCH 292/563] Better error message on stack deploy against not a swarm. Signed-off-by: Daniel Nephin --- command/stack/deploy.go | 29 +++++++++++++++++++++++++---- command/stack/deploy_bundlefile.go | 8 +++++--- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/command/stack/deploy.go b/command/stack/deploy.go index 2cd4efebc..d8e76106a 100644 --- a/command/stack/deploy.go +++ b/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/command/stack/deploy_bundlefile.go b/command/stack/deploy_bundlefile.go index 5ec8a2a05..c82c46e42 100644 --- a/command/stack/deploy_bundlefile.go +++ b/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 7604609bed8696798e496adade06df8102b0780b Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 15 Nov 2016 11:18:33 -0500 Subject: [PATCH 293/563] exit with status 1 if help is called on an invalid command. Signed-off-by: Daniel Nephin --- cobra.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/cobra.go b/cobra.go index 324c0d7b2..139845cb1 100644 --- a/cobra.go +++ b/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 dc5c8a7713e3a27c5104e780fd34ef689e87862e Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Wed, 23 Nov 2016 14:30:57 -0800 Subject: [PATCH 294/563] support src in --secret Signed-off-by: Victor Vieux --- command/service/opts.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/service/opts.go b/command/service/opts.go index 90d0f9924..92e00ce41 100644 --- a/command/service/opts.go +++ b/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 7a89624bd516e00845d31b997dcb1d992ea6d42e Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Fri, 25 Nov 2016 04:07:06 +0800 Subject: [PATCH 295/563] Add options for docker plugin enable and fix some typos Signed-off-by: yuexiao-wang --- command/plugin/enable.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/plugin/enable.go b/command/plugin/enable.go index d84da2466..9201e38e1 100644 --- a/command/plugin/enable.go +++ b/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 961046c5a8e4917498e4c21ba84d5325faa20a45 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 23 Nov 2016 23:39:14 -0800 Subject: [PATCH 296/563] 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 --- command/network/list.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/network/list.go b/command/network/list.go index 9ba803275..1a5d28510 100644 --- a/command/network/list.go +++ b/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 c40696023b0f439633379086340fdc751724d07a Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 23 Nov 2016 14:42:56 -0500 Subject: [PATCH 297/563] Allow hostname to be updated on service. Signed-off-by: Daniel Nephin --- command/service/create.go | 1 - command/service/opts.go | 1 + command/service/update.go | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/command/service/create.go b/command/service/create.go index 96c9f36da..ea078e43a 100644 --- a/command/service/create.go +++ b/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/command/service/opts.go b/command/service/opts.go index 90d0f9924..7e631743a 100644 --- a/command/service/opts.go +++ b/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/command/service/update.go b/command/service/update.go index 20a4fc570..92329d143 100644 --- a/command/service/update.go +++ b/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 48537db8498635f891dcc825c998a237a85685b3 Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Fri, 25 Nov 2016 19:46:24 +0800 Subject: [PATCH 298/563] Modify reponame to PLUGIN and fix some typos Signed-off-by: yuexiao-wang --- command/plugin/create.go | 2 +- command/plugin/push.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/command/plugin/create.go b/command/plugin/create.go index 94c0d2c36..e0041c1b8 100644 --- a/command/plugin/create.go +++ b/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/command/plugin/push.go b/command/plugin/push.go index e37a0483a..add4a2b0a 100644 --- a/command/plugin/push.go +++ b/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 7b35599e2d6695266e4c1aaf61aa6a70b6dc8201 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 24 Nov 2016 16:11:38 -0500 Subject: [PATCH 299/563] Add a short flag for docker stack deploy Signed-off-by: Daniel Nephin --- command/stack/opts.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/stack/opts.go b/command/stack/opts.go index 440d6099e..74fe4f534 100644 --- a/command/stack/opts.go +++ b/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 8feea86e0f09cb81261d40d0d2f6e27aaa8fe99c Mon Sep 17 00:00:00 2001 From: Kei Ohmura Date: Mon, 28 Nov 2016 13:24:02 +0900 Subject: [PATCH 300/563] fix description of 'docker swarm init' Signed-off-by: Kei Ohmura --- command/swarm/opts.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/command/swarm/opts.go b/command/swarm/opts.go index 885a3cd04..9db46dcf5 100644 --- a/command/swarm/opts.go +++ b/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 8e63000bf359322be9ebda10a6d1d5ecd80bd8eb Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 28 Nov 2016 17:38:41 +0100 Subject: [PATCH 301/563] 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 --- command/stack/common.go | 2 +- command/stack/deploy.go | 66 ++++++++++++++++++++++++++++++++++------- command/stack/remove.go | 2 +- 3 files changed, 57 insertions(+), 13 deletions(-) diff --git a/command/stack/common.go b/command/stack/common.go index b94c10866..920a1af0c 100644 --- a/command/stack/common.go +++ b/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/command/stack/deploy.go b/command/stack/deploy.go index d8e76106a..66195ffbb 100644 --- a/command/stack/deploy.go +++ b/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/command/stack/remove.go b/command/stack/remove.go index 8137903d4..734ff92a5 100644 --- a/command/stack/remove.go +++ b/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 6ffb62368a88d08f0721da4a511917ea2aab63fd Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 28 Nov 2016 18:08:45 +0100 Subject: [PATCH 302/563] Revert "Add -a option to service/node ps" This reverts commit 139fff2bf0ebe12b61871ba8ec8be8d51c2338db. Signed-off-by: Vincent Demeester --- command/node/ps.go | 7 ------- command/service/ps.go | 8 -------- 2 files changed, 15 deletions(-) diff --git a/command/node/ps.go b/command/node/ps.go index 8591f0466..a034721d2 100644 --- a/command/node/ps.go +++ b/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/command/service/ps.go b/command/service/ps.go index 0028507c2..cf94ad737 100644 --- a/command/service/ps.go +++ b/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 a913891b7d2558c4b8612875f5e90731d16753c0 Mon Sep 17 00:00:00 2001 From: John Howard Date: Wed, 23 Nov 2016 13:42:50 -0800 Subject: [PATCH 303/563] Align output of docker version again Signed-off-by: John Howard --- command/system/version.go | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/command/system/version.go b/command/system/version.go index 00a84a3cb..ded4f4d11 100644 --- a/command/system/version.go +++ b/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 798c4a614e2488b46058fe145281db9b149c2537 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 28 Nov 2016 18:02:39 -0500 Subject: [PATCH 304/563] Use namespace label on stack volumes. Signed-off-by: Daniel Nephin --- command/stack/deploy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/stack/deploy.go b/command/stack/deploy.go index d8e76106a..9161c0965 100644 --- a/command/stack/deploy.go +++ b/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 0227275b7f8d09ed5c8787ca8ac534a95cbcac11 Mon Sep 17 00:00:00 2001 From: allencloud Date: Tue, 29 Nov 2016 14:28:46 +0800 Subject: [PATCH 305/563] change secret remove logic in cli Signed-off-by: allencloud --- command/secret/remove.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/command/secret/remove.go b/command/secret/remove.go index 5026a437f..97d1f445c 100644 --- a/command/secret/remove.go +++ b/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 5e2a13b97193bb1bd13c11cf8c118dde476c3814 Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Tue, 29 Nov 2016 01:21:47 +0800 Subject: [PATCH 306/563] Fix some typos Signed-off-by: yuexiao-wang --- command/registry/search.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/registry/search.go b/command/registry/search.go index 124b4ae6c..bbcedbdd9 100644 --- a/command/registry/search.go +++ b/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 7a9e414988cd16db520b083ddae728fd986d971e Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Tue, 29 Nov 2016 23:44:12 +0800 Subject: [PATCH 307/563] Fix the inconsistency for docker secret Signed-off-by: yuexiao-wang --- command/secret/inspect.go | 2 +- command/secret/ls.go | 7 ++++--- command/secret/remove.go | 7 ++++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/command/secret/inspect.go b/command/secret/inspect.go index 1dda6f783..0a8bd4a23 100644 --- a/command/secret/inspect.go +++ b/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/command/secret/ls.go b/command/secret/ls.go index d96b37786..faeab314b 100644 --- a/command/secret/ls.go +++ b/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/command/secret/remove.go b/command/secret/remove.go index 5026a437f..41651a16b 100644 --- a/command/secret/remove.go +++ b/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 cd79095c814a29dea0987aafc32257dfaeb6965d Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Thu, 1 Dec 2016 18:30:44 +0800 Subject: [PATCH 308/563] Fix the use for secret create Signed-off-by: yuexiao-wang --- command/secret/create.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/command/secret/create.go b/command/secret/create.go index faef32ef8..381a93141 100644 --- a/command/secret/create.go +++ b/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 c84b90291ca7b75099eaa6f52349cc073ee0f0fe Mon Sep 17 00:00:00 2001 From: Jake Sanders Date: Thu, 18 Aug 2016 14:23:10 -0700 Subject: [PATCH 309/563] Add registry-specific credential helper support Signed-off-by: Jake Sanders --- command/cli.go | 49 ++++++++++++++++++++++++++++++++++---- command/image/build.go | 4 ++-- command/registry.go | 4 ++-- command/registry/login.go | 2 +- command/registry/logout.go | 2 +- 5 files changed, 51 insertions(+), 10 deletions(-) diff --git a/command/cli.go b/command/cli.go index 99ea6331a..6d1dd7472 100644 --- a/command/cli.go +++ b/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/command/image/build.go b/command/image/build.go index ebec87d64..78cc41494 100644 --- a/command/image/build.go +++ b/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/command/registry.go b/command/registry.go index b70d6f444..65f6b3309 100644 --- a/command/registry.go +++ b/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/command/registry/login.go b/command/registry/login.go index f161f2d40..bdcc9a103 100644 --- a/command/registry/login.go +++ b/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/command/registry/logout.go b/command/registry/logout.go index 8e820dcc8..877e60e8c 100644 --- a/command/registry/logout.go +++ b/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 312958f4db93d6675a39913cd2e1de8dbed3fc70 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 23 Nov 2016 20:04:44 -0800 Subject: [PATCH 310/563] 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 --- command/plugin/inspect.go | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/command/plugin/inspect.go b/command/plugin/inspect.go index 13c7fa72d..46ec7b229 100644 --- a/command/plugin/inspect.go +++ b/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 8a379d7bcebea5415e462f85ee2c1c0bf704178a Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Thu, 1 Dec 2016 14:08:06 -0800 Subject: [PATCH 311/563] 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 --- command/formatter/service.go | 14 ++++++++++---- command/service/inspect_test.go | 6 +++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/command/formatter/service.go b/command/formatter/service.go index aaa78386c..2690029ce 100644 --- a/command/formatter/service.go +++ b/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/command/service/inspect_test.go b/command/service/inspect_test.go index 04a65080c..34c41ee78 100644 --- a/command/service/inspect_test.go +++ b/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 8597e231a5caa23247934f7a89c1001d87e26192 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Thu, 18 Aug 2016 18:09:07 -0700 Subject: [PATCH 312/563] 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 --- command/service/update.go | 19 +++++++++++++++- command/service/update_test.go | 40 ++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/command/service/update.go b/command/service/update.go index 92329d143..200f58c3a 100644 --- a/command/service/update.go +++ b/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/command/service/update_test.go b/command/service/update_test.go index 998d06d3b..bb2e9c107 100644 --- a/command/service/update_test.go +++ b/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 b68a6ad2ac8fdb513ff18d4c127848cd50b23525 Mon Sep 17 00:00:00 2001 From: Arash Deshmeh Date: Thu, 1 Dec 2016 23:28:51 -0500 Subject: [PATCH 313/563] Print checkpoint id when creating a checkpoint Signed-off-by: Arash Deshmeh --- command/checkpoint/create.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/command/checkpoint/create.go b/command/checkpoint/create.go index 2377b5e2e..473a94173 100644 --- a/command/checkpoint/create.go +++ b/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 492c2c8da878fecd9c2bc7a708af7963f56ece21 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Tue, 29 Nov 2016 17:31:29 -0800 Subject: [PATCH 314/563] 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 --- command/system/inspect.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/command/system/inspect.go b/command/system/inspect.go index a403685ee..dee4efcfe 100644 --- a/command/system/inspect.go +++ b/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 0449997cf6f1db858fc8274ab653bc49b6b3e835 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Fri, 2 Dec 2016 13:42:50 -0800 Subject: [PATCH 315/563] 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 --- command/plugin/list.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/command/plugin/list.go b/command/plugin/list.go index e402d44b3..4f800d7ec 100644 --- a/command/plugin/list.go +++ b/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 dd39897fcaa611bdcd87cd7a560395dfb55ead80 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 16 Nov 2016 21:46:37 -0800 Subject: [PATCH 316/563] 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 --- command/container/prune.go | 4 ++-- command/image/prune.go | 9 +++++---- command/network/prune.go | 4 ++-- command/volume/prune.go | 4 ++-- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/command/container/prune.go b/command/container/prune.go index ec6b0e314..064f4c08e 100644 --- a/command/container/prune.go +++ b/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/command/image/prune.go b/command/image/prune.go index ea84cda87..82c28fcf4 100644 --- a/command/image/prune.go +++ b/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/command/network/prune.go b/command/network/prune.go index f2f8cc20c..9f1979e6b 100644 --- a/command/network/prune.go +++ b/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/command/volume/prune.go b/command/volume/prune.go index ac9c94451..405fbeb29 100644 --- a/command/volume/prune.go +++ b/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 ff9ff6fdd29a8465f873773f39228bcaccbdc89d Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Mon, 28 Nov 2016 12:18:15 -0800 Subject: [PATCH 317/563] 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 --- command/secret/utils.go | 64 +++++++++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 18 deletions(-) diff --git a/command/secret/utils.go b/command/secret/utils.go index 0134853e0..42493896c 100644 --- a/command/secret/utils.go +++ b/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 68db0a20ddb44f9d3bd3220af7a1556b39baac9c Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 5 Dec 2016 15:18:36 +0100 Subject: [PATCH 318/563] 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 --- command/stack/deploy.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/command/stack/deploy.go b/command/stack/deploy.go index e7764f3b8..1f41cb7d8 100644 --- a/command/stack/deploy.go +++ b/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 a5a246dbbc2a49b0a283409c0c5f879c38ca84db Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Tue, 6 Dec 2016 11:27:27 -0800 Subject: [PATCH 319/563] 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 --- command/image/pull.go | 17 +++---------- command/image/trust.go | 55 +++++++++++++++++++----------------------- 2 files changed, 29 insertions(+), 43 deletions(-) diff --git a/command/image/pull.go b/command/image/pull.go index 9116d4584..13de492f9 100644 --- a/command/image/pull.go +++ b/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/command/image/trust.go b/command/image/trust.go index d1106b532..8f5c76d8c 100644 --- a/command/image/trust.go +++ b/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 43ed65ee28a64f9b490b9d2df5077a8ccf417f68 Mon Sep 17 00:00:00 2001 From: Andrea Luzzardi Date: Tue, 6 Dec 2016 18:52:47 -0800 Subject: [PATCH 320/563] 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 --- command/task/print.go | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/command/task/print.go b/command/task/print.go index 2995e9afb..0f1c2cf72 100644 --- a/command/task/print.go +++ b/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 f1801ba475fe5a7c51b6303ad481d0dd52a56d10 Mon Sep 17 00:00:00 2001 From: Doug Davis Date: Sat, 3 Dec 2016 05:46:04 -0800 Subject: [PATCH 321/563] 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 --- command/image/build.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/command/image/build.go b/command/image/build.go index 78cc41494..1699b2c45 100644 --- a/command/image/build.go +++ b/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 e41cf4a8600631b3fe90212e1c12ca4d5d4ad132 Mon Sep 17 00:00:00 2001 From: wefine Date: Fri, 2 Dec 2016 00:08:24 +0800 Subject: [PATCH 322/563] Give a order to AddCommands, for easy read and maintenance. Signed-off-by: wefine --- command/commands/commands.go | 54 ++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/command/commands/commands.go b/command/commands/commands.go index d64d5680c..0db7f3a40 100644 --- a/command/commands/commands.go +++ b/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 1da163febe0109b6c719b218dfc070bd52b76280 Mon Sep 17 00:00:00 2001 From: John Howard Date: Fri, 9 Dec 2016 14:21:45 -0800 Subject: [PATCH 323/563] Windows: Prompt fix Signed-off-by: John Howard --- command/utils.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/command/utils.go b/command/utils.go index 9f9a1ee80..1837ca41f 100644 --- a/command/utils.go +++ b/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 7fbc616b4779d6290afcb7b97709eb784424df0b Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Thu, 8 Dec 2016 22:32:10 +0100 Subject: [PATCH 324/563] 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 --- command/service/create.go | 2 - command/service/opts.go | 54 +++++------------------ command/service/update.go | 79 +++++++++++----------------------- command/service/update_test.go | 1 + command/stack/deploy.go | 3 +- 5 files changed, 37 insertions(+), 102 deletions(-) diff --git a/command/service/create.go b/command/service/create.go index ea078e43a..a8382835a 100644 --- a/command/service/create.go +++ b/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/command/service/opts.go b/command/service/opts.go index 023b922a1..c7518e597 100644 --- a/command/service/opts.go +++ b/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/command/service/update.go b/command/service/update.go index 200f58c3a..2ceaf275a 100644 --- a/command/service/update.go +++ b/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/command/service/update_test.go b/command/service/update_test.go index bb2e9c107..3cb765799 100644 --- a/command/service/update_test.go +++ b/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/command/stack/deploy.go b/command/stack/deploy.go index 1f41cb7d8..00a7634a0 100644 --- a/command/stack/deploy.go +++ b/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 b4a6d83dc278b9a8cf0c83b4f3685320eed4deb4 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 9 Dec 2016 21:17:57 +0100 Subject: [PATCH 325/563] 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 --- command/service/update.go | 43 +++++++++++++++++----------------- command/service/update_test.go | 36 +++++++++++++--------------- 2 files changed, 37 insertions(+), 42 deletions(-) diff --git a/command/service/update.go b/command/service/update.go index 2ceaf275a..4bbcf35a8 100644 --- a/command/service/update.go +++ b/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/command/service/update_test.go b/command/service/update_test.go index 3cb765799..08fe24876 100644 --- a/command/service/update_test.go +++ b/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 a51750a650e073bdc88f084baee68f27297b153e Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 12 Dec 2016 09:33:58 +0100 Subject: [PATCH 326/563] Move debug functions to cli/debug package Signed-off-by: Vincent Demeester --- command/system/info.go | 4 ++-- debug/debug.go | 26 +++++++++++++++++++++++++ debug/debug_test.go | 43 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 debug/debug.go create mode 100644 debug/debug_test.go diff --git a/command/system/info.go b/command/system/info.go index e0b876737..6c3487de8 100644 --- a/command/system/info.go +++ b/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/debug/debug.go b/debug/debug.go new file mode 100644 index 000000000..51dfab2a9 --- /dev/null +++ b/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/debug/debug_test.go b/debug/debug_test.go new file mode 100644 index 000000000..ad8412a94 --- /dev/null +++ b/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 26fca512dd6d99a5b8fa144676eb6a3cdaa4da90 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 12 Dec 2016 09:34:03 +0100 Subject: [PATCH 327/563] Move templates to pkg/templates Signed-off-by: Vincent Demeester --- command/container/list.go | 2 +- command/formatter/formatter.go | 2 +- command/inspect/inspector.go | 2 +- command/inspect/inspector_test.go | 2 +- command/system/events.go | 2 +- command/system/info.go | 2 +- command/system/version.go | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/command/container/list.go b/command/container/list.go index 60c246298..5104e9b6c 100644 --- a/command/container/list.go +++ b/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/command/formatter/formatter.go b/command/formatter/formatter.go index e859a1ca2..4345f7c3b 100644 --- a/command/formatter/formatter.go +++ b/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/command/inspect/inspector.go b/command/inspect/inspector.go index 1d81643fb..1e53671f8 100644 --- a/command/inspect/inspector.go +++ b/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/command/inspect/inspector_test.go b/command/inspect/inspector_test.go index 1ce1593ab..9085230ac 100644 --- a/command/inspect/inspector_test.go +++ b/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/command/system/events.go b/command/system/events.go index 087523051..441ef91d3 100644 --- a/command/system/events.go +++ b/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/command/system/info.go b/command/system/info.go index 6c3487de8..e11aff77b 100644 --- a/command/system/info.go +++ b/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/command/system/version.go b/command/system/version.go index ded4f4d11..569da2188 100644 --- a/command/system/version.go +++ b/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 dd2b83e297e1a8b48668b968635d04bed7420597 Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Fri, 9 Dec 2016 23:15:26 +0800 Subject: [PATCH 328/563] Update the option 'network' for docker build Signed-off-by: yuexiao-wang --- command/image/build.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/image/build.go b/command/image/build.go index 1699b2c45..e3e7ff2b0 100644 --- a/command/image/build.go +++ b/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 249d4e5709da7e1438e9eeab1190fb9f097b4058 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 30 Nov 2016 13:23:18 -0800 Subject: [PATCH 329/563] 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 --- command/swarm/update.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/command/swarm/update.go b/command/swarm/update.go index cb0d83ef2..dbbd26872 100644 --- a/command/swarm/update.go +++ b/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 8e680c48f1e112e2b477939bffb090c30b3b0f1b Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sat, 19 Nov 2016 17:41:11 -0800 Subject: [PATCH 330/563] 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 --- command/secret/create.go | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/command/secret/create.go b/command/secret/create.go index 381a93141..5d4dc34d1 100644 --- a/command/secret/create.go +++ b/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 518b65c6f5f2de65932a715d161324305cbdc693 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Thu, 8 Dec 2016 12:04:22 +0100 Subject: [PATCH 331/563] 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 --- command/system/inspect.go | 93 +++++++++++++++++++++++++++++++-------- 1 file changed, 74 insertions(+), 19 deletions(-) diff --git a/command/system/inspect.go b/command/system/inspect.go index dee4efcfe..cb5a1213a 100644 --- a/command/system/inspect.go +++ b/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 639f97daea2e286e7dbb811591b0b5d3909b6677 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Mon, 5 Dec 2016 16:06:29 -0800 Subject: [PATCH 332/563] 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 --- command/image/trust.go | 241 +++--------------------------------- command/image/trust_test.go | 9 +- trust/trust.go | 221 +++++++++++++++++++++++++++++++++ 3 files changed, 244 insertions(+), 227 deletions(-) create mode 100644 trust/trust.go diff --git a/command/image/trust.go b/command/image/trust.go index 8f5c76d8c..f32c30195 100644 --- a/command/image/trust.go +++ b/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/command/image/trust_test.go b/command/image/trust_test.go index ba6373f2d..78146465e 100644 --- a/command/image/trust_test.go +++ b/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/trust/trust.go b/trust/trust.go new file mode 100644 index 000000000..0f3482f2d --- /dev/null +++ b/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 fdb6a6ee1cf6f69a2dee00bce2d2d062d4cfa15e Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Mon, 5 Dec 2016 17:02:26 -0800 Subject: [PATCH 333/563] 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 --- command/service/create.go | 4 ++ command/service/trust.go | 96 +++++++++++++++++++++++++++++++++++++++ command/service/update.go | 6 +++ 3 files changed, 106 insertions(+) create mode 100644 command/service/trust.go diff --git a/command/service/create.go b/command/service/create.go index a8382835a..ca2bb089f 100644 --- a/command/service/create.go +++ b/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/command/service/trust.go b/command/service/trust.go new file mode 100644 index 000000000..052d49c32 --- /dev/null +++ b/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/command/service/update.go b/command/service/update.go index 4bbcf35a8..514b1bd51 100644 --- a/command/service/update.go +++ b/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 9197940e4e21061febce4115cb79a9594160f0c8 Mon Sep 17 00:00:00 2001 From: unclejack Date: Wed, 14 Dec 2016 23:16:12 +0200 Subject: [PATCH 334/563] return directly without ifs in remaining packages Signed-off-by: Cristian Staretu --- command/task/print.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/command/task/print.go b/command/task/print.go index 0f1c2cf72..57c4e0c8c 100644 --- a/command/task/print.go +++ b/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 4cf95aeaa2e13e3d8e302eeffbdee3345aaae331 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Fri, 16 Dec 2016 15:10:20 +0100 Subject: [PATCH 335/563] 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 --- command/swarm/leave.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/swarm/leave.go b/command/swarm/leave.go index 1ffaa3fcc..e2cfa0a04 100644 --- a/command/swarm/leave.go +++ b/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 a28db56b0f02effe389287cc6a3e3d90bce82b3f Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 30 Nov 2016 15:33:54 -0500 Subject: [PATCH 336/563] Move ConvertNetworks to composetransform package. Signed-off-by: Daniel Nephin --- command/stack/common.go | 20 ------------------ command/stack/deploy.go | 45 ----------------------------------------- 2 files changed, 65 deletions(-) diff --git a/command/stack/common.go b/command/stack/common.go index 920a1af0c..4ae818493 100644 --- a/command/stack/common.go +++ b/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/command/stack/deploy.go b/command/stack/deploy.go index 00a7634a0..f1ab65ce9 100644 --- a/command/stack/deploy.go +++ b/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 af6a4113583ce2fb1c4e31aec5204dc11b454e54 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 30 Nov 2016 16:34:29 -0500 Subject: [PATCH 337/563] Move ConvertVolumes to composetransform package. Signed-off-by: Daniel Nephin --- command/stack/common.go | 3 +- command/stack/deploy.go | 127 +++------------------------------------- 2 files changed, 10 insertions(+), 120 deletions(-) diff --git a/command/stack/common.go b/command/stack/common.go index 4ae818493..050528de4 100644 --- a/command/stack/common.go +++ b/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/command/stack/deploy.go b/command/stack/deploy.go index f1ab65ce9..e8238cde6 100644 --- a/command/stack/deploy.go +++ b/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 31355030b38d6e856098df8d790feebeb9edddc6 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 30 Nov 2016 17:38:40 -0500 Subject: [PATCH 338/563] Move ConvertService to composetransform package. Signed-off-by: Daniel Nephin --- command/stack/common.go | 13 ++ command/stack/deploy.go | 327 +---------------------------- command/stack/deploy_bundlefile.go | 13 +- command/stack/list.go | 12 +- command/stack/ps.go | 3 +- command/stack/services.go | 4 +- 6 files changed, 30 insertions(+), 342 deletions(-) diff --git a/command/stack/common.go b/command/stack/common.go index 050528de4..c3a43f2cd 100644 --- a/command/stack/common.go +++ b/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/command/stack/deploy.go b/command/stack/deploy.go index e8238cde6..957f92f29 100644 --- a/command/stack/deploy.go +++ b/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/command/stack/deploy_bundlefile.go b/command/stack/deploy_bundlefile.go index c82c46e42..f9a416238 100644 --- a/command/stack/deploy_bundlefile.go +++ b/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/command/stack/list.go b/command/stack/list.go index f655b929a..52e593316 100644 --- a/command/stack/list.go +++ b/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/command/stack/ps.go b/command/stack/ps.go index 7a5e069cb..497fb97b5 100644 --- a/command/stack/ps.go +++ b/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/command/stack/services.go b/command/stack/services.go index 1ca1c8c12..a46652df7 100644 --- a/command/stack/services.go +++ b/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 c4ea22972f6e50789eb8472ffda9974cf20f5b61 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 5 Dec 2016 16:14:08 -0500 Subject: [PATCH 339/563] Move pkg to cli/compose/convert Signed-off-by: Daniel Nephin --- command/stack/common.go | 8 +- command/stack/deploy.go | 12 +- command/stack/deploy_bundlefile.go | 10 +- command/stack/list.go | 6 +- compose/convert/compose.go | 88 ++++++++ compose/convert/compose_test.go | 85 ++++++++ compose/convert/service.go | 330 +++++++++++++++++++++++++++++ compose/convert/service_test.go | 193 +++++++++++++++++ compose/convert/volume.go | 116 ++++++++++ compose/convert/volume_test.go | 112 ++++++++++ 10 files changed, 942 insertions(+), 18 deletions(-) create mode 100644 compose/convert/compose.go create mode 100644 compose/convert/compose_test.go create mode 100644 compose/convert/service.go create mode 100644 compose/convert/service_test.go create mode 100644 compose/convert/volume.go create mode 100644 compose/convert/volume_test.go diff --git a/command/stack/common.go b/command/stack/common.go index c3a43f2cd..5c4996d66 100644 --- a/command/stack/common.go +++ b/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/command/stack/deploy.go b/command/stack/deploy.go index 957f92f29..32ebd62d3 100644 --- a/command/stack/deploy.go +++ b/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/command/stack/deploy_bundlefile.go b/command/stack/deploy_bundlefile.go index f9a416238..5a178c4ab 100644 --- a/command/stack/deploy_bundlefile.go +++ b/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/command/stack/list.go b/command/stack/list.go index 52e593316..9b6c645e2 100644 --- a/command/stack/list.go +++ b/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/compose/convert/compose.go b/compose/convert/compose.go new file mode 100644 index 000000000..e0684482b --- /dev/null +++ b/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/compose/convert/compose_test.go b/compose/convert/compose_test.go new file mode 100644 index 000000000..8f8e8ea6d --- /dev/null +++ b/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/compose/convert/service.go b/compose/convert/service.go new file mode 100644 index 000000000..458b518a4 --- /dev/null +++ b/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/compose/convert/service_test.go b/compose/convert/service_test.go new file mode 100644 index 000000000..a6884917d --- /dev/null +++ b/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/compose/convert/volume.go b/compose/convert/volume.go new file mode 100644 index 000000000..4eb578820 --- /dev/null +++ b/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/compose/convert/volume_test.go b/compose/convert/volume_test.go new file mode 100644 index 000000000..5e9c042b5 --- /dev/null +++ b/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 e4102ce61e8727296c0acc75531088064173de8c Mon Sep 17 00:00:00 2001 From: Ying Li Date: Thu, 15 Dec 2016 18:36:37 -0800 Subject: [PATCH 340/563] 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 --- command/swarm/unlock.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/command/swarm/unlock.go b/command/swarm/unlock.go index 048fb56e3..abb9e89fe 100644 --- a/command/swarm/unlock.go +++ b/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 c6b3fcbe3290c79689a2c941273982d79b8fc5a1 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Fri, 16 Dec 2016 12:17:39 -0800 Subject: [PATCH 341/563] 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 --- command/container/stats.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/command/container/stats.go b/command/container/stats.go index 12d5c6852..ebbd36e7e 100644 --- a/command/container/stats.go +++ b/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 bc4590fd7d5cc7745e66def87895b2776ef4876e Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sun, 18 Dec 2016 16:50:32 +0100 Subject: [PATCH 342/563] 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 --- compose/convert/volume.go | 10 +++++++++- compose/convert/volume_test.go | 12 ++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/compose/convert/volume.go b/compose/convert/volume.go index 4eb578820..027774bce 100644 --- a/compose/convert/volume.go +++ b/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/compose/convert/volume_test.go b/compose/convert/volume_test.go index 5e9c042b5..3ca6ab4a5 100644 --- a/compose/convert/volume_test.go +++ b/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 1f57f0707041595f1eeec20ee5836d6873d6faae Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 19 Dec 2016 01:50:08 +0100 Subject: [PATCH 343/563] 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 --- compose/convert/volume.go | 8 ++++++-- compose/convert/volume_test.go | 9 +++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/compose/convert/volume.go b/compose/convert/volume.go index 027774bce..3a7504106 100644 --- a/compose/convert/volume.go +++ b/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/compose/convert/volume_test.go b/compose/convert/volume_test.go index 3ca6ab4a5..bcbfb08b9 100644 --- a/compose/convert/volume_test.go +++ b/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 8246c4949806912961bd3b18a7d9a83ac7959175 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 22 Nov 2016 14:51:22 +0100 Subject: [PATCH 344/563] 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 --- command/container/logs.go | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/command/container/logs.go b/command/container/logs.go index 3a37cedf4..9f1d9f90d 100644 --- a/command/container/logs.go +++ b/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 476adcfd20c0dabfc85c6cbc3abc665d7e80427d Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Fri, 16 Dec 2016 11:19:05 -0800 Subject: [PATCH 345/563] 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 --- command/image/pull.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/image/pull.go b/command/image/pull.go index 13de492f9..24933fe84 100644 --- a/command/image/pull.go +++ b/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 23ab849f06f07c7efae6dd8c169070a81b9e40f0 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Thu, 15 Dec 2016 06:12:33 -0800 Subject: [PATCH 346/563] 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 --- command/service/opts.go | 4 ++-- command/service/opts_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/command/service/opts.go b/command/service/opts.go index c7518e597..cbe544aac 100644 --- a/command/service/opts.go +++ b/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/command/service/opts_test.go b/command/service/opts_test.go index aa2d999dc..78b956ad6 100644 --- a/command/service/opts_test.go +++ b/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 3c8d009c7a8504ab552606cf2158e56fbaf7b27a Mon Sep 17 00:00:00 2001 From: allencloud Date: Mon, 19 Dec 2016 14:45:48 +0800 Subject: [PATCH 347/563] change minor mistake of spelling Signed-off-by: allencloud --- command/registry/logout.go | 2 +- command/system/info.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/command/registry/logout.go b/command/registry/logout.go index 877e60e8c..f1f397fa0 100644 --- a/command/registry/logout.go +++ b/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/command/system/info.go b/command/system/info.go index e0b876737..973ee1824 100644 --- a/command/system/info.go +++ b/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 1e7c22c80a75b735959c75e86e62bacb9e441a6e Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Tue, 20 Dec 2016 19:14:41 +0800 Subject: [PATCH 348/563] Change tls to TLS Signed-off-by: yuexiao-wang --- flags/common.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flags/common.go b/flags/common.go index 690e8da4b..490c2922f 100644 --- a/flags/common.go +++ b/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 606a16a07d6d3fec886253e24a17ed1e7de366c4 Mon Sep 17 00:00:00 2001 From: Misty Stanley-Jones Date: Tue, 20 Dec 2016 11:47:54 -0800 Subject: [PATCH 349/563] Clarify what docker diff shows Signed-off-by: Misty Stanley-Jones --- command/container/diff.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/container/diff.go b/command/container/diff.go index 12d659101..168af7417 100644 --- a/command/container/diff.go +++ b/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 86a07d3fec4f8e39c50d97e3b89d05c5dcdb0a95 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 7 Dec 2016 07:38:18 -0800 Subject: [PATCH 350/563] 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 --- command/system/inspect.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/command/system/inspect.go b/command/system/inspect.go index cb5a1213a..c86e858a2 100644 --- a/command/system/inspect.go +++ b/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 d04375bd4aa85794ef65737525da76ab84b8c8de Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Fri, 29 Jul 2016 08:20:03 -0700 Subject: [PATCH 351/563] 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 --- command/service/ps.go | 56 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 9 deletions(-) diff --git a/command/service/ps.go b/command/service/ps.go index cf94ad737..12b25bf4f 100644 --- a/command/service/ps.go +++ b/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 bf3250ae0ab6e54d998f94386c25e59a32ad87f1 Mon Sep 17 00:00:00 2001 From: Anusha Ragunathan Date: Tue, 20 Dec 2016 08:26:58 -0800 Subject: [PATCH 352/563] 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 --- command/plugin/disable.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/command/plugin/disable.go b/command/plugin/disable.go index 9089a3cf6..5399e61f1 100644 --- a/command/plugin/disable.go +++ b/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 2825296deb853f09785952796a9e0edb5092089e Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Mon, 12 Dec 2016 15:05:53 -0800 Subject: [PATCH 353/563] 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 --- command/plugin/create.go | 4 +-- command/plugin/disable.go | 14 +------- command/plugin/enable.go | 15 +-------- command/plugin/install.go | 70 ++++++++++++++++++++++++++++++--------- command/plugin/list.go | 4 +-- command/plugin/push.go | 8 ++++- command/plugin/remove.go | 16 +-------- command/plugin/set.go | 20 +---------- 8 files changed, 70 insertions(+), 81 deletions(-) diff --git a/command/plugin/create.go b/command/plugin/create.go index e0041c1b8..2aab1e9e4 100644 --- a/command/plugin/create.go +++ b/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/command/plugin/disable.go b/command/plugin/disable.go index 5399e61f1..c3d36e20a 100644 --- a/command/plugin/disable.go +++ b/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/command/plugin/enable.go b/command/plugin/enable.go index 9201e38e1..77762f402 100644 --- a/command/plugin/enable.go +++ b/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/command/plugin/install.go b/command/plugin/install.go index eae018367..71bdeeff2 100644 --- a/command/plugin/install.go +++ b/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/command/plugin/list.go b/command/plugin/list.go index 4f800d7ec..8fd16dae3 100644 --- a/command/plugin/list.go +++ b/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/command/plugin/push.go b/command/plugin/push.go index add4a2b0a..667379cdd 100644 --- a/command/plugin/push.go +++ b/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/command/plugin/remove.go b/command/plugin/remove.go index 7a51dce06..9f3aba9a0 100644 --- a/command/plugin/remove.go +++ b/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/command/plugin/set.go b/command/plugin/set.go index 5660523ed..52b09fb50 100644 --- a/command/plugin/set.go +++ b/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 672687938244babd42941ea0630f33950566519f Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 23 Dec 2016 20:09:12 +0100 Subject: [PATCH 354/563] =?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 --- command/container/create.go | 9 +- command/container/exec.go | 3 +- command/container/opts.go | 899 +++++++++++++++++++++++++ command/container/opts_test.go | 857 +++++++++++++++++++++++ command/container/run.go | 11 +- command/container/testdata/utf16.env | Bin 0 -> 54 bytes command/container/testdata/utf16be.env | Bin 0 -> 54 bytes command/container/testdata/utf8.env | 3 + command/container/testdata/valid.env | 1 + command/container/testdata/valid.label | 1 + command/image/build.go | 11 +- command/network/connect.go | 3 +- command/network/create.go | 2 +- command/secret/create.go | 2 +- command/service/opts.go | 10 +- command/volume/create.go | 5 +- 16 files changed, 1786 insertions(+), 31 deletions(-) create mode 100644 command/container/opts.go create mode 100644 command/container/opts_test.go create mode 100755 command/container/testdata/utf16.env create mode 100755 command/container/testdata/utf16be.env create mode 100755 command/container/testdata/utf8.env create mode 100644 command/container/testdata/valid.env create mode 100644 command/container/testdata/valid.label diff --git a/command/container/create.go b/command/container/create.go index 7dc644d28..804ef9c48 100644 --- a/command/container/create.go +++ b/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/command/container/exec.go b/command/container/exec.go index f0381494e..ca47e59af 100644 --- a/command/container/exec.go +++ b/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/command/container/opts.go b/command/container/opts.go new file mode 100644 index 000000000..0f41dd507 --- /dev/null +++ b/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/command/container/opts_test.go b/command/container/opts_test.go new file mode 100644 index 000000000..d02a0f7bf --- /dev/null +++ b/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/command/container/run.go b/command/container/run.go index 0fad93e68..f106a7e3b 100644 --- a/command/container/run.go +++ b/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/command/container/testdata/utf16.env b/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 355/563] 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 --- command/cli.go | 10 +- config/config.go | 120 ++++ config/config_test.go | 621 ++++++++++++++++++ config/configfile/file.go | 183 ++++++ config/configfile/file_test.go | 27 + config/credentials/credentials.go | 17 + config/credentials/default_store.go | 22 + config/credentials/default_store_darwin.go | 3 + config/credentials/default_store_linux.go | 3 + .../credentials/default_store_unsupported.go | 5 + config/credentials/default_store_windows.go | 3 + config/credentials/file_store.go | 53 ++ config/credentials/file_store_test.go | 139 ++++ config/credentials/native_store.go | 144 ++++ config/credentials/native_store_test.go | 355 ++++++++++ flags/common.go | 4 +- trust/trust.go | 6 +- 17 files changed, 1705 insertions(+), 10 deletions(-) create mode 100644 config/config.go create mode 100644 config/config_test.go create mode 100644 config/configfile/file.go create mode 100644 config/configfile/file_test.go create mode 100644 config/credentials/credentials.go create mode 100644 config/credentials/default_store.go create mode 100644 config/credentials/default_store_darwin.go create mode 100644 config/credentials/default_store_linux.go create mode 100644 config/credentials/default_store_unsupported.go create mode 100644 config/credentials/default_store_windows.go create mode 100644 config/credentials/file_store.go create mode 100644 config/credentials/file_store_test.go create mode 100644 config/credentials/native_store.go create mode 100644 config/credentials/native_store_test.go diff --git a/command/cli.go b/command/cli.go index 6d1dd7472..c287ebcf7 100644 --- a/command/cli.go +++ b/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/config/config.go b/config/config.go new file mode 100644 index 000000000..ab0fa5451 --- /dev/null +++ b/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/config/config_test.go b/config/config_test.go new file mode 100644 index 000000000..195473bb8 --- /dev/null +++ b/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/config/configfile/file.go b/config/configfile/file.go new file mode 100644 index 000000000..39097133a --- /dev/null +++ b/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/config/configfile/file_test.go b/config/configfile/file_test.go new file mode 100644 index 000000000..435797f68 --- /dev/null +++ b/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/config/credentials/credentials.go b/config/credentials/credentials.go new file mode 100644 index 000000000..ca874cac5 --- /dev/null +++ b/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/config/credentials/default_store.go b/config/credentials/default_store.go new file mode 100644 index 000000000..263a4ea87 --- /dev/null +++ b/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/config/credentials/default_store_darwin.go b/config/credentials/default_store_darwin.go new file mode 100644 index 000000000..63e8ed401 --- /dev/null +++ b/config/credentials/default_store_darwin.go @@ -0,0 +1,3 @@ +package credentials + +const defaultCredentialsStore = "osxkeychain" diff --git a/config/credentials/default_store_linux.go b/config/credentials/default_store_linux.go new file mode 100644 index 000000000..864c540f6 --- /dev/null +++ b/config/credentials/default_store_linux.go @@ -0,0 +1,3 @@ +package credentials + +const defaultCredentialsStore = "secretservice" diff --git a/config/credentials/default_store_unsupported.go b/config/credentials/default_store_unsupported.go new file mode 100644 index 000000000..519ef53dc --- /dev/null +++ b/config/credentials/default_store_unsupported.go @@ -0,0 +1,5 @@ +// +build !windows,!darwin,!linux + +package credentials + +const defaultCredentialsStore = "" diff --git a/config/credentials/default_store_windows.go b/config/credentials/default_store_windows.go new file mode 100644 index 000000000..fb6a9745c --- /dev/null +++ b/config/credentials/default_store_windows.go @@ -0,0 +1,3 @@ +package credentials + +const defaultCredentialsStore = "wincred" diff --git a/config/credentials/file_store.go b/config/credentials/file_store.go new file mode 100644 index 000000000..3cab4a448 --- /dev/null +++ b/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/config/credentials/file_store_test.go b/config/credentials/file_store_test.go new file mode 100644 index 000000000..b6bfaa060 --- /dev/null +++ b/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/config/credentials/native_store.go b/config/credentials/native_store.go new file mode 100644 index 000000000..9e0ab7f0f --- /dev/null +++ b/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/config/credentials/native_store_test.go b/config/credentials/native_store_test.go new file mode 100644 index 000000000..7664faf9e --- /dev/null +++ b/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/flags/common.go b/flags/common.go index 490c2922f..9d3245c99 100644 --- a/flags/common.go +++ b/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/trust/trust.go b/trust/trust.go index 0f3482f2d..495bfa344 100644 --- a/trust/trust.go +++ b/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 f459796896f5e2074486540ae34cebcd015c6d4b Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sun, 25 Dec 2016 01:05:37 +0100 Subject: [PATCH 356/563] 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 --- command/container/attach.go | 9 ++++----- command/container/cmd.go | 3 +-- command/container/commit.go | 3 +-- command/container/cp.go | 14 +++++++------- command/container/create.go | 24 ++++++++++++------------ command/container/diff.go | 8 ++++---- command/container/exec.go | 7 +++---- command/container/export.go | 3 +-- command/container/inspect.go | 3 +-- command/container/kill.go | 8 ++++---- command/container/list.go | 3 +-- command/container/logs.go | 3 +-- command/container/pause.go | 10 +++++----- command/container/port.go | 3 +-- command/container/prune.go | 3 +-- command/container/rename.go | 8 ++++---- command/container/restart.go | 10 +++++----- command/container/rm.go | 16 ++++++++-------- command/container/run.go | 21 ++++++++++----------- command/container/start.go | 18 +++++++++--------- command/container/stats.go | 6 +++--- command/container/stop.go | 10 +++++----- command/container/top.go | 3 +-- command/container/unpause.go | 10 +++++----- command/container/update.go | 12 ++++++------ command/container/utils.go | 3 +-- command/container/wait.go | 10 +++++----- 27 files changed, 109 insertions(+), 122 deletions(-) diff --git a/command/container/attach.go b/command/container/attach.go index 31bb10934..073914dc3 100644 --- a/command/container/attach.go +++ b/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/command/container/cmd.go b/command/container/cmd.go index 3e9b4880a..b78411e0a 100644 --- a/command/container/cmd.go +++ b/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/command/container/commit.go b/command/container/commit.go index cf8d0102a..8f67d96d8 100644 --- a/command/container/commit.go +++ b/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/command/container/cp.go b/command/container/cp.go index 17ab2accf..8df850b36 100644 --- a/command/container/cp.go +++ b/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/command/container/create.go b/command/container/create.go index 7dc644d28..bc4fde571 100644 --- a/command/container/create.go +++ b/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/command/container/diff.go b/command/container/diff.go index 168af7417..81260b05b 100644 --- a/command/container/diff.go +++ b/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/command/container/exec.go b/command/container/exec.go index f0381494e..2253d44d5 100644 --- a/command/container/exec.go +++ b/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/command/container/export.go b/command/container/export.go index 8fa2e5d77..42f90bbaa 100644 --- a/command/container/export.go +++ b/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/command/container/inspect.go b/command/container/inspect.go index 08a8d244d..d08b38dc9 100644 --- a/command/container/inspect.go +++ b/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/command/container/kill.go b/command/container/kill.go index 6da91a40e..5c7f7ba14 100644 --- a/command/container/kill.go +++ b/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/command/container/list.go b/command/container/list.go index 5104e9b6c..451c531a8 100644 --- a/command/container/list.go +++ b/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/command/container/logs.go b/command/container/logs.go index 9f1d9f90d..2e1ce5205 100644 --- a/command/container/logs.go +++ b/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/command/container/pause.go b/command/container/pause.go index 6817cf60e..7d42ca571 100644 --- a/command/container/pause.go +++ b/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/command/container/port.go b/command/container/port.go index ea1529014..dd1a6b245 100644 --- a/command/container/port.go +++ b/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/command/container/prune.go b/command/container/prune.go index 064f4c08e..0aad66e6e 100644 --- a/command/container/prune.go +++ b/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/command/container/rename.go b/command/container/rename.go index 346fb7b3b..a24711ad3 100644 --- a/command/container/rename.go +++ b/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/command/container/restart.go b/command/container/restart.go index fc3ba93c8..0a3dd9218 100644 --- a/command/container/restart.go +++ b/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/command/container/rm.go b/command/container/rm.go index 60724f194..c02533d78 100644 --- a/command/container/rm.go +++ b/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/command/container/run.go b/command/container/run.go index 0fad93e68..2bfc49f28 100644 --- a/command/container/run.go +++ b/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/command/container/start.go b/command/container/start.go index 3521a4194..f5d8ca0bc 100644 --- a/command/container/start.go +++ b/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/command/container/stats.go b/command/container/stats.go index ebbd36e7e..593db27b2 100644 --- a/command/container/stats.go +++ b/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/command/container/stop.go b/command/container/stop.go index c68ede536..48fd63a9f 100644 --- a/command/container/stop.go +++ b/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/command/container/top.go b/command/container/top.go index 160153ba7..4a6d3ed5c 100644 --- a/command/container/top.go +++ b/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/command/container/unpause.go b/command/container/unpause.go index c4d8d4841..5f342da0d 100644 --- a/command/container/unpause.go +++ b/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/command/container/update.go b/command/container/update.go index 75765856c..6a7cc820e 100644 --- a/command/container/update.go +++ b/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/command/container/utils.go b/command/container/utils.go index 6bef92463..e4664b745 100644 --- a/command/container/utils.go +++ b/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/command/container/wait.go b/command/container/wait.go index 19ccf7ac2..d8dce6ef1 100644 --- a/command/container/wait.go +++ b/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 3e7dca79007f7e51d7794b589261e94d8d0742b2 Mon Sep 17 00:00:00 2001 From: allencloud Date: Mon, 21 Nov 2016 18:22:22 +0800 Subject: [PATCH 357/563] split function out of command description scope Signed-off-by: allencloud --- command/swarm/join_token.go | 129 ++++++++++++++++++++---------------- command/swarm/unlock.go | 66 ++++++++++-------- command/swarm/unlock_key.go | 100 +++++++++++++++------------- 3 files changed, 163 insertions(+), 132 deletions(-) diff --git a/command/swarm/join_token.go b/command/swarm/join_token.go index 3a17a8020..d800b769b 100644 --- a/command/swarm/join_token.go +++ b/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/command/swarm/unlock.go b/command/swarm/unlock.go index abb9e89fe..f7d418760 100644 --- a/command/swarm/unlock.go +++ b/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/command/swarm/unlock_key.go b/command/swarm/unlock_key.go index 96450f55b..e571e6645 100644 --- a/command/swarm/unlock_key.go +++ b/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 65be5677bd59ccedc128d0fc19e78c428cf68191 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Mon, 26 Dec 2016 13:47:43 -0800 Subject: [PATCH 358/563] 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 --- command/stack/ps.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/command/stack/ps.go b/command/stack/ps.go index 497fb97b5..7bbcf5420 100644 --- a/command/stack/ps.go +++ b/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 bcc61e1300ae4cc12ea8a377efafec59aed4ea9a Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Thu, 22 Dec 2016 11:44:09 -0800 Subject: [PATCH 359/563] Define PushResult in api types Signed-off-by: Tonis Tiigi --- command/image/trust.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/command/image/trust.go b/command/image/trust.go index f32c30195..192bc047c 100644 --- a/command/image/trust.go +++ b/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 c41bfce39acce23c7268144c22bb776d9c9c38b1 Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Thu, 22 Dec 2016 13:25:02 -0800 Subject: [PATCH 360/563] Move builder cli helper functions to own pkg Signed-off-by: Tonis Tiigi --- command/image/build.go | 14 +- command/image/build/context.go | 265 +++++++++++++++++ command/image/build/context_test.go | 383 +++++++++++++++++++++++++ command/image/build/context_unix.go | 11 + command/image/build/context_windows.go | 17 ++ 5 files changed, 683 insertions(+), 7 deletions(-) create mode 100644 command/image/build/context.go create mode 100644 command/image/build/context_test.go create mode 100644 command/image/build/context_unix.go create mode 100644 command/image/build/context_windows.go diff --git a/command/image/build.go b/command/image/build.go index e3e7ff2b0..f194659e0 100644 --- a/command/image/build.go +++ b/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/command/image/build/context.go b/command/image/build/context.go new file mode 100644 index 000000000..86157c359 --- /dev/null +++ b/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/command/image/build/context_test.go b/command/image/build/context_test.go new file mode 100644 index 000000000..afa04a4fc --- /dev/null +++ b/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/command/image/build/context_unix.go b/command/image/build/context_unix.go new file mode 100644 index 000000000..cb2634f07 --- /dev/null +++ b/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/command/image/build/context_windows.go b/command/image/build/context_windows.go new file mode 100644 index 000000000..c577cfa7b --- /dev/null +++ b/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 4e68d651b35689f1d583b5c7a95e61d5af2d3a34 Mon Sep 17 00:00:00 2001 From: allencloud Date: Fri, 23 Dec 2016 20:48:25 +0800 Subject: [PATCH 361/563] fix nits in comments Signed-off-by: allencloud --- command/swarm/unlock.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/swarm/unlock.go b/command/swarm/unlock.go index f7d418760..aa752e214 100644 --- a/command/swarm/unlock.go +++ b/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 fcaa89f296d768153418301ffff218c0501a27b3 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Tue, 27 Dec 2016 12:51:00 -0800 Subject: [PATCH 362/563] Support for docker content trust for plugins Add integration test for docker content trust Signed-off-by: Derek McGowan (github: dmcgowan) --- command/container/create.go | 2 +- command/image/build.go | 2 +- command/image/trust.go | 23 ++++++++++++---- command/plugin/install.go | 53 +++++++++++++++++++++++++++++++++++-- command/plugin/push.go | 12 +++++++++ trust/trust.go | 13 ++++++++- 6 files changed, 95 insertions(+), 10 deletions(-) diff --git a/command/container/create.go b/command/container/create.go index 7dc644d28..d5e63bd9e 100644 --- a/command/container/create.go +++ b/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/command/image/build.go b/command/image/build.go index e3e7ff2b0..0c88af5fc 100644 --- a/command/image/build.go +++ b/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/command/image/trust.go b/command/image/trust.go index f32c30195..5136a2215 100644 --- a/command/image/trust.go +++ b/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/command/plugin/install.go b/command/plugin/install.go index 71bdeeff2..a64dc2525 100644 --- a/command/plugin/install.go +++ b/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/command/plugin/push.go b/command/plugin/push.go index 667379cdd..b0766307f 100644 --- a/command/plugin/push.go +++ b/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/trust/trust.go b/trust/trust.go index 0f3482f2d..51914f74b 100644 --- a/trust/trust.go +++ b/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 52c01570361c5f2dc1bb03623baa31f42dc912e3 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 20 Dec 2016 16:26:49 -0500 Subject: [PATCH 363/563] Replace vendor of aanand/compose-file with a local copy. Add go-bindata for including the schema. Signed-off-by: Daniel Nephin --- command/stack/deploy.go | 4 +- compose/convert/compose.go | 2 +- compose/convert/compose_test.go | 2 +- compose/convert/service.go | 2 +- compose/convert/service_test.go | 2 +- compose/convert/volume.go | 2 +- compose/convert/volume_test.go | 2 +- compose/interpolation/interpolation.go | 90 +++ compose/interpolation/interpolation_test.go | 59 ++ compose/loader/example1.env | 8 + compose/loader/example2.env | 1 + compose/loader/full-example.yml | 287 +++++++ compose/loader/loader.go | 611 +++++++++++++++ compose/loader/loader_test.go | 782 ++++++++++++++++++++ compose/schema/bindata.go | 237 ++++++ compose/schema/data/config_schema_v3.0.json | 379 ++++++++++ compose/schema/schema.go | 113 +++ compose/schema/schema_test.go | 35 + compose/template/template.go | 100 +++ compose/template/template_test.go | 83 +++ compose/types/types.go | 232 ++++++ 21 files changed, 3025 insertions(+), 8 deletions(-) create mode 100644 compose/interpolation/interpolation.go create mode 100644 compose/interpolation/interpolation_test.go create mode 100644 compose/loader/example1.env create mode 100644 compose/loader/example2.env create mode 100644 compose/loader/full-example.yml create mode 100644 compose/loader/loader.go create mode 100644 compose/loader/loader_test.go create mode 100644 compose/schema/bindata.go create mode 100644 compose/schema/data/config_schema_v3.0.json create mode 100644 compose/schema/schema.go create mode 100644 compose/schema/schema_test.go create mode 100644 compose/template/template.go create mode 100644 compose/template/template_test.go create mode 100644 compose/types/types.go diff --git a/command/stack/deploy.go b/command/stack/deploy.go index 32ebd62d3..f4730db55 100644 --- a/command/stack/deploy.go +++ b/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/compose/convert/compose.go b/compose/convert/compose.go index e0684482b..7c410844c 100644 --- a/compose/convert/compose.go +++ b/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/compose/convert/compose_test.go b/compose/convert/compose_test.go index 8f8e8ea6d..27a67047d 100644 --- a/compose/convert/compose_test.go +++ b/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/compose/convert/service.go b/compose/convert/service.go index 458b518a4..2a8ed8288 100644 --- a/compose/convert/service.go +++ b/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/compose/convert/service_test.go b/compose/convert/service_test.go index a6884917d..45da76432 100644 --- a/compose/convert/service_test.go +++ b/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/compose/convert/volume.go b/compose/convert/volume.go index 3a7504106..24442d4dc 100644 --- a/compose/convert/volume.go +++ b/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/compose/convert/volume_test.go b/compose/convert/volume_test.go index bcbfb08b9..1132136b2 100644 --- a/compose/convert/volume_test.go +++ b/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/compose/interpolation/interpolation.go b/compose/interpolation/interpolation.go new file mode 100644 index 000000000..734f28ec9 --- /dev/null +++ b/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/compose/interpolation/interpolation_test.go b/compose/interpolation/interpolation_test.go new file mode 100644 index 000000000..c3921701b --- /dev/null +++ b/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/compose/loader/example1.env b/compose/loader/example1.env new file mode 100644 index 000000000..3e7a05961 --- /dev/null +++ b/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/compose/loader/example2.env b/compose/loader/example2.env new file mode 100644 index 000000000..0920d5ab0 --- /dev/null +++ b/compose/loader/example2.env @@ -0,0 +1 @@ +BAR=2 diff --git a/compose/loader/full-example.yml b/compose/loader/full-example.yml new file mode 100644 index 000000000..fb5686a38 --- /dev/null +++ b/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/compose/loader/loader.go b/compose/loader/loader.go new file mode 100644 index 000000000..9e46b9759 --- /dev/null +++ b/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/compose/loader/loader_test.go b/compose/loader/loader_test.go new file mode 100644 index 000000000..e15be7c54 --- /dev/null +++ b/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/compose/schema/bindata.go b/compose/schema/bindata.go new file mode 100644 index 000000000..2acc7d29f --- /dev/null +++ b/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/compose/schema/data/config_schema_v3.0.json b/compose/schema/data/config_schema_v3.0.json new file mode 100644 index 000000000..520e57d5e --- /dev/null +++ b/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/compose/schema/schema.go b/compose/schema/schema.go new file mode 100644 index 000000000..6366cab48 --- /dev/null +++ b/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/compose/schema/schema_test.go b/compose/schema/schema_test.go new file mode 100644 index 000000000..be98f807d --- /dev/null +++ b/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/compose/template/template.go b/compose/template/template.go new file mode 100644 index 000000000..28495baf5 --- /dev/null +++ b/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/compose/template/template_test.go b/compose/template/template_test.go new file mode 100644 index 000000000..6b81bf0a3 --- /dev/null +++ b/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/compose/types/types.go b/compose/types/types.go new file mode 100644 index 000000000..45923b346 --- /dev/null +++ b/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 48930c8bbf8ec4d6a022c1b204dd326dae9089bc Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 22 Sep 2016 14:11:08 -0400 Subject: [PATCH 364/563] Read long description from a file. Signed-off-by: Daniel Nephin --- command/volume/cmd.go | 19 ------------------- command/volume/create.go | 40 --------------------------------------- command/volume/inspect.go | 9 --------- command/volume/list.go | 17 ----------------- 4 files changed, 85 deletions(-) diff --git a/command/volume/cmd.go b/command/volume/cmd.go index 40862f29d..2bc768775 100644 --- a/command/volume/cmd.go +++ b/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/command/volume/create.go b/command/volume/create.go index 7b2a7e331..de45ce67e 100644 --- a/command/volume/create.go +++ b/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/command/volume/inspect.go b/command/volume/inspect.go index 5eb8ad251..f58b927ac 100644 --- a/command/volume/inspect.go +++ b/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/command/volume/list.go b/command/volume/list.go index d76006a1b..0de83aea4 100644 --- a/command/volume/list.go +++ b/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 5860dd5f80b842fe026542118eb565c20189a7d2 Mon Sep 17 00:00:00 2001 From: Xianglin Gao Date: Wed, 28 Dec 2016 16:28:32 +0800 Subject: [PATCH 365/563] exit collect when we get EOF Signed-off-by: Xianglin Gao --- command/container/stats_helpers.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/command/container/stats_helpers.go b/command/container/stats_helpers.go index 4b57e3fe0..4a58134a4 100644 --- a/command/container/stats_helpers.go +++ b/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 edeb5b6e0d3ac2bc6db33735a503cecccc2828de Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Fri, 30 Dec 2016 18:15:53 +0100 Subject: [PATCH 366/563] 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 --- command/service/parse.go | 2 +- command/service/update.go | 23 +++++++------- command/service/update_test.go | 57 ++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 12 deletions(-) diff --git a/command/service/parse.go b/command/service/parse.go index ff3249e58..6af7e3bb8 100644 --- a/command/service/parse.go +++ b/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/command/service/update.go b/command/service/update.go index 514b1bd51..6d13927da 100644 --- a/command/service/update.go +++ b/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/command/service/update_test.go b/command/service/update_test.go index 08fe24876..a6df6b985 100644 --- a/command/service/update_test.go +++ b/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 87fea846fc1223d9594c2e4e68d9cb9e2b1e72d0 Mon Sep 17 00:00:00 2001 From: Harald Albers Date: Sat, 31 Dec 2016 09:55:04 -0800 Subject: [PATCH 367/563] Fix usage message of `plugin inspect` Signed-off-by: Harald Albers --- command/plugin/inspect.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/plugin/inspect.go b/command/plugin/inspect.go index 46ec7b229..c2c7a0d6b 100644 --- a/command/plugin/inspect.go +++ b/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 b5bc69238cfdf6414e8757fc37606e9b561fcfae Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 2 Jan 2017 11:19:33 +0100 Subject: [PATCH 368/563] 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 --- command/service/opts.go | 95 ----------------------------------------- 1 file changed, 95 deletions(-) diff --git a/command/service/opts.go b/command/service/opts.go index 78c27eae2..b794b07a3 100644 --- a/command/service/opts.go +++ b/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 fe181a18d5f818c15b0647f2a6d272480db71017 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 3 Jan 2017 15:58:41 -0500 Subject: [PATCH 369/563] Trim quotes from TLS flags. Signed-off-by: Daniel Nephin --- flags/common.go | 12 ++++++++---- flags/common_test.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 flags/common_test.go diff --git a/flags/common.go b/flags/common.go index 9d3245c99..af2fe0603 100644 --- a/flags/common.go +++ b/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/flags/common_test.go b/flags/common_test.go new file mode 100644 index 000000000..616d577f0 --- /dev/null +++ b/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 9651fbd1974797bbf9ce447b78d6309c857be9e4 Mon Sep 17 00:00:00 2001 From: John Howard Date: Tue, 3 Jan 2017 11:40:44 -0800 Subject: [PATCH 370/563] 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. --- command/image/build.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/image/build.go b/command/image/build.go index 1e4e8a267..5d6e61140 100644 --- a/command/image/build.go +++ b/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 567b5545401badbb18cdb6d692345372f9c3de84 Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Wed, 28 Dec 2016 19:34:32 +0800 Subject: [PATCH 371/563] keep network option consistent between network connect and run Signed-off-by: yuexiao-wang --- command/container/opts.go | 6 +++--- command/network/connect.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/command/container/opts.go b/command/container/opts.go index 0f41dd507..c5fc15216 100644 --- a/command/container/opts.go +++ b/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/command/network/connect.go b/command/network/connect.go index 113c6c03f..bc90ddaba 100644 --- a/command/network/connect.go +++ b/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 e2416af0136b72d369de53ff7928d7cc603d4f46 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 7 Dec 2016 14:02:13 -0800 Subject: [PATCH 372/563] 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 --- command/container/prune.go | 16 ++++++++++------ command/image/prune.go | 16 +++++++++------- command/network/prune.go | 16 ++++++++++------ command/prune/prune.go | 15 ++++++++------- command/system/prune.go | 21 ++++++++++++--------- 5 files changed, 49 insertions(+), 35 deletions(-) diff --git a/command/container/prune.go b/command/container/prune.go index 0aad66e6e..ca50e2e15 100644 --- a/command/container/prune.go +++ b/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/command/image/prune.go b/command/image/prune.go index 82c28fcf4..f17aed741 100644 --- a/command/image/prune.go +++ b/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/command/network/prune.go b/command/network/prune.go index 9f1979e6b..c5c535992 100644 --- a/command/network/prune.go +++ b/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/command/prune/prune.go b/command/prune/prune.go index a022487fd..6314718c6 100644 --- a/command/prune/prune.go +++ b/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/command/system/prune.go b/command/system/prune.go index 92dddbdca..46e4316f4 100644 --- a/command/system/prune.go +++ b/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 4e89a50a31b5956cc5aa99de47193efafc6d5989 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 4 Jan 2017 15:17:54 -0800 Subject: [PATCH 373/563] 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 --- command/formatter/network.go | 5 +++++ command/formatter/network_test.go | 19 +++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/command/formatter/network.go b/command/formatter/network.go index 7fbad7d2a..c29be412a 100644 --- a/command/formatter/network.go +++ b/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/command/formatter/network_test.go b/command/formatter/network_test.go index b40a534ee..e105afbdf 100644 --- a/command/formatter/network_test.go +++ b/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 62cbb25a176cc4f743127203f4663a99d78eace7 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Fri, 6 Jan 2017 12:06:02 -0800 Subject: [PATCH 374/563] remove -f on secret create and unify usage with other commands Signed-off-by: Victor Vieux --- command/secret/create.go | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/command/secret/create.go b/command/secret/create.go index 6967fb51e..a3248e5df 100644 --- a/command/secret/create.go +++ b/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 5d67ac20cbca18a852e2239cbdc9502e64167b68 Mon Sep 17 00:00:00 2001 From: Stephen J Day Date: Fri, 6 Jan 2017 17:23:18 -0800 Subject: [PATCH 375/563] *: 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 --- command/image/trust.go | 4 ++-- command/service/trust.go | 4 ++-- compose/schema/bindata.go | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/command/image/trust.go b/command/image/trust.go index 948e002bf..58e057439 100644 --- a/command/image/trust.go +++ b/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/command/service/trust.go b/command/service/trust.go index 052d49c32..15f8a708f 100644 --- a/command/service/trust.go +++ b/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/compose/schema/bindata.go b/compose/schema/bindata.go index 2acc7d29f..c3774130b 100644 --- a/compose/schema/bindata.go +++ b/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 6081f43bd1494377ff38415b09377c0fe469120b Mon Sep 17 00:00:00 2001 From: ttronicum Date: Sun, 25 Dec 2016 05:13:53 +0100 Subject: [PATCH 376/563] explain since format and give examples Signed-off-by: tronicum --- command/container/logs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/container/logs.go b/command/container/logs.go index 3a37cedf4..f15a64492 100644 --- a/command/container/logs.go +++ b/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 ee8f9e084af7aaf71db3b55e345475ab079fa923 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Sun, 25 Dec 2016 22:23:35 +0100 Subject: [PATCH 377/563] 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 --- command/cli.go | 10 +- command/node/client_test.go | 68 ++++++ command/node/demote.go | 4 +- command/node/demote_test.go | 88 +++++++ command/node/inspect.go | 4 +- command/node/inspect_test.go | 122 ++++++++++ command/node/list.go | 4 +- command/node/list_test.go | 101 ++++++++ command/node/opts.go | 36 --- command/node/promote.go | 4 +- command/node/promote_test.go | 88 +++++++ command/node/ps.go | 4 +- command/node/ps_test.go | 132 +++++++++++ command/node/remove.go | 4 +- 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 ++ command/node/testdata/node-ps.simple.golden | 2 + .../node/testdata/node-ps.with-errors.golden | 4 + command/node/update.go | 6 +- command/node/update_test.go | 172 ++++++++++++++ command/swarm/client_test.go | 84 +++++++ command/swarm/init.go | 6 +- command/swarm/init_test.go | 129 +++++++++++ command/swarm/join.go | 4 +- command/swarm/join_test.go | 102 +++++++++ command/swarm/join_token.go | 6 +- command/swarm/join_token_test.go | 215 ++++++++++++++++++ command/swarm/leave.go | 4 +- command/swarm/leave_test.go | 52 +++++ 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 + command/swarm/testdata/update-noargs.golden | 13 ++ command/swarm/unlock.go | 4 +- command/swarm/unlock_key.go | 9 +- command/swarm/unlock_key_test.go | 175 ++++++++++++++ command/swarm/unlock_test.go | 101 ++++++++ command/swarm/update.go | 14 +- command/swarm/update_test.go | 182 +++++++++++++++ command/task/print.go | 4 +- internal/test/builders/node.go | 117 ++++++++++ internal/test/builders/swarm.go | 39 ++++ internal/test/builders/task.go | 111 +++++++++ internal/test/cli.go | 48 ++++ 57 files changed, 2451 insertions(+), 78 deletions(-) create mode 100644 command/node/client_test.go create mode 100644 command/node/demote_test.go create mode 100644 command/node/inspect_test.go create mode 100644 command/node/list_test.go create mode 100644 command/node/promote_test.go create mode 100644 command/node/ps_test.go create mode 100644 command/node/remove_test.go create mode 100644 command/node/testdata/node-inspect-pretty.manager-leader.golden create mode 100644 command/node/testdata/node-inspect-pretty.manager.golden create mode 100644 command/node/testdata/node-inspect-pretty.simple.golden create mode 100644 command/node/testdata/node-ps.simple.golden create mode 100644 command/node/testdata/node-ps.with-errors.golden create mode 100644 command/node/update_test.go create mode 100644 command/swarm/client_test.go create mode 100644 command/swarm/init_test.go create mode 100644 command/swarm/join_test.go create mode 100644 command/swarm/join_token_test.go create mode 100644 command/swarm/leave_test.go create mode 100644 command/swarm/testdata/init-init-autolock.golden create mode 100644 command/swarm/testdata/init-init.golden create mode 100644 command/swarm/testdata/jointoken-manager-quiet.golden create mode 100644 command/swarm/testdata/jointoken-manager-rotate.golden create mode 100644 command/swarm/testdata/jointoken-manager.golden create mode 100644 command/swarm/testdata/jointoken-worker-quiet.golden create mode 100644 command/swarm/testdata/jointoken-worker.golden create mode 100644 command/swarm/testdata/unlockkeys-unlock-key-quiet.golden create mode 100644 command/swarm/testdata/unlockkeys-unlock-key-rotate-quiet.golden create mode 100644 command/swarm/testdata/unlockkeys-unlock-key-rotate.golden create mode 100644 command/swarm/testdata/unlockkeys-unlock-key.golden create mode 100644 command/swarm/testdata/update-all-flags-quiet.golden create mode 100644 command/swarm/testdata/update-autolock-unlock-key.golden create mode 100644 command/swarm/testdata/update-noargs.golden create mode 100644 command/swarm/unlock_key_test.go create mode 100644 command/swarm/unlock_test.go create mode 100644 command/swarm/update_test.go create mode 100644 internal/test/builders/node.go create mode 100644 internal/test/builders/swarm.go create mode 100644 internal/test/builders/task.go create mode 100644 internal/test/cli.go diff --git a/command/cli.go b/command/cli.go index c287ebcf7..bf9d55460 100644 --- a/command/cli.go +++ b/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/command/node/client_test.go b/command/node/client_test.go new file mode 100644 index 000000000..1f5cdc7ce --- /dev/null +++ b/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/command/node/demote.go b/command/node/demote.go index 33f86c649..72ed3ea63 100644 --- a/command/node/demote.go +++ b/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/command/node/demote_test.go b/command/node/demote_test.go new file mode 100644 index 000000000..3ba88f41c --- /dev/null +++ b/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/command/node/inspect.go b/command/node/inspect.go index fde70185f..97a271778 100644 --- a/command/node/inspect.go +++ b/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/command/node/inspect_test.go b/command/node/inspect_test.go new file mode 100644 index 000000000..91bd41e16 --- /dev/null +++ b/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/command/node/list.go b/command/node/list.go index 9cacdcf44..d166401ab 100644 --- a/command/node/list.go +++ b/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/command/node/list_test.go b/command/node/list_test.go new file mode 100644 index 000000000..237c4be9c --- /dev/null +++ b/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/command/node/opts.go b/command/node/opts.go index 7e6c55d48..0ad365f0c 100644 --- a/command/node/opts.go +++ b/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/command/node/promote.go b/command/node/promote.go index f47d783f4..94fff6400 100644 --- a/command/node/promote.go +++ b/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/command/node/promote_test.go b/command/node/promote_test.go new file mode 100644 index 000000000..ef4666321 --- /dev/null +++ b/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/command/node/ps.go b/command/node/ps.go index a034721d2..52ac36646 100644 --- a/command/node/ps.go +++ b/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/command/node/ps_test.go b/command/node/ps_test.go new file mode 100644 index 000000000..1a1022d21 --- /dev/null +++ b/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/command/node/remove.go b/command/node/remove.go index 19b4a9663..0e4963aca 100644 --- a/command/node/remove.go +++ b/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/command/node/remove_test.go b/command/node/remove_test.go new file mode 100644 index 000000000..54930a276 --- /dev/null +++ b/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/command/node/testdata/node-inspect-pretty.manager-leader.golden b/command/node/testdata/node-inspect-pretty.manager-leader.golden new file mode 100644 index 000000000..461fc46ea --- /dev/null +++ b/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/command/node/testdata/node-inspect-pretty.manager.golden b/command/node/testdata/node-inspect-pretty.manager.golden new file mode 100644 index 000000000..2c660188d --- /dev/null +++ b/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/command/node/testdata/node-inspect-pretty.simple.golden b/command/node/testdata/node-inspect-pretty.simple.golden new file mode 100644 index 000000000..e63bc1259 --- /dev/null +++ b/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/command/node/testdata/node-ps.simple.golden b/command/node/testdata/node-ps.simple.golden new file mode 100644 index 000000000..f9555d879 --- /dev/null +++ b/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/command/node/testdata/node-ps.with-errors.golden b/command/node/testdata/node-ps.with-errors.golden new file mode 100644 index 000000000..273b30fa1 --- /dev/null +++ b/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/command/node/update.go b/command/node/update.go index 65339e138..6ca2a7c1e 100644 --- a/command/node/update.go +++ b/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/command/node/update_test.go b/command/node/update_test.go new file mode 100644 index 000000000..439ba9443 --- /dev/null +++ b/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/command/swarm/client_test.go b/command/swarm/client_test.go new file mode 100644 index 000000000..1d42b9499 --- /dev/null +++ b/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/command/swarm/init.go b/command/swarm/init.go index 2550feeb4..e038ac62a 100644 --- a/command/swarm/init.go +++ b/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/command/swarm/init_test.go b/command/swarm/init_test.go new file mode 100644 index 000000000..13de1cd55 --- /dev/null +++ b/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/command/swarm/join.go b/command/swarm/join.go index 004313b4c..3ea1462df 100644 --- a/command/swarm/join.go +++ b/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/command/swarm/join_test.go b/command/swarm/join_test.go new file mode 100644 index 000000000..66dd6d66b --- /dev/null +++ b/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/command/swarm/join_token.go b/command/swarm/join_token.go index d800b769b..5c84c7a31 100644 --- a/command/swarm/join_token.go +++ b/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/command/swarm/join_token_test.go b/command/swarm/join_token_test.go new file mode 100644 index 000000000..624401641 --- /dev/null +++ b/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/command/swarm/leave.go b/command/swarm/leave.go index e2cfa0a04..128ed46d8 100644 --- a/command/swarm/leave.go +++ b/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/command/swarm/leave_test.go b/command/swarm/leave_test.go new file mode 100644 index 000000000..09b41b251 --- /dev/null +++ b/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/command/swarm/opts_test.go b/command/swarm/opts_test.go index 568dc8730..9a97e8bd2 100644 --- a/command/swarm/opts_test.go +++ b/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/command/swarm/testdata/init-init-autolock.golden b/command/swarm/testdata/init-init-autolock.golden new file mode 100644 index 000000000..cdd3c666b --- /dev/null +++ b/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/command/swarm/testdata/init-init.golden b/command/swarm/testdata/init-init.golden new file mode 100644 index 000000000..6e82be010 --- /dev/null +++ b/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/command/swarm/testdata/jointoken-manager-quiet.golden b/command/swarm/testdata/jointoken-manager-quiet.golden new file mode 100644 index 000000000..0c7cfc608 --- /dev/null +++ b/command/swarm/testdata/jointoken-manager-quiet.golden @@ -0,0 +1 @@ +manager-join-token diff --git a/command/swarm/testdata/jointoken-manager-rotate.golden b/command/swarm/testdata/jointoken-manager-rotate.golden new file mode 100644 index 000000000..7ee455bec --- /dev/null +++ b/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/command/swarm/testdata/jointoken-manager.golden b/command/swarm/testdata/jointoken-manager.golden new file mode 100644 index 000000000..d56527aa5 --- /dev/null +++ b/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/command/swarm/testdata/jointoken-worker-quiet.golden b/command/swarm/testdata/jointoken-worker-quiet.golden new file mode 100644 index 000000000..b445e191e --- /dev/null +++ b/command/swarm/testdata/jointoken-worker-quiet.golden @@ -0,0 +1 @@ +worker-join-token diff --git a/command/swarm/testdata/jointoken-worker.golden b/command/swarm/testdata/jointoken-worker.golden new file mode 100644 index 000000000..5d44f3dae --- /dev/null +++ b/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/command/swarm/testdata/unlockkeys-unlock-key-quiet.golden b/command/swarm/testdata/unlockkeys-unlock-key-quiet.golden new file mode 100644 index 000000000..ed53505e2 --- /dev/null +++ b/command/swarm/testdata/unlockkeys-unlock-key-quiet.golden @@ -0,0 +1 @@ +unlock-key diff --git a/command/swarm/testdata/unlockkeys-unlock-key-rotate-quiet.golden b/command/swarm/testdata/unlockkeys-unlock-key-rotate-quiet.golden new file mode 100644 index 000000000..ed53505e2 --- /dev/null +++ b/command/swarm/testdata/unlockkeys-unlock-key-rotate-quiet.golden @@ -0,0 +1 @@ +unlock-key diff --git a/command/swarm/testdata/unlockkeys-unlock-key-rotate.golden b/command/swarm/testdata/unlockkeys-unlock-key-rotate.golden new file mode 100644 index 000000000..89152b864 --- /dev/null +++ b/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/command/swarm/testdata/unlockkeys-unlock-key.golden b/command/swarm/testdata/unlockkeys-unlock-key.golden new file mode 100644 index 000000000..8316df478 --- /dev/null +++ b/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/command/swarm/testdata/update-all-flags-quiet.golden b/command/swarm/testdata/update-all-flags-quiet.golden new file mode 100644 index 000000000..3d195a258 --- /dev/null +++ b/command/swarm/testdata/update-all-flags-quiet.golden @@ -0,0 +1 @@ +Swarm updated. diff --git a/command/swarm/testdata/update-autolock-unlock-key.golden b/command/swarm/testdata/update-autolock-unlock-key.golden new file mode 100644 index 000000000..a077b9e16 --- /dev/null +++ b/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/command/swarm/testdata/update-noargs.golden b/command/swarm/testdata/update-noargs.golden new file mode 100644 index 000000000..381c0ccf1 --- /dev/null +++ b/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/command/swarm/unlock.go b/command/swarm/unlock.go index aa752e214..45dd6e79e 100644 --- a/command/swarm/unlock.go +++ b/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/command/swarm/unlock_key.go b/command/swarm/unlock_key.go index e571e6645..77c97d88e 100644 --- a/command/swarm/unlock_key.go +++ b/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/command/swarm/unlock_key_test.go b/command/swarm/unlock_key_test.go new file mode 100644 index 000000000..17a07d3fb --- /dev/null +++ b/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/command/swarm/unlock_test.go b/command/swarm/unlock_test.go new file mode 100644 index 000000000..abf858a28 --- /dev/null +++ b/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/command/swarm/update.go b/command/swarm/update.go index dbbd26872..1ccd268e7 100644 --- a/command/swarm/update.go +++ b/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/command/swarm/update_test.go b/command/swarm/update_test.go new file mode 100644 index 000000000..c8a2860a0 --- /dev/null +++ b/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/command/task/print.go b/command/task/print.go index 57c4e0c8c..60a2bca85 100644 --- a/command/task/print.go +++ b/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/internal/test/builders/node.go b/internal/test/builders/node.go new file mode 100644 index 000000000..63fdebba1 --- /dev/null +++ b/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/internal/test/builders/swarm.go b/internal/test/builders/swarm.go new file mode 100644 index 000000000..ab1a93062 --- /dev/null +++ b/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/internal/test/builders/task.go b/internal/test/builders/task.go new file mode 100644 index 000000000..688c62a3a --- /dev/null +++ b/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/internal/test/cli.go b/internal/test/cli.go new file mode 100644 index 000000000..06ab053e9 --- /dev/null +++ b/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 e8ad538d90190d81ecef623de92d089409f26fcf Mon Sep 17 00:00:00 2001 From: Dong Chen Date: Thu, 5 Jan 2017 11:21:22 -0800 Subject: [PATCH 378/563] add port PublishMode to service inspect --pretty output Signed-off-by: Dong Chen --- command/formatter/service.go | 1 + command/service/update.go | 6 +----- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/command/formatter/service.go b/command/formatter/service.go index 2690029ce..8242e1cb9 100644 --- a/command/formatter/service.go +++ b/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/command/service/update.go b/command/service/update.go index 514b1bd51..f1e41c5cd 100644 --- a/command/service/update.go +++ b/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 c2f0402f4d8b4f676931c9c670449f0629bc8ea7 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 9 Jan 2017 14:22:02 -0500 Subject: [PATCH 379/563] Fix parsing resources from compose file for stack deploy. Signed-off-by: Daniel Nephin --- compose/convert/service.go | 19 +++++++++++++------ compose/convert/service_test.go | 23 +++++++++++++++++++++++ 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/compose/convert/service.go b/compose/convert/service.go index 2a8ed8288..37f3ece40 100644 --- a/compose/convert/service.go +++ b/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/compose/convert/service_test.go b/compose/convert/service_test.go index 45da76432..2e614d730 100644 --- a/compose/convert/service_test.go +++ b/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 70643ad005145a10af049a5ffb705b682ae1454e Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Tue, 10 Jan 2017 09:57:36 +0100 Subject: [PATCH 380/563] 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 --- command/stack/deploy.go | 18 +++++++++++++++++- compose/convert/compose.go | 11 +++-------- compose/convert/compose_test.go | 7 ++++++- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/command/stack/deploy.go b/command/stack/deploy.go index f4730db55..306a583e1 100644 --- a/command/stack/deploy.go +++ b/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/compose/convert/compose.go b/compose/convert/compose.go index 7c410844c..70c1762a4 100644 --- a/compose/convert/compose.go +++ b/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/compose/convert/compose_test.go b/compose/convert/compose_test.go index 27a67047d..d88ac7f7c 100644 --- a/compose/convert/compose_test.go +++ b/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 ab2635ead050ea2f283da637ed5115d7cfb7f694 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 21 Dec 2016 18:06:16 -0800 Subject: [PATCH 381/563] 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 --- command/swarm/join.go | 21 ++++++++++++++++++--- command/swarm/opts.go | 1 + 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/command/swarm/join.go b/command/swarm/join.go index 3ea1462df..40fc5c192 100644 --- a/command/swarm/join.go +++ b/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/command/swarm/opts.go b/command/swarm/opts.go index 9db46dcf5..40f88a441 100644 --- a/command/swarm/opts.go +++ b/command/swarm/opts.go @@ -28,6 +28,7 @@ const ( flagSnapshotInterval = "snapshot-interval" flagLockKey = "lock-key" flagAutolock = "autolock" + flagAvailability = "availability" ) type swarmOptions struct { From 3c47987838fc272a493237b8e11301a99f4412ad Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 21 Dec 2016 18:13:31 -0800 Subject: [PATCH 382/563] 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 --- command/swarm/init.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/command/swarm/init.go b/command/swarm/init.go index e038ac62a..b79602267 100644 --- a/command/swarm/init.go +++ b/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 384611596b47e6c8f958dd3ca644b6d10db9a1db Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 11 Jan 2017 11:57:24 -0500 Subject: [PATCH 383/563] Improve the error message for extends in stack deploy. Signed-off-by: Daniel Nephin --- compose/types/types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose/types/types.go b/compose/types/types.go index 45923b346..5244bd116 100644 --- a/compose/types/types.go +++ b/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 ce209504224356839fc42e62171f45b405f9a35b Mon Sep 17 00:00:00 2001 From: Kenfe-Mickael Laventure Date: Wed, 11 Jan 2017 13:42:49 -0800 Subject: [PATCH 384/563] 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 --- command/formatter/image.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/command/formatter/image.go b/command/formatter/image.go index 594b2f392..5c7de826f 100644 --- a/command/formatter/image.go +++ b/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 266900235c3ef24ca92b719c1057f1a81e0c7265 Mon Sep 17 00:00:00 2001 From: Daehyeok Mun Date: Tue, 29 Nov 2016 01:17:35 -0700 Subject: [PATCH 385/563] 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 --- command/container/stats_helpers.go | 13 +++++-------- command/plugin/create.go | 4 +++- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/command/container/stats_helpers.go b/command/container/stats_helpers.go index 4b57e3fe0..8eb7da0fd 100644 --- a/command/container/stats_helpers.go +++ b/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/command/plugin/create.go b/command/plugin/create.go index 2aab1e9e4..82d17af48 100644 --- a/command/plugin/create.go +++ b/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 acda56d47c83d896abca1a0d41f7f7473949f181 Mon Sep 17 00:00:00 2001 From: Josh Hawn Date: Tue, 22 Nov 2016 11:03:23 -0800 Subject: [PATCH 386/563] 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) --- command/service/update_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/command/service/update_test.go b/command/service/update_test.go index a6df6b985..bb931929c 100644 --- a/command/service/update_test.go +++ b/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 74c29fde046149401a6b572b06c465b4d565a315 Mon Sep 17 00:00:00 2001 From: Tony Abboud Date: Thu, 12 Jan 2017 12:01:29 -0500 Subject: [PATCH 387/563] 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 --- compose/convert/service.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/compose/convert/service.go b/compose/convert/service.go index 37f3ece40..a245987c8 100644 --- a/compose/convert/service.go +++ b/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 35de37289bbf17265b1e6a8ccd7d91eff4087878 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Fri, 13 Jan 2017 01:05:39 +0100 Subject: [PATCH 388/563] 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 --- command/container/opts.go | 4 ++++ command/container/opts_test.go | 8 ++++++++ command/container/run.go | 12 ++++-------- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/command/container/opts.go b/command/container/opts.go index c5fc15216..55cc3c3b2 100644 --- a/command/container/opts.go +++ b/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/command/container/opts_test.go b/command/container/opts_test.go index d02a0f7bf..ce3bb21b4 100644 --- a/command/container/opts_test.go +++ b/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/command/container/run.go b/command/container/run.go index 0f8da3fa4..4d85ee77a 100644 --- a/command/container/run.go +++ b/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 5d2722f83db9e301c6dcbe1c562c2051a52905db Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 16 Jan 2017 17:57:26 +0100 Subject: [PATCH 389/563] 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 --- command/container/opts.go | 5 +++++ command/container/update.go | 2 ++ command/image/build.go | 1 + command/network/create.go | 1 + command/service/create.go | 6 ++++++ command/service/opts.go | 9 +++++++++ command/service/update.go | 14 ++++++++++++++ command/stack/opts.go | 1 + command/swarm/opts.go | 2 ++ 9 files changed, 41 insertions(+) diff --git a/command/container/opts.go b/command/container/opts.go index 55cc3c3b2..9896323be 100644 --- a/command/container/opts.go +++ b/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/command/container/update.go b/command/container/update.go index 6a7cc820e..4a1220a26 100644 --- a/command/container/update.go +++ b/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/command/image/build.go b/command/image/build.go index 5d6e61140..2bead42ec 100644 --- a/command/image/build.go +++ b/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/command/network/create.go b/command/network/create.go index dd5e94ea2..57c59ed05 100644 --- a/command/network/create.go +++ b/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/command/service/create.go b/command/service/create.go index ca2bb089f..3c82b78bc 100644 --- a/command/service/create.go +++ b/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/command/service/opts.go b/command/service/opts.go index b794b07a3..2218890aa 100644 --- a/command/service/opts.go +++ b/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/command/service/update.go b/command/service/update.go index df0977d86..a33d599b6 100644 --- a/command/service/update.go +++ b/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/command/stack/opts.go b/command/stack/opts.go index 74fe4f534..996ff68f2 100644 --- a/command/stack/opts.go +++ b/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/command/swarm/opts.go b/command/swarm/opts.go index 40f88a441..b32cc9210 100644 --- a/command/swarm/opts.go +++ b/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 b554a5f62572da7eac6fb5a6947001dc129393c2 Mon Sep 17 00:00:00 2001 From: allencloud Date: Thu, 29 Dec 2016 01:09:25 +0800 Subject: [PATCH 390/563] purify error message in cli for create and run command Signed-off-by: allencloud --- command/container/run.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/command/container/run.go b/command/container/run.go index 4d85ee77a..cbe64548e 100644 --- a/command/container/run.go +++ b/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 182bccefbe2be037846f05fab9aa36ef3f6ede0f Mon Sep 17 00:00:00 2001 From: kaiwentan Date: Wed, 18 Jan 2017 00:26:37 +0800 Subject: [PATCH 391/563] correct all the formate to formatter Signed-off-by: kaiwentan --- command/formatter/disk_usage.go | 2 +- command/formatter/image.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/command/formatter/disk_usage.go b/command/formatter/disk_usage.go index 6f97d3b0f..f4abac59e 100644 --- a/command/formatter/disk_usage.go +++ b/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/command/formatter/image.go b/command/formatter/image.go index 5c7de826f..9187dfb2e 100644 --- a/command/formatter/image.go +++ b/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 ca5bd1c10677c572509d88dbdf63a98beba09952 Mon Sep 17 00:00:00 2001 From: allencloud Date: Sun, 8 Jan 2017 22:23:36 +0800 Subject: [PATCH 392/563] return error when listNode fails Signed-off-by: allencloud --- command/system/info.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/system/info.go b/command/system/info.go index ec1cf47de..d9fafd1aa 100644 --- a/command/system/info.go +++ b/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 775d9759c6b669de7a841a8497dcc7e26ab77e8a Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Thu, 19 Jan 2017 09:50:28 +0000 Subject: [PATCH 393/563] 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 --- command/formatter/disk_usage.go | 12 ++++++++++++ command/formatter/image.go | 4 ++++ command/formatter/stats.go | 4 ++++ 3 files changed, 20 insertions(+) diff --git a/command/formatter/disk_usage.go b/command/formatter/disk_usage.go index f4abac59e..ff1ab768c 100644 --- a/command/formatter/disk_usage.go +++ b/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/command/formatter/image.go b/command/formatter/image.go index 9187dfb2e..3dbb1b964 100644 --- a/command/formatter/image.go +++ b/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/command/formatter/stats.go b/command/formatter/stats.go index 7997f996d..a37e9d792 100644 --- a/command/formatter/stats.go +++ b/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 bbc4ac69fa3f5de90e3e8602fbfd60fbce4f2ca6 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Wed, 11 Jan 2017 13:54:52 -0800 Subject: [PATCH 394/563] 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) --- command/container/create.go | 37 +++++++++++++++----------- command/formatter/image.go | 12 +++++---- command/image/build.go | 13 ++++++---- command/image/pull.go | 12 +++++---- command/image/push.go | 6 ++--- command/image/trust.go | 39 ++++++++++++++-------------- command/plugin/create.go | 4 +-- command/plugin/install.go | 52 ++++++++++++++----------------------- command/plugin/push.go | 16 +++++++----- command/registry.go | 4 +-- command/service/trust.go | 48 ++++++++++++++-------------------- 11 files changed, 119 insertions(+), 124 deletions(-) diff --git a/command/container/create.go b/command/container/create.go index 13890d9ef..01d7815c9 100644 --- a/command/container/create.go +++ b/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/command/formatter/image.go b/command/formatter/image.go index 9187dfb2e..fc0168cf7 100644 --- a/command/formatter/image.go +++ b/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/command/image/build.go b/command/image/build.go index 5d6e61140..ecc686170 100644 --- a/command/image/build.go +++ b/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/command/image/pull.go b/command/image/pull.go index 24933fe84..d5aa3eefb 100644 --- a/command/image/pull.go +++ b/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/command/image/push.go b/command/image/push.go index a8ce4945e..7972718e6 100644 --- a/command/image/push.go +++ b/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/command/image/trust.go b/command/image/trust.go index 58e057439..2ff9b463d 100644 --- a/command/image/trust.go +++ b/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/command/plugin/create.go b/command/plugin/create.go index 82d17af48..e1e6f74ee 100644 --- a/command/plugin/create.go +++ b/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/command/plugin/install.go b/command/plugin/install.go index a64dc2525..39b8c15ec 100644 --- a/command/plugin/install.go +++ b/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/command/plugin/push.go b/command/plugin/push.go index b0766307f..b0ddad939 100644 --- a/command/plugin/push.go +++ b/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/command/registry.go b/command/registry.go index 65f6b3309..411310fa3 100644 --- a/command/registry.go +++ b/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/command/service/trust.go b/command/service/trust.go index 15f8a708f..d466f3b64 100644 --- a/command/service/trust.go +++ b/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 cd3c323c381e28fc06b0885b4b3d70237d7e7ca8 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 4 Jan 2017 10:06:59 -0500 Subject: [PATCH 395/563] Update Compose schema to match docker-compose. Signed-off-by: Daniel Nephin --- compose/schema/bindata.go | 4 ++-- compose/schema/data/config_schema_v3.0.json | 23 ++++++++++++--------- compose/types/types.go | 2 ++ 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/compose/schema/bindata.go b/compose/schema/bindata.go index c3774130b..c97650935 100644 --- a/compose/schema/bindata.go +++ b/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/compose/schema/data/config_schema_v3.0.json b/compose/schema/data/config_schema_v3.0.json index 520e57d5e..584b6ef5d 100644 --- a/compose/schema/data/config_schema_v3.0.json +++ b/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/compose/types/types.go b/compose/types/types.go index 5244bd116..393bee2f8 100644 --- a/compose/types/types.go +++ b/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 3dd116fede13f299aa824222f54d4bee07248c01 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 20 Jan 2017 12:53:19 -0500 Subject: [PATCH 396/563] Add missing network.internal. Signed-off-by: Daniel Nephin --- compose/convert/compose.go | 7 ++++--- compose/schema/data/config_schema_v3.0.json | 1 + compose/types/types.go | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/compose/convert/compose.go b/compose/convert/compose.go index 70c1762a4..532f4c4b2 100644 --- a/compose/convert/compose.go +++ b/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/compose/schema/data/config_schema_v3.0.json b/compose/schema/data/config_schema_v3.0.json index 584b6ef5d..fbcd8bb85 100644 --- a/compose/schema/data/config_schema_v3.0.json +++ b/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/compose/types/types.go b/compose/types/types.go index 393bee2f8..3f2f03883 100644 --- a/compose/types/types.go +++ b/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 c799b20f5b5cc209e1220c674ed005bba42743ef Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Tue, 22 Nov 2016 16:23:21 -0800 Subject: [PATCH 397/563] 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 --- command/formatter/plugin.go | 87 ++++++++++++++ command/formatter/plugin_test.go | 188 +++++++++++++++++++++++++++++++ command/plugin/list.go | 39 +++---- config/configfile/file.go | 1 + 4 files changed, 294 insertions(+), 21 deletions(-) create mode 100644 command/formatter/plugin.go create mode 100644 command/formatter/plugin_test.go diff --git a/command/formatter/plugin.go b/command/formatter/plugin.go new file mode 100644 index 000000000..5f94714a6 --- /dev/null +++ b/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/command/formatter/plugin_test.go b/command/formatter/plugin_test.go new file mode 100644 index 000000000..9ddbe11df --- /dev/null +++ b/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/command/plugin/list.go b/command/plugin/list.go index 8fd16dae3..51590224b 100644 --- a/command/plugin/list.go +++ b/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/config/configfile/file.go b/config/configfile/file.go index 39097133a..e8fe96e84 100644 --- a/config/configfile/file.go +++ b/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 62ff1a0ea7738218bf3b2ca1a99e00d8f2af4285 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 17 Jan 2017 15:46:07 +0100 Subject: [PATCH 398/563] 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 --- command/container/create.go | 2 +- command/container/run.go | 2 +- command/image/build.go | 2 +- command/image/pull.go | 2 +- command/image/push.go | 2 +- command/plugin/install.go | 2 +- command/plugin/push.go | 2 +- command/trust.go | 26 +++++++++++++++----------- 8 files changed, 22 insertions(+), 18 deletions(-) diff --git a/command/container/create.go b/command/container/create.go index 13890d9ef..787d09b3f 100644 --- a/command/container/create.go +++ b/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/command/container/run.go b/command/container/run.go index cbe64548e..e805ca1a5 100644 --- a/command/container/run.go +++ b/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/command/image/build.go b/command/image/build.go index 5d6e61140..3c92ba20b 100644 --- a/command/image/build.go +++ b/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/command/image/pull.go b/command/image/pull.go index 24933fe84..e840671c6 100644 --- a/command/image/pull.go +++ b/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/command/image/push.go b/command/image/push.go index a8ce4945e..a5ba7d794 100644 --- a/command/image/push.go +++ b/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/command/plugin/install.go b/command/plugin/install.go index a64dc2525..fd3060037 100644 --- a/command/plugin/install.go +++ b/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/command/plugin/push.go b/command/plugin/push.go index b0766307f..1a9c592a9 100644 --- a/command/plugin/push.go +++ b/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/command/trust.go b/command/trust.go index b4c8a84ee..c0742bc5b 100644 --- a/command/trust.go +++ b/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 a2afbcbb57a25009081c9cd867416a98215baecd Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Thu, 19 Jan 2017 10:17:56 -0800 Subject: [PATCH 399/563] 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 --- command/container/list.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/command/container/list.go b/command/container/list.go index 451c531a8..e0f4fdf21 100644 --- a/command/container/list.go +++ b/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 0964347819b79d0e8bd5d21a1b1715cec2cf856c Mon Sep 17 00:00:00 2001 From: John Howard Date: Wed, 11 Jan 2017 15:55:43 -0800 Subject: [PATCH 400/563] Windows: Use sequential file access Signed-off-by: John Howard --- command/utils.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/command/utils.go b/command/utils.go index 1837ca41f..f9255cf87 100644 --- a/command/utils.go +++ b/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 3494b518a5cb080da11096aac513b37cb545cc44 Mon Sep 17 00:00:00 2001 From: Kenfe-Mickael Laventure Date: Mon, 23 Jan 2017 13:52:33 -0800 Subject: [PATCH 401/563] 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 --- command/formatter/disk_usage.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/command/formatter/disk_usage.go b/command/formatter/disk_usage.go index ff1ab768c..dc5eec41d 100644 --- a/command/formatter/disk_usage.go +++ b/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 bfd9613e5f97f8ecc9bc3b0033fc0b6ce271fba4 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 24 Jan 2017 14:19:31 +0100 Subject: [PATCH 402/563] 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 --- command/image/build.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/command/image/build.go b/command/image/build.go index 3c92ba20b..fe903c74e 100644 --- a/command/image/build.go +++ b/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 f33bf818a2af8acc3095a17a70ab8ac3a36e2578 Mon Sep 17 00:00:00 2001 From: Kenfe-Mickael Laventure Date: Fri, 6 May 2016 15:09:46 -0700 Subject: [PATCH 403/563] 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 --- command/container/opts.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/command/container/opts.go b/command/container/opts.go index 55cc3c3b2..eabf9faca 100644 --- a/command/container/opts.go +++ b/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 7215ebffa872ed6b34aa14e63bd4fd0fc46c0717 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 10 Jan 2017 17:10:53 -0500 Subject: [PATCH 404/563] Add v3.1 schema and support validating multiple version. Signed-off-by: Daniel Nephin --- compose/loader/loader.go | 7 +- compose/schema/bindata.go | 27 +- compose/schema/data/config_schema_v3.1.json | 426 ++++++++++++++++++++ compose/schema/schema.go | 30 +- compose/schema/schema_test.go | 47 ++- 5 files changed, 511 insertions(+), 26 deletions(-) create mode 100644 compose/schema/data/config_schema_v3.1.json diff --git a/compose/loader/loader.go b/compose/loader/loader.go index 9e46b9759..c9554a4b4 100644 --- a/compose/loader/loader.go +++ b/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/compose/schema/bindata.go b/compose/schema/bindata.go index c97650935..6d900e0a9 100644 --- a/compose/schema/bindata.go +++ b/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/compose/schema/data/config_schema_v3.1.json b/compose/schema/data/config_schema_v3.1.json new file mode 100644 index 000000000..c43f296b5 --- /dev/null +++ b/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/compose/schema/schema.go b/compose/schema/schema.go index 6366cab48..ae33c77fb 100644 --- a/compose/schema/schema.go +++ b/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/compose/schema/schema_test.go b/compose/schema/schema_test.go index be98f807d..0935d4022 100644 --- a/compose/schema/schema_test.go +++ b/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 0382f4f3657ffbcd30a2a3b36f61f390b9e2f2fb Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 10 Jan 2017 17:40:53 -0500 Subject: [PATCH 405/563] Implement secret types for compose file. Signed-off-by: Daniel Nephin --- command/service/create.go | 2 +- command/service/parse.go | 4 +- command/service/update.go | 2 +- command/stack/deploy.go | 29 +++++++- compose/convert/compose.go | 27 ++++++++ compose/convert/service.go | 39 ++++++++++- compose/loader/loader.go | 76 +++++++++++++++++---- compose/loader/loader_test.go | 18 +++++ compose/schema/bindata.go | 8 +-- compose/schema/data/config_schema_v3.1.json | 4 +- compose/types/types.go | 18 +++++ 11 files changed, 201 insertions(+), 26 deletions(-) diff --git a/command/service/create.go b/command/service/create.go index ca2bb089f..1355c19c6 100644 --- a/command/service/create.go +++ b/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/command/service/parse.go b/command/service/parse.go index 6af7e3bb8..ce9b454ed 100644 --- a/command/service/parse.go +++ b/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/command/service/update.go b/command/service/update.go index df0977d86..3feef4823 100644 --- a/command/service/update.go +++ b/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/command/stack/deploy.go b/command/stack/deploy.go index 306a583e1..685662412 100644 --- a/command/stack/deploy.go +++ b/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/compose/convert/compose.go b/compose/convert/compose.go index 532f4c4b2..efcf8a697 100644 --- a/compose/convert/compose.go +++ b/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/compose/convert/service.go b/compose/convert/service.go index a245987c8..78ad308d3 100644 --- a/compose/convert/service.go +++ b/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/compose/loader/loader.go b/compose/loader/loader.go index c9554a4b4..a43347f47 100644 --- a/compose/loader/loader.go +++ b/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/compose/loader/loader_test.go b/compose/loader/loader_test.go index e15be7c54..f7fee89ed 100644 --- a/compose/loader/loader_test.go +++ b/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/compose/schema/bindata.go b/compose/schema/bindata.go index 6d900e0a9..3713315b2 100644 --- a/compose/schema/bindata.go +++ b/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/compose/schema/data/config_schema_v3.1.json b/compose/schema/data/config_schema_v3.1.json index c43f296b5..b67203218 100644 --- a/compose/schema/data/config_schema_v3.1.json +++ b/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/compose/types/types.go b/compose/types/types.go index 3f2f03883..d70d01ed2 100644 --- a/compose/types/types.go +++ b/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 4a1c23bc262b6933e941117eba78149171d715d9 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 13 Jan 2017 11:26:29 -0500 Subject: [PATCH 406/563] Add integration test for stack deploy with secrets. Signed-off-by: Daniel Nephin --- command/secret/utils.go | 5 +++-- command/stack/deploy.go | 27 ++++++++++++++++++++------- compose/convert/compose_test.go | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 9 deletions(-) diff --git a/command/secret/utils.go b/command/secret/utils.go index 42493896c..11d31ffd1 100644 --- a/command/secret/utils.go +++ b/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/command/stack/deploy.go b/command/stack/deploy.go index 685662412..203ae6d39 100644 --- a/command/stack/deploy.go +++ b/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/compose/convert/compose_test.go b/compose/convert/compose_test.go index d88ac7f7c..18c7aac93 100644 --- a/compose/convert/compose_test.go +++ b/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 682d75fa3fb73b536aa3e98c51130c82771b173a Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 18 Jan 2017 13:06:36 -0500 Subject: [PATCH 407/563] Test and fix external secrets in stack deploy. Signed-off-by: Daniel Nephin --- compose/convert/service.go | 12 ++++++++++-- compose/loader/loader.go | 3 +-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/compose/convert/service.go b/compose/convert/service.go index 78ad308d3..573f7723f 100644 --- a/compose/convert/service.go +++ b/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/compose/loader/loader.go b/compose/loader/loader.go index a43347f47..39f69a03f 100644 --- a/compose/loader/loader.go +++ b/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 40bde3ee0092005d3e09a4b3150e8d888bc620c2 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 18 Jan 2017 14:40:29 -0500 Subject: [PATCH 408/563] Remove secrets as part of stack remove. Signed-off-by: Daniel Nephin --- command/stack/common.go | 10 ++++++ command/stack/remove.go | 75 +++++++++++++++++++++++++++++++---------- 2 files changed, 67 insertions(+), 18 deletions(-) diff --git a/command/stack/common.go b/command/stack/common.go index 5c4996d66..72719f94f 100644 --- a/command/stack/common.go +++ b/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/command/stack/remove.go b/command/stack/remove.go index 734ff92a5..966c1aa6b 100644 --- a/command/stack/remove.go +++ b/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 b0eabe77183d513661fcc76b49c87b6accc4e33a Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 20 Jan 2017 17:06:35 -0500 Subject: [PATCH 409/563] Rebase Compose v3.1 schema on the latest v3 schema. Signed-off-by: Daniel Nephin --- compose/schema/bindata.go | 2 +- compose/schema/data/config_schema_v3.1.json | 22 +++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/compose/schema/bindata.go b/compose/schema/bindata.go index 3713315b2..9486e91ae 100644 --- a/compose/schema/bindata.go +++ b/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/compose/schema/data/config_schema_v3.1.json b/compose/schema/data/config_schema_v3.1.json index b67203218..b7037485f 100644 --- a/compose/schema/data/config_schema_v3.1.json +++ b/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 485a2b2b2fd11c93853fdedbf8434e3c6868dda2 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 26 Jan 2017 12:00:46 -0500 Subject: [PATCH 410/563] Set default values for uid and gid to prevent errors when starting a service. Signed-off-by: Daniel Nephin --- compose/convert/service.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/compose/convert/service.go b/compose/convert/service.go index 573f7723f..f23df2612 100644 --- a/compose/convert/service.go +++ b/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 2a949b557446f784b654191056ae4bac57fbccc4 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 23 Nov 2016 04:58:15 -0800 Subject: [PATCH 411/563] 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 --- command/plugin/list.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/command/plugin/list.go b/command/plugin/list.go index 51590224b..a1b231f57 100644 --- a/command/plugin/list.go +++ b/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 3df952523cd3508dcb5d0a96e7eb994b3f9f891e Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Fri, 27 Jan 2017 16:09:02 +0100 Subject: [PATCH 412/563] 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 --- compose/convert/service.go | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/compose/convert/service.go b/compose/convert/service.go index f23df2612..a8613c087 100644 --- a/compose/convert/service.go +++ b/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 0e9401f84b29cde01b24f71158154b0d1e7f688e Mon Sep 17 00:00:00 2001 From: Harald Albers Date: Fri, 27 Jan 2017 16:17:02 +0100 Subject: [PATCH 413/563] Add [OPTIONS] to usage of `plugin disable|push` Signed-off-by: Harald Albers --- command/plugin/disable.go | 2 +- command/plugin/push.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/command/plugin/disable.go b/command/plugin/disable.go index c3d36e20a..07b0ec228 100644 --- a/command/plugin/disable.go +++ b/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/command/plugin/push.go b/command/plugin/push.go index c5c906b82..6b826dce6 100644 --- a/command/plugin/push.go +++ b/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 383ed6f121abd078d4e3c8677f7cb1e54893505e Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 7 Dec 2016 11:06:07 -0800 Subject: [PATCH 414/563] 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 --- command/secret/inspect.go | 6 +----- command/secret/remove.go | 11 +++-------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/command/secret/inspect.go b/command/secret/inspect.go index 0a8bd4a23..fb694c5fb 100644 --- a/command/secret/inspect.go +++ b/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/command/secret/remove.go b/command/secret/remove.go index f45a619f6..91ca4388f 100644 --- a/command/secret/remove.go +++ b/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 ab794c55793096c9ef7330fb2203bf2808c419fa Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 25 Dec 2016 01:11:12 -0800 Subject: [PATCH 415/563] 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 --- command/service/opts.go | 25 ++----------------------- command/service/opts_test.go | 4 ++-- 2 files changed, 4 insertions(+), 25 deletions(-) diff --git a/command/service/opts.go b/command/service/opts.go index b794b07a3..742c02eee 100644 --- a/command/service/opts.go +++ b/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/command/service/opts_test.go b/command/service/opts_test.go index 78b956ad6..4031d6f25 100644 --- a/command/service/opts_test.go +++ b/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 f75ecc5ad224548e3e4105efb2d54dbdfe1fdcb7 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 28 Dec 2016 14:44:07 -0800 Subject: [PATCH 416/563] 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 --- command/container/opts.go | 14 +++----------- command/container/opts_test.go | 5 +++-- command/image/build.go | 14 +++----------- 3 files changed, 9 insertions(+), 24 deletions(-) diff --git a/command/container/opts.go b/command/container/opts.go index 55cc3c3b2..4f70c7a92 100644 --- a/command/container/opts.go +++ b/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/command/container/opts_test.go b/command/container/opts_test.go index ce3bb21b4..d0655069e 100644 --- a/command/container/opts_test.go +++ b/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/command/image/build.go b/command/image/build.go index 67753bea1..09625d5b2 100644 --- a/command/image/build.go +++ b/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 1eefdba226988149f3191ad071a10aa62c1d3fbf Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 18 Jan 2017 15:27:02 -0500 Subject: [PATCH 417/563] 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 --- compose/loader/loader.go | 175 +++++++++++----------------------- compose/loader/loader_test.go | 13 +-- compose/types/types.go | 66 ++++++++----- 3 files changed, 106 insertions(+), 148 deletions(-) diff --git a/compose/loader/loader.go b/compose/loader/loader.go index 39f69a03f..2c92666c5 100644 --- a/compose/loader/loader.go +++ b/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/compose/loader/loader_test.go b/compose/loader/loader_test.go index f7fee89ed..bb5d3ecc0 100644 --- a/compose/loader/loader_test.go +++ b/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/compose/types/types.go b/compose/types/types.go index d70d01ed2..3b9a2b2a0 100644 --- a/compose/types/types.go +++ b/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 67db25f42e61c72d24d9159a3eaf6d598a5de129 Mon Sep 17 00:00:00 2001 From: allencloud Date: Mon, 16 Jan 2017 16:58:23 +0800 Subject: [PATCH 418/563] add endpoint mode a default value Signed-off-by: allencloud --- command/service/opts.go | 54 ++++++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/command/service/opts.go b/command/service/opts.go index b794b07a3..292aa66d5 100644 --- a/command/service/opts.go +++ b/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 ca1e5ffeea109b96d0cdae9b5a645ea6b3cd5138 Mon Sep 17 00:00:00 2001 From: allencloud Date: Sun, 29 Jan 2017 01:04:10 +0800 Subject: [PATCH 419/563] remove cli/command/secrets/utils.go Signed-off-by: allencloud --- command/secret/utils.go | 76 ----------------------------------------- command/stack/deploy.go | 30 +++++++--------- 2 files changed, 13 insertions(+), 93 deletions(-) delete mode 100644 command/secret/utils.go diff --git a/command/secret/utils.go b/command/secret/utils.go deleted file mode 100644 index 11d31ffd1..000000000 --- a/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/command/stack/deploy.go b/command/stack/deploy.go index 203ae6d39..753b1503b 100644 --- a/command/stack/deploy.go +++ b/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 406c6348b62fd709cedea3d9878fb23db4e6f1ad Mon Sep 17 00:00:00 2001 From: allencloud Date: Tue, 17 Jan 2017 15:55:45 +0800 Subject: [PATCH 420/563] validate healthcheck params in daemon side Signed-off-by: allencloud --- command/container/opts.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/command/container/opts.go b/command/container/opts.go index 55cc3c3b2..245d8e856 100644 --- a/command/container/opts.go +++ b/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 b849aa6b95e14450c90657cfe9668dfd5704a464 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sat, 14 Jan 2017 00:12:19 -0800 Subject: [PATCH 421/563] 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 --- command/service/opts.go | 6 ++++++ command/service/update.go | 8 ++++++++ command/service/update_test.go | 22 ++++++++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/command/service/opts.go b/command/service/opts.go index 2218890aa..dcd52ac7a 100644 --- a/command/service/opts.go +++ b/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/command/service/update.go b/command/service/update.go index 66002b69e..57a4577f8 100644 --- a/command/service/update.go +++ b/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/command/service/update_test.go b/command/service/update_test.go index bb931929c..992ae9ef3 100644 --- a/command/service/update_test.go +++ b/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 31fb756bb6ac30c5815a5e4410624ba1c6a2244d Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Thu, 26 Jan 2017 13:08:07 -0800 Subject: [PATCH 422/563] 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 --- command/formatter/service.go | 92 ++++++++++++++++ command/formatter/service_test.go | 177 ++++++++++++++++++++++++++++++ command/service/list.go | 98 ++++++----------- command/stack/services.go | 28 ++++- config/configfile/file.go | 1 + 5 files changed, 327 insertions(+), 69 deletions(-) create mode 100644 command/formatter/service_test.go diff --git a/command/formatter/service.go b/command/formatter/service.go index 8242e1cb9..9d9241b22 100644 --- a/command/formatter/service.go +++ b/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/command/formatter/service_test.go b/command/formatter/service_test.go new file mode 100644 index 000000000..d4474297d --- /dev/null +++ b/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/command/service/list.go b/command/service/list.go index 724126079..ca3e741fa 100644 --- a/command/service/list.go +++ b/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/command/stack/services.go b/command/stack/services.go index a46652df7..78ddd399c 100644 --- a/command/stack/services.go +++ b/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/config/configfile/file.go b/config/configfile/file.go index e8fe96e84..c321b97f2 100644 --- a/config/configfile/file.go +++ b/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 2469463a227703c1aa922135f73af51aa9b2e731 Mon Sep 17 00:00:00 2001 From: Ian Campbell Date: Wed, 1 Feb 2017 16:20:51 +0000 Subject: [PATCH 423/563] 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 --- cobra.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/cobra.go b/cobra.go index 139845cb1..962b31441 100644 --- a/cobra.go +++ b/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 d98ab3d3ab63706e1d731581cdbce286de20d60a Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Sat, 28 Jan 2017 16:54:32 -0800 Subject: [PATCH 424/563] 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 --- command/formatter/plugin.go | 5 ++ command/formatter/plugin_test.go | 4 +- command/plugin/cmd.go | 1 + command/plugin/install.go | 89 ++++++++++++++------------- command/plugin/upgrade.go | 100 +++++++++++++++++++++++++++++++ 5 files changed, 156 insertions(+), 43 deletions(-) create mode 100644 command/plugin/upgrade.go diff --git a/command/formatter/plugin.go b/command/formatter/plugin.go index 5f94714a6..00bdf3d0f 100644 --- a/command/formatter/plugin.go +++ b/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/command/formatter/plugin_test.go b/command/formatter/plugin_test.go index 9ddbe11df..a6c8f7e6c 100644 --- a/command/formatter/plugin_test.go +++ b/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/command/plugin/cmd.go b/command/plugin/cmd.go index 2173943f8..92c990a97 100644 --- a/command/plugin/cmd.go +++ b/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/command/plugin/install.go b/command/plugin/install.go index ebfe1f1ee..631917a07 100644 --- a/command/plugin/install.go +++ b/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/command/plugin/upgrade.go b/command/plugin/upgrade.go new file mode 100644 index 000000000..d212cd7e5 --- /dev/null +++ b/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 1a677699aede7b68ebcc7fc1f6c0b693e00f075f Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sat, 4 Feb 2017 13:55:28 -0800 Subject: [PATCH 425/563] 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 --- compose/convert/compose.go | 9 +++---- compose/convert/compose_test.go | 18 +++++++++++--- compose/loader/loader_test.go | 26 +++++++++++++++++++++ compose/schema/bindata.go | 4 ++-- compose/schema/data/config_schema_v3.1.json | 1 + compose/types/types.go | 1 + 6 files changed, 50 insertions(+), 9 deletions(-) diff --git a/compose/convert/compose.go b/compose/convert/compose.go index efcf8a697..a4571df02 100644 --- a/compose/convert/compose.go +++ b/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/compose/convert/compose_test.go b/compose/convert/compose_test.go index 18c7aac93..c26782095 100644 --- a/compose/convert/compose_test.go +++ b/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/compose/loader/loader_test.go b/compose/loader/loader_test.go index bb5d3ecc0..3a2f27204 100644 --- a/compose/loader/loader_test.go +++ b/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/compose/schema/bindata.go b/compose/schema/bindata.go index 9486e91ae..bb91fbfa5 100644 --- a/compose/schema/bindata.go +++ b/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/compose/schema/data/config_schema_v3.1.json b/compose/schema/data/config_schema_v3.1.json index b7037485f..f76154ea1 100644 --- a/compose/schema/data/config_schema_v3.1.json +++ b/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/compose/types/types.go b/compose/types/types.go index 3b9a2b2a0..4bb5cb6d2 100644 --- a/compose/types/types.go +++ b/compose/types/types.go @@ -238,6 +238,7 @@ type NetworkConfig struct { Ipam IPAMConfig External External Internal bool + Attachable bool Labels MappingWithEquals } From af80020ef256edda93b223482d05f41fdd10038a Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 5 Feb 2017 08:55:30 -0800 Subject: [PATCH 426/563] 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 --- command/container/stats.go | 2 +- command/formatter/stats.go | 22 ++++----- command/formatter/stats_test.go | 83 +++++++++++++++++++++++---------- 3 files changed, 71 insertions(+), 36 deletions(-) diff --git a/command/container/stats.go b/command/container/stats.go index 593db27b2..940a03914 100644 --- a/command/container/stats.go +++ b/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/command/formatter/stats.go b/command/formatter/stats.go index a37e9d792..7302bca01 100644 --- a/command/formatter/stats.go +++ b/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/command/formatter/stats_test.go b/command/formatter/stats_test.go index d5a17cc70..f9ecda33e 100644 --- a/command/formatter/stats_test.go +++ b/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 21c0b6c0e6143d8228ad84d48741a66cac8d32d8 Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Fri, 3 Feb 2017 16:07:04 -0800 Subject: [PATCH 427/563] 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 --- command/plugin/install.go | 10 +--------- command/plugin/upgrade.go | 13 +------------ command/utils.go | 11 ++++------- 3 files changed, 6 insertions(+), 28 deletions(-) diff --git a/command/plugin/install.go b/command/plugin/install.go index 631917a07..15877761a 100644 --- a/command/plugin/install.go +++ b/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/command/plugin/upgrade.go b/command/plugin/upgrade.go index d212cd7e5..6861aa1b3 100644 --- a/command/plugin/upgrade.go +++ b/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/command/utils.go b/command/utils.go index f9255cf87..4c52ce61b 100644 --- a/command/utils.go +++ b/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 6f8f1d20a2a0c87ebf6dcaca6bb1816e286b15e2 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 6 Feb 2017 14:16:03 +0100 Subject: [PATCH 428/563] 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 --- command/service/opts.go | 12 ------------ command/service/update.go | 18 ------------------ command/service/update_test.go | 23 ----------------------- 3 files changed, 53 deletions(-) diff --git a/command/service/opts.go b/command/service/opts.go index f2470673a..9a0ae64ca 100644 --- a/command/service/opts.go +++ b/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/command/service/update.go b/command/service/update.go index 57a4577f8..7f461c90a 100644 --- a/command/service/update.go +++ b/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/command/service/update_test.go b/command/service/update_test.go index 992ae9ef3..f2887e229 100644 --- a/command/service/update_test.go +++ b/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 c69e0f7dd54777a5f348e395284d4aef1fc4cec0 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Tue, 31 Jan 2017 12:44:05 -0800 Subject: [PATCH 429/563] 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 --- compose/schema/bindata.go | 2 +- compose/schema/data/config_schema_v3.1.json | 16 ++++++++++++++-- compose/types/types.go | 10 +++++++++- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/compose/schema/bindata.go b/compose/schema/bindata.go index bb91fbfa5..0b5aa18b7 100644 --- a/compose/schema/bindata.go +++ b/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/compose/schema/data/config_schema_v3.1.json b/compose/schema/data/config_schema_v3.1.json index f76154ea1..b9d422199 100644 --- a/compose/schema/data/config_schema_v3.1.json +++ b/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/compose/types/types.go b/compose/types/types.go index 4bb5cb6d2..c74014fb1 100644 --- a/compose/types/types.go +++ b/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 c53471254b4184dee4c907ae51aac6628e0be873 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Tue, 31 Jan 2017 12:45:45 -0800 Subject: [PATCH 430/563] 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 --- compose/convert/service.go | 22 ++- compose/convert/service_test.go | 34 +++++ compose/loader/loader.go | 78 ++++++++++- compose/loader/loader_test.go | 232 ++++++++++++++++++++++++++++++-- 4 files changed, 342 insertions(+), 24 deletions(-) diff --git a/compose/convert/service.go b/compose/convert/service.go index a8613c087..ef6a04ebc 100644 --- a/compose/convert/service.go +++ b/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/compose/convert/service_test.go b/compose/convert/service_test.go index 2e614d730..64ccfd038 100644 --- a/compose/convert/service_test.go +++ b/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/compose/loader/loader.go b/compose/loader/loader.go index 2c92666c5..2ccef7198 100644 --- a/compose/loader/loader.go +++ b/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/compose/loader/loader_test.go b/compose/loader/loader_test.go index 3a2f27204..53f4280b6 100644 --- a/compose/loader/loader_test.go +++ b/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 635d686a88e7ff6c5a3f9e36ec333779926b3151 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Wed, 25 Jan 2017 16:54:18 -0800 Subject: [PATCH 431/563] 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) --- command/container/create.go | 6 +----- command/formatter/disk_usage.go | 4 ++-- command/formatter/image.go | 4 ++-- command/formatter/service.go | 13 +++++++------ command/image/build.go | 4 +--- command/image/pull.go | 8 ++++---- command/image/trust.go | 16 ++++++++-------- command/plugin/install.go | 20 +++----------------- command/plugin/push.go | 7 ++----- command/plugin/upgrade.go | 12 ++++++------ command/service/trust.go | 12 +++++++----- command/task/print.go | 14 ++++++++------ trust/trust.go | 4 ++-- 13 files changed, 53 insertions(+), 71 deletions(-) diff --git a/command/container/create.go b/command/container/create.go index cfd672e77..9559ba0c0 100644 --- a/command/container/create.go +++ b/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/command/formatter/disk_usage.go b/command/formatter/disk_usage.go index dc5eec41d..fd7aabc7c 100644 --- a/command/formatter/disk_usage.go +++ b/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/command/formatter/image.go b/command/formatter/image.go index 06319b935..b6508224a 100644 --- a/command/formatter/image.go +++ b/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/command/formatter/service.go b/command/formatter/service.go index 9d9241b22..8e38cb3a1 100644 --- a/command/formatter/service.go +++ b/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/command/image/build.go b/command/image/build.go index 34e0a3950..96d90cf58 100644 --- a/command/image/build.go +++ b/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/command/image/pull.go b/command/image/pull.go index 967beca86..515273d43 100644 --- a/command/image/pull.go +++ b/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/command/image/trust.go b/command/image/trust.go index 2ff9b463d..8332dd7de 100644 --- a/command/image/trust.go +++ b/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/command/plugin/install.go b/command/plugin/install.go index 15877761a..9e9ea40e2 100644 --- a/command/plugin/install.go +++ b/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/command/plugin/push.go b/command/plugin/push.go index 6b826dce6..f3643b7f1 100644 --- a/command/plugin/push.go +++ b/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/command/plugin/upgrade.go b/command/plugin/upgrade.go index 6861aa1b3..07f0c7bb9 100644 --- a/command/plugin/upgrade.go +++ b/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/command/service/trust.go b/command/service/trust.go index d466f3b64..3fd80ae87 100644 --- a/command/service/trust.go +++ b/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/command/task/print.go b/command/task/print.go index 60a2bca85..d7e20bb59 100644 --- a/command/task/print.go +++ b/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/trust/trust.go b/trust/trust.go index 44f8197ba..777a61118 100644 --- a/trust/trust.go +++ b/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 6ef0b64945a6af9666ac601021d8de99317b5207 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Tue, 7 Feb 2017 21:58:56 +0100 Subject: [PATCH 432/563] 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 --- command/formatter/container_test.go | 16 ++++++++-------- command/formatter/image_test.go | 26 +++++++++++++------------- command/formatter/stats_test.go | 12 ++++++------ command/service/opts_test.go | 2 +- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/command/formatter/container_test.go b/command/formatter/container_test.go index 16137897b..f01332815 100644 --- a/command/formatter/container_test.go +++ b/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/command/formatter/image_test.go b/command/formatter/image_test.go index ffe77f667..cf134300a 100644 --- a/command/formatter/image_test.go +++ b/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/command/formatter/stats_test.go b/command/formatter/stats_test.go index f9ecda33e..546319eb8 100644 --- a/command/formatter/stats_test.go +++ b/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/command/service/opts_test.go b/command/service/opts_test.go index 4031d6f25..ac5106793 100644 --- a/command/service/opts_test.go +++ b/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 758383200e127e00eabe11811e22ad3e7aa30930 Mon Sep 17 00:00:00 2001 From: Zhang Wei Date: Tue, 7 Feb 2017 10:27:40 +0800 Subject: [PATCH 433/563] 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 --- command/formatter/stats.go | 6 ++++-- command/formatter/stats_test.go | 8 ++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/command/formatter/stats.go b/command/formatter/stats.go index a37e9d792..0e31792c4 100644 --- a/command/formatter/stats.go +++ b/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/command/formatter/stats_test.go b/command/formatter/stats_test.go index d5a17cc70..f5c6cae0c 100644 --- a/command/formatter/stats_test.go +++ b/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 9e940b9020237f13308d2ab1441a011e85abac70 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Thu, 9 Feb 2017 14:54:05 -0800 Subject: [PATCH 434/563] print 'worker' join token after swarm init Signed-off-by: Victor Vieux --- command/swarm/init.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/swarm/init.go b/command/swarm/init.go index b79602267..59bfa5b62 100644 --- a/command/swarm/init.go +++ b/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 04bea1696925f86460d591843b1be71204f162a9 Mon Sep 17 00:00:00 2001 From: yupengzte Date: Fri, 10 Feb 2017 14:53:18 +0800 Subject: [PATCH 435/563] Add = before the value of PublishedPort Signed-off-by: yupengzte --- command/formatter/service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/formatter/service.go b/command/formatter/service.go index 8e38cb3a1..09f4368f4 100644 --- a/command/formatter/service.go +++ b/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 53bdc98713739cb1f6d08d52d535e361de6442f2 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 6 Nov 2016 21:54:40 -0800 Subject: [PATCH 436/563] 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 --- command/cli.go | 1 + command/formatter/task.go | 145 +++++++++++++++++++++++++++++++++ command/formatter/task_test.go | 107 ++++++++++++++++++++++++ command/node/ps.go | 16 +++- command/service/ps.go | 15 +++- command/stack/ps.go | 16 +++- command/task/print.go | 105 ++++-------------------- config/configfile/file.go | 1 + 8 files changed, 311 insertions(+), 95 deletions(-) create mode 100644 command/formatter/task.go create mode 100644 command/formatter/task_test.go diff --git a/command/cli.go b/command/cli.go index bf9d55460..782c3a507 100644 --- a/command/cli.go +++ b/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/command/formatter/task.go b/command/formatter/task.go new file mode 100644 index 000000000..caf765151 --- /dev/null +++ b/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/command/formatter/task_test.go b/command/formatter/task_test.go new file mode 100644 index 000000000..c990f6861 --- /dev/null +++ b/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/command/node/ps.go b/command/node/ps.go index 52ac36646..cb0f3efdf 100644 --- a/command/node/ps.go +++ b/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/command/service/ps.go b/command/service/ps.go index 12b25bf4f..c4ff1b9e3 100644 --- a/command/service/ps.go +++ b/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/command/stack/ps.go b/command/stack/ps.go index 7bbcf5420..bac5307bd 100644 --- a/command/stack/ps.go +++ b/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/command/task/print.go b/command/task/print.go index d7e20bb59..3df3b2985 100644 --- a/command/task/print.go +++ b/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/config/configfile/file.go b/config/configfile/file.go index c321b97f2..d83434676 100644 --- a/config/configfile/file.go +++ b/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 9dda1155f3acc1d6e7f2532d569758f6c11228d6 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Fri, 3 Feb 2017 16:48:46 -0800 Subject: [PATCH 437/563] 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 --- command/formatter/container.go | 58 ++++++++++++++----------- command/formatter/container_test.go | 63 ++++++++++------------------ command/formatter/custom.go | 26 +++--------- command/formatter/disk_usage.go | 31 ++++++-------- command/formatter/disk_usage_test.go | 56 +++++++++++++++++++++++++ command/formatter/formatter.go | 14 +++---- command/formatter/image.go | 51 +++++++++++++++------- command/formatter/image_test.go | 26 +++++------- command/formatter/network.go | 37 +++++++++------- command/formatter/network_test.go | 26 +++++------- command/formatter/plugin.go | 15 ++++--- command/formatter/plugin_test.go | 14 ++----- command/formatter/service.go | 15 ++++--- command/formatter/stats.go | 33 ++++++++------- command/formatter/stats_test.go | 5 --- command/formatter/volume.go | 40 +++++++++++------- command/formatter/volume_test.go | 18 +++----- 17 files changed, 281 insertions(+), 247 deletions(-) create mode 100644 command/formatter/disk_usage_test.go diff --git a/command/formatter/container.go b/command/formatter/container.go index 627345335..c8cb7b69e 100644 --- a/command/formatter/container.go +++ b/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/command/formatter/container_test.go b/command/formatter/container_test.go index f01332815..ef6e86c59 100644 --- a/command/formatter/container_test.go +++ b/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/command/formatter/custom.go b/command/formatter/custom.go index df3268442..73487f63e 100644 --- a/command/formatter/custom.go +++ b/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/command/formatter/disk_usage.go b/command/formatter/disk_usage.go index fd7aabc7c..7170411e1 100644 --- a/command/formatter/disk_usage.go +++ b/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/command/formatter/disk_usage_test.go b/command/formatter/disk_usage_test.go new file mode 100644 index 000000000..06d1c2c1f --- /dev/null +++ b/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/command/formatter/formatter.go b/command/formatter/formatter.go index 4345f7c3b..16e8e6af2 100644 --- a/command/formatter/formatter.go +++ b/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/command/formatter/image.go b/command/formatter/image.go index b6508224a..8f18045c1 100644 --- a/command/formatter/image.go +++ b/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/command/formatter/image_test.go b/command/formatter/image_test.go index cf134300a..e7c15dbf5 100644 --- a/command/formatter/image_test.go +++ b/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/command/formatter/network.go b/command/formatter/network.go index c29be412a..4aeebd175 100644 --- a/command/formatter/network.go +++ b/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/command/formatter/network_test.go b/command/formatter/network_test.go index e105afbdf..24bf46d25 100644 --- a/command/formatter/network_test.go +++ b/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/command/formatter/plugin.go b/command/formatter/plugin.go index 00bdf3d0f..2b71281a5 100644 --- a/command/formatter/plugin.go +++ b/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/command/formatter/plugin_test.go b/command/formatter/plugin_test.go index a6c8f7e6c..3cc0af8a3 100644 --- a/command/formatter/plugin_test.go +++ b/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/command/formatter/service.go b/command/formatter/service.go index 8e38cb3a1..f7d78154e 100644 --- a/command/formatter/service.go +++ b/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/command/formatter/stats.go b/command/formatter/stats.go index 750f57eb4..c0151101a 100644 --- a/command/formatter/stats.go +++ b/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/command/formatter/stats_test.go b/command/formatter/stats_test.go index 9f48862b2..5d6a91e7c 100644 --- a/command/formatter/stats_test.go +++ b/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/command/formatter/volume.go b/command/formatter/volume.go index 90c9b1353..342f2fb93 100644 --- a/command/formatter/volume.go +++ b/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/command/formatter/volume_test.go b/command/formatter/volume_test.go index 9ec18b691..9c23ae447 100644 --- a/command/formatter/volume_test.go +++ b/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 82bf90ffbcd9003326dd43fd4d862171e3138a06 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Fri, 3 Feb 2017 20:23:00 -0800 Subject: [PATCH 438/563] 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 --- command/formatter/container_test.go | 8 ++++++++ command/formatter/disk_usage_test.go | 9 --------- command/formatter/formatter.go | 2 +- command/formatter/image.go | 14 -------------- 4 files changed, 9 insertions(+), 24 deletions(-) diff --git a/command/formatter/container_test.go b/command/formatter/container_test.go index ef6e86c59..a5615d176 100644 --- a/command/formatter/container_test.go +++ b/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/command/formatter/disk_usage_test.go b/command/formatter/disk_usage_test.go index 06d1c2c1f..318e1692b 100644 --- a/command/formatter/disk_usage_test.go +++ b/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/command/formatter/formatter.go b/command/formatter/formatter.go index 16e8e6af2..a151e9c28 100644 --- a/command/formatter/formatter.go +++ b/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/command/formatter/image.go b/command/formatter/image.go index 8f18045c1..3aae34ea1 100644 --- a/command/formatter/image.go +++ b/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 03aed78d68d80c490d360975313fcde2f7160b62 Mon Sep 17 00:00:00 2001 From: yupengzte Date: Wed, 8 Feb 2017 16:31:16 +0800 Subject: [PATCH 439/563] fix the type Signed-off-by: yupengzte --- command/node/update.go | 4 ++-- command/service/opts.go | 4 ++-- command/swarm/init.go | 2 +- command/swarm/join.go | 2 +- flags/common.go | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/command/node/update.go b/command/node/update.go index 6ca2a7c1e..aecb88c4a 100644 --- a/command/node/update.go +++ b/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/command/service/opts.go b/command/service/opts.go index f2470673a..3b43c7042 100644 --- a/command/service/opts.go +++ b/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/command/swarm/init.go b/command/swarm/init.go index b79602267..28fff3c8a 100644 --- a/command/swarm/init.go +++ b/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/command/swarm/join.go b/command/swarm/join.go index 40fc5c192..3022f6e89 100644 --- a/command/swarm/join.go +++ b/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/flags/common.go b/flags/common.go index af2fe0603..3c9d8fa6e 100644 --- a/flags/common.go +++ b/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 6887337d86e8247e6251b8fabbb2cef118acaf3c Mon Sep 17 00:00:00 2001 From: "bingshen.wbs" Date: Wed, 15 Feb 2017 17:32:37 +0800 Subject: [PATCH 440/563] fix docker stack volume's nocopy parameter Signed-off-by: bingshen.wbs --- compose/convert/volume.go | 3 +++ compose/convert/volume_test.go | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/compose/convert/volume.go b/compose/convert/volume.go index 24442d4dc..53c50958f 100644 --- a/compose/convert/volume.go +++ b/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/compose/convert/volume_test.go b/compose/convert/volume_test.go index 1132136b2..d218e7c2f 100644 --- a/compose/convert/volume_test.go +++ b/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 645f6ba7f5700e0e9ad2ba7436bad5e87f0cafad Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Tue, 14 Feb 2017 15:12:03 +0100 Subject: [PATCH 441/563] 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 --- compose/convert/service.go | 10 +++++++++- compose/types/types.go | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/compose/convert/service.go b/compose/convert/service.go index ef6a04ebc..93b910967 100644 --- a/compose/convert/service.go +++ b/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/compose/types/types.go b/compose/types/types.go index c74014fb1..058c4d09b 100644 --- a/compose/types/types.go +++ b/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 ddae8d967b7fd5dc80357351e540df5700aa1656 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Thu, 16 Feb 2017 14:30:39 +0100 Subject: [PATCH 442/563] Sort `docker stack ls` by name Signed-off-by: Vincent Demeester --- command/stack/list.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/command/stack/list.go b/command/stack/list.go index 9b6c645e2..3d81242b7 100644 --- a/command/stack/list.go +++ b/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 ca2aeb5a3e6521d146d971c5ebe4a7d46fa94086 Mon Sep 17 00:00:00 2001 From: "Aaron.L.Xu" Date: Thu, 16 Feb 2017 23:56:53 +0800 Subject: [PATCH 443/563] why there are so many mistakes in our repo (up to /cmd) Signed-off-by: Aaron.L.Xu --- command/container/exec.go | 2 +- command/formatter/reflect.go | 2 +- command/image/build.go | 2 +- command/image/build/context.go | 2 +- command/network/create.go | 2 +- command/swarm/init_test.go | 2 +- compose/types/types.go | 2 +- config/credentials/native_store.go | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/command/container/exec.go b/command/container/exec.go index 73329869a..676708c77 100644 --- a/command/container/exec.go +++ b/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/command/formatter/reflect.go b/command/formatter/reflect.go index d1d8737d2..9692bbce7 100644 --- a/command/formatter/reflect.go +++ b/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/command/image/build.go b/command/image/build.go index 96d90cf58..34c231d63 100644 --- a/command/image/build.go +++ b/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/command/image/build/context.go b/command/image/build/context.go index 86157c359..9ea065adf 100644 --- a/command/image/build/context.go +++ b/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/command/network/create.go b/command/network/create.go index 57c59ed05..21300d783 100644 --- a/command/network/create.go +++ b/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/command/swarm/init_test.go b/command/swarm/init_test.go index 13de1cd55..4f56de357 100644 --- a/command/swarm/init_test.go +++ b/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/compose/types/types.go b/compose/types/types.go index c74014fb1..66c164186 100644 --- a/compose/types/types.go +++ b/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/config/credentials/native_store.go b/config/credentials/native_store.go index 9e0ab7f0f..68a87e8c6 100644 --- a/config/credentials/native_store.go +++ b/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 9e78c9b063f27cda9bdbeee51a643ed09866ee11 Mon Sep 17 00:00:00 2001 From: Nishant Totla Date: Wed, 8 Feb 2017 14:15:32 -0800 Subject: [PATCH 444/563] Suppressing image digest in docker ps Signed-off-by: Nishant Totla --- command/formatter/container.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/command/formatter/container.go b/command/formatter/container.go index 627345335..e31611c1e 100644 --- a/command/formatter/container.go +++ b/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 16b16315944f8ae2e483eef11333a24292400fb9 Mon Sep 17 00:00:00 2001 From: allencloud Date: Fri, 17 Feb 2017 16:28:08 +0800 Subject: [PATCH 445/563] split compose deploy from deploy.go Signed-off-by: allencloud --- command/stack/deploy.go | 282 --------------------------- command/stack/deploy_composefile.go | 290 ++++++++++++++++++++++++++++ 2 files changed, 290 insertions(+), 282 deletions(-) create mode 100644 command/stack/deploy_composefile.go diff --git a/command/stack/deploy.go b/command/stack/deploy.go index 753b1503b..22557fc45 100644 --- a/command/stack/deploy.go +++ b/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/command/stack/deploy_composefile.go b/command/stack/deploy_composefile.go new file mode 100644 index 000000000..72f9b8aac --- /dev/null +++ b/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 e858f5f7c4dca2d73d2648155e07ac871c73788c Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Sun, 19 Feb 2017 00:43:08 -0800 Subject: [PATCH 446/563] add missing API changes Signed-off-by: Victor Vieux --- command/service/opts.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/service/opts.go b/command/service/opts.go index 9a0ae64ca..35c5f2f65 100644 --- a/command/service/opts.go +++ b/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 e66e519e8dbc1028806f4dc965f705017af3dd8f Mon Sep 17 00:00:00 2001 From: Tony Abboud Date: Fri, 13 Jan 2017 10:01:58 -0500 Subject: [PATCH 447/563] Add --add-host for docker build Signed-off-by: Tony Abboud --- command/image/build.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/command/image/build.go b/command/image/build.go index 34c231d63..4639833a9 100644 --- a/command/image/build.go +++ b/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 a77cf51173e2844e0e289de74b78f037bce1b57f Mon Sep 17 00:00:00 2001 From: Reficul Date: Tue, 21 Feb 2017 10:26:06 +0800 Subject: [PATCH 448/563] fix wrong print format Signed-off-by: Reficul --- command/container/opts_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/container/opts_test.go b/command/container/opts_test.go index d0655069e..3aef42704 100644 --- a/command/container/opts_test.go +++ b/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 1d0e556669544bf38f50cc98a00cfac64cdc418f Mon Sep 17 00:00:00 2001 From: Krasi Georgiev Date: Wed, 15 Feb 2017 21:43:01 +0200 Subject: [PATCH 449/563] ignore registry url from user when it is the default namespace Signed-off-by: Krasi Georgiev --- command/registry/login.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/command/registry/login.go b/command/registry/login.go index bdcc9a103..ee4fd97f1 100644 --- a/command/registry/login.go +++ b/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 9037f77d315acf2c794bdef337564aa713620aed Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sun, 5 Feb 2017 21:22:57 -0800 Subject: [PATCH 450/563] 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 --- command/service/opts.go | 28 +++++++++++++++++----------- command/service/update.go | 2 ++ command/service/update_test.go | 22 ++++++++++++++++++++++ 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/command/service/opts.go b/command/service/opts.go index d8618e73c..adab9b365 100644 --- a/command/service/opts.go +++ b/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/command/service/update.go b/command/service/update.go index 7f461c90a..770a5bd26 100644 --- a/command/service/update.go +++ b/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/command/service/update_test.go b/command/service/update_test.go index f2887e229..c43e59613 100644 --- a/command/service/update_test.go +++ b/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 98c222239e4f807ea668fc16713926b476463a1d Mon Sep 17 00:00:00 2001 From: fate-grand-order Date: Tue, 21 Feb 2017 16:53:29 +0800 Subject: [PATCH 451/563] 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 --- command/container/opts_test.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/command/container/opts_test.go b/command/container/opts_test.go index d0655069e..725c9beb4 100644 --- a/command/container/opts_test.go +++ b/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 5b67f20a917e4fbe42b7c19d1c689a2c5f380eb8 Mon Sep 17 00:00:00 2001 From: yupengzte Date: Thu, 23 Feb 2017 16:46:08 +0800 Subject: [PATCH 452/563] Delete dots to align with other commands description Signed-off-by: yupengzte --- command/node/inspect.go | 2 +- command/service/inspect.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/command/node/inspect.go b/command/node/inspect.go index 97a271778..a08497003 100644 --- a/command/node/inspect.go +++ b/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/command/service/inspect.go b/command/service/inspect.go index deb701bf6..7af9b98c3 100644 --- a/command/service/inspect.go +++ b/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 b09aa604c8b6abb81356b1a29457e11d72121791 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Thu, 23 Feb 2017 13:36:57 +0100 Subject: [PATCH 453/563] 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 --- command/system/info.go | 121 ++++++++++++++++++++++++++--------------- 1 file changed, 76 insertions(+), 45 deletions(-) diff --git a/command/system/info.go b/command/system/info.go index d9fafd1aa..aa9246670 100644 --- a/command/system/info.go +++ b/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 3581bec44220e38193f0b69e0d2d159494592c04 Mon Sep 17 00:00:00 2001 From: Arash Deshmeh Date: Sat, 18 Feb 2017 00:29:51 -0500 Subject: [PATCH 454/563] docker compose interpolation format error now includes a hint on escaping $ characters. Signed-off-by: Arash Deshmeh --- compose/interpolation/interpolation.go | 2 +- compose/interpolation/interpolation_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/compose/interpolation/interpolation.go b/compose/interpolation/interpolation.go index 734f28ec9..29c2e0e27 100644 --- a/compose/interpolation/interpolation.go +++ b/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/compose/interpolation/interpolation_test.go b/compose/interpolation/interpolation_test.go index c3921701b..1852b9eb4 100644 --- a/compose/interpolation/interpolation_test.go +++ b/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 5de7378cbe4ddd1e41e89b54fa9a06939e0e6614 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 22 Feb 2017 13:52:09 -0500 Subject: [PATCH 455/563] Support customizing the default network for a stack. Signed-off-by: Daniel Nephin --- compose/convert/service.go | 13 ++++++------- compose/convert/service_test.go | 28 ++++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/compose/convert/service.go b/compose/convert/service.go index 93b910967..9af4a7430 100644 --- a/compose/convert/service.go +++ b/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/compose/convert/service_test.go b/compose/convert/service_test.go index 64ccfd038..10bde3508 100644 --- a/compose/convert/service_test.go +++ b/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 5a53ae51707e955add875d92f830f7607770aac1 Mon Sep 17 00:00:00 2001 From: Genki Takiuchi Date: Sun, 26 Feb 2017 12:51:03 +0900 Subject: [PATCH 456/563] Fixed typo. Signed-off-by: Genki Takiuchi --- command/formatter/service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/formatter/service.go b/command/formatter/service.go index 09f4368f4..b13c5ee60 100644 --- a/command/formatter/service.go +++ b/command/formatter/service.go @@ -40,7 +40,7 @@ UpdateStatus: {{- end }} Placement: {{- if .TaskPlacementConstraints -}} - Contraints: {{ .TaskPlacementConstraints }} + Constraints: {{ .TaskPlacementConstraints }} {{- end }} {{- if .HasUpdateConfig }} UpdateConfig: From 407d65df9d0929b649095ba6018e091197aa9b9b Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 27 Feb 2017 18:39:35 +0100 Subject: [PATCH 457/563] Add unit tests to cli/command/volume package Signed-off-by: Vincent Demeester --- command/cli.go | 1 + command/volume/client_test.go | 53 +++++++ command/volume/cmd.go | 3 +- command/volume/create.go | 7 +- command/volume/create_test.go | 142 +++++++++++++++++ command/volume/inspect.go | 7 +- command/volume/inspect_test.go | 150 ++++++++++++++++++ command/volume/list.go | 7 +- command/volume/list_test.go | 124 +++++++++++++++ command/volume/prune.go | 7 +- command/volume/prune_test.go | 132 +++++++++++++++ command/volume/remove.go | 18 +-- 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 + internal/test/builders/doc.go | 3 + internal/test/builders/node.go | 3 + internal/test/builders/volume.go | 43 +++++ internal/test/cli.go | 37 ++++- internal/test/doc.go | 5 + 29 files changed, 815 insertions(+), 34 deletions(-) create mode 100644 command/volume/client_test.go create mode 100644 command/volume/create_test.go create mode 100644 command/volume/inspect_test.go create mode 100644 command/volume/list_test.go create mode 100644 command/volume/prune_test.go create mode 100644 command/volume/remove_test.go create mode 100644 command/volume/testdata/volume-inspect-with-format.json-template.golden create mode 100644 command/volume/testdata/volume-inspect-with-format.simple-template.golden create mode 100644 command/volume/testdata/volume-inspect-without-format.multiple-volume-with-labels.golden create mode 100644 command/volume/testdata/volume-inspect-without-format.single-volume.golden create mode 100644 command/volume/testdata/volume-list-with-config-format.golden create mode 100644 command/volume/testdata/volume-list-with-format.golden create mode 100644 command/volume/testdata/volume-list-without-format.golden create mode 100644 command/volume/testdata/volume-prune-no.golden create mode 100644 command/volume/testdata/volume-prune-yes.golden create mode 100644 command/volume/testdata/volume-prune.deletedVolumes.golden create mode 100644 command/volume/testdata/volume-prune.empty.golden create mode 100644 internal/test/builders/doc.go create mode 100644 internal/test/builders/volume.go create mode 100644 internal/test/doc.go diff --git a/command/cli.go b/command/cli.go index bf9d55460..782c3a507 100644 --- a/command/cli.go +++ b/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/command/volume/client_test.go b/command/volume/client_test.go new file mode 100644 index 000000000..c29655cdb --- /dev/null +++ b/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/command/volume/cmd.go b/command/volume/cmd.go index 2bc768775..4ef838133 100644 --- a/command/volume/cmd.go +++ b/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/command/volume/create.go b/command/volume/create.go index 21cfa84b7..f7ca36215 100644 --- a/command/volume/create.go +++ b/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/command/volume/create_test.go b/command/volume/create_test.go new file mode 100644 index 000000000..b7d5a443a --- /dev/null +++ b/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/command/volume/inspect.go b/command/volume/inspect.go index f58b927ac..70db26495 100644 --- a/command/volume/inspect.go +++ b/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/command/volume/inspect_test.go b/command/volume/inspect_test.go new file mode 100644 index 000000000..e2ea7b35d --- /dev/null +++ b/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/command/volume/list.go b/command/volume/list.go index 0de83aea4..3577db955 100644 --- a/command/volume/list.go +++ b/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/command/volume/list_test.go b/command/volume/list_test.go new file mode 100644 index 000000000..2f4a36633 --- /dev/null +++ b/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/command/volume/prune.go b/command/volume/prune.go index 405fbeb29..7e78c66e0 100644 --- a/command/volume/prune.go +++ b/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/command/volume/prune_test.go b/command/volume/prune_test.go new file mode 100644 index 000000000..c07834675 --- /dev/null +++ b/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/command/volume/remove.go b/command/volume/remove.go index f464bb3e1..c1267f1ea 100644 --- a/command/volume/remove.go +++ b/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/command/volume/remove_test.go b/command/volume/remove_test.go new file mode 100644 index 000000000..b2a106c22 --- /dev/null +++ b/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/command/volume/testdata/volume-inspect-with-format.json-template.golden b/command/volume/testdata/volume-inspect-with-format.json-template.golden new file mode 100644 index 000000000..2393cd01d --- /dev/null +++ b/command/volume/testdata/volume-inspect-with-format.json-template.golden @@ -0,0 +1 @@ +{"foo":"bar"} diff --git a/command/volume/testdata/volume-inspect-with-format.simple-template.golden b/command/volume/testdata/volume-inspect-with-format.simple-template.golden new file mode 100644 index 000000000..4833bbb03 --- /dev/null +++ b/command/volume/testdata/volume-inspect-with-format.simple-template.golden @@ -0,0 +1 @@ +volume diff --git a/command/volume/testdata/volume-inspect-without-format.multiple-volume-with-labels.golden b/command/volume/testdata/volume-inspect-without-format.multiple-volume-with-labels.golden new file mode 100644 index 000000000..19cad5024 --- /dev/null +++ b/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/command/volume/testdata/volume-inspect-without-format.single-volume.golden b/command/volume/testdata/volume-inspect-without-format.single-volume.golden new file mode 100644 index 000000000..22d0c5a65 --- /dev/null +++ b/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/command/volume/testdata/volume-list-with-config-format.golden b/command/volume/testdata/volume-list-with-config-format.golden new file mode 100644 index 000000000..72fa0bd4d --- /dev/null +++ b/command/volume/testdata/volume-list-with-config-format.golden @@ -0,0 +1,3 @@ +baz local foo=bar +foo bar +volume local diff --git a/command/volume/testdata/volume-list-with-format.golden b/command/volume/testdata/volume-list-with-format.golden new file mode 100644 index 000000000..72fa0bd4d --- /dev/null +++ b/command/volume/testdata/volume-list-with-format.golden @@ -0,0 +1,3 @@ +baz local foo=bar +foo bar +volume local diff --git a/command/volume/testdata/volume-list-without-format.golden b/command/volume/testdata/volume-list-without-format.golden new file mode 100644 index 000000000..9cf779e82 --- /dev/null +++ b/command/volume/testdata/volume-list-without-format.golden @@ -0,0 +1,4 @@ +DRIVER VOLUME NAME +local baz +bar foo +local volume diff --git a/command/volume/testdata/volume-prune-no.golden b/command/volume/testdata/volume-prune-no.golden new file mode 100644 index 000000000..df5a31597 --- /dev/null +++ b/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/command/volume/testdata/volume-prune-yes.golden b/command/volume/testdata/volume-prune-yes.golden new file mode 100644 index 000000000..9f6054e92 --- /dev/null +++ b/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/command/volume/testdata/volume-prune.deletedVolumes.golden b/command/volume/testdata/volume-prune.deletedVolumes.golden new file mode 100644 index 000000000..fbe996c74 --- /dev/null +++ b/command/volume/testdata/volume-prune.deletedVolumes.golden @@ -0,0 +1,6 @@ +Deleted Volumes: +foo +bar +baz + +Total reclaimed space: 2kB diff --git a/command/volume/testdata/volume-prune.empty.golden b/command/volume/testdata/volume-prune.empty.golden new file mode 100644 index 000000000..6c537e1ac --- /dev/null +++ b/command/volume/testdata/volume-prune.empty.golden @@ -0,0 +1 @@ +Total reclaimed space: 0B diff --git a/internal/test/builders/doc.go b/internal/test/builders/doc.go new file mode 100644 index 000000000..eac991c2e --- /dev/null +++ b/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/internal/test/builders/node.go b/internal/test/builders/node.go index 63fdebba1..040955785 100644 --- a/internal/test/builders/node.go +++ b/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/internal/test/builders/volume.go b/internal/test/builders/volume.go new file mode 100644 index 000000000..9b84df423 --- /dev/null +++ b/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/internal/test/cli.go b/internal/test/cli.go index 06ab053e9..72de42586 100644 --- a/internal/test/cli.go +++ b/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/internal/test/doc.go b/internal/test/doc.go new file mode 100644 index 000000000..41601bd8f --- /dev/null +++ b/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 21d5d1fa9dfa652585f0398fe186557d406fedee Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Thu, 19 Jan 2017 15:27:37 -0800 Subject: [PATCH 458/563] 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 --- command/formatter/service.go | 18 ++++++++++- command/service/create.go | 2 ++ command/service/opts.go | 55 ++++++++++++++++++++++++++++++---- command/service/update.go | 44 +++++++++++++++++++++++++-- command/service/update_test.go | 30 +++++++++++++++++-- 5 files changed, 139 insertions(+), 10 deletions(-) diff --git a/command/formatter/service.go b/command/formatter/service.go index b13c5ee60..b8b476dd6 100644 --- a/command/formatter/service.go +++ b/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/command/service/create.go b/command/service/create.go index ab9086842..c2eb81727 100644 --- a/command/service/create.go +++ b/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/command/service/opts.go b/command/service/opts.go index d8618e73c..060f7017f 100644 --- a/command/service/opts.go +++ b/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/command/service/update.go b/command/service/update.go index 7f461c90a..1300e5e38 100644 --- a/command/service/update.go +++ b/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/command/service/update_test.go b/command/service/update_test.go index f2887e229..422ab33da 100644 --- a/command/service/update_test.go +++ b/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 05a3caff2309cf4b0334f74107f4d8c751028109 Mon Sep 17 00:00:00 2001 From: Doug Davis Date: Tue, 10 Jan 2017 19:27:55 -0800 Subject: [PATCH 459/563] 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 --- command/image/pull.go | 2 +- command/plugin/install.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/command/image/pull.go b/command/image/pull.go index 967beca86..08e2e8b7e 100644 --- a/command/image/pull.go +++ b/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/command/plugin/install.go b/command/plugin/install.go index ebfe1f1ee..d15784f03 100644 --- a/command/plugin/install.go +++ b/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 3f82787403dfedcde10aae809325417a1300dd94 Mon Sep 17 00:00:00 2001 From: yuexiao-wang Date: Wed, 1 Mar 2017 01:28:33 +0800 Subject: [PATCH 460/563] 'docker daemon' deprecation message doesn't use the new versioning scheme Signed-off-by: yuexiao-wang --- command/system/info.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/system/info.go b/command/system/info.go index d9fafd1aa..28d69c286 100644 --- a/command/system/info.go +++ b/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 e94294e902a649b66c77c5641524fc841265d2ce Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Fri, 3 Mar 2017 13:26:00 -0800 Subject: [PATCH 461/563] 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 --- command/formatter/task.go | 23 ++++++++++++++--------- command/formatter/task_test.go | 6 +++--- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/command/formatter/task.go b/command/formatter/task.go index caf765151..2c6e7bb12 100644 --- a/command/formatter/task.go +++ b/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/command/formatter/task_test.go b/command/formatter/task_test.go index c990f6861..8de9d66f5 100644 --- a/command/formatter/task_test.go +++ b/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 5232868f46c0650c9a9cd565c65fbb0744dd91c6 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Wed, 15 Feb 2017 14:53:58 -0800 Subject: [PATCH 462/563] Add support for the "rollback" failure action Signed-off-by: Aaron Lehmann --- command/service/opts.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/service/opts.go b/command/service/opts.go index 36ab2c630..b9ae89ad0 100644 --- a/command/service/opts.go +++ b/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 8de01fb7a8ff5589a57400684cd6fc07fbd89308 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Wed, 15 Feb 2017 16:04:30 -0800 Subject: [PATCH 463/563] Add support for rollback flags Signed-off-by: Aaron Lehmann --- command/formatter/service.go | 44 ++++++++ command/service/inspect_test.go | 1 - command/service/opts.go | 188 ++++++++++++++++++-------------- command/service/update.go | 11 ++ 4 files changed, 160 insertions(+), 84 deletions(-) diff --git a/command/formatter/service.go b/command/formatter/service.go index 421728976..98c760ed7 100644 --- a/command/formatter/service.go +++ b/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/command/service/inspect_test.go b/command/service/inspect_test.go index 34c41ee78..94c96cc16 100644 --- a/command/service/inspect_test.go +++ b/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/command/service/opts.go b/command/service/opts.go index b9ae89ad0..baaa58e1f 100644 --- a/command/service/opts.go +++ b/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/command/service/update.go b/command/service/update.go index 0c19c0713..b52933150 100644 --- a/command/service/update.go +++ b/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 78c204ef798c7380e11ba26e5cd231e04fc6efe4 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Thu, 16 Feb 2017 09:27:01 -0800 Subject: [PATCH 464/563] 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 --- command/service/update.go | 43 ++++++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/command/service/update.go b/command/service/update.go index b52933150..ab8391e03 100644 --- a/command/service/update.go +++ b/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 80a8d7ca26b13745d6a3e83b9bc8d043b09d5ebf Mon Sep 17 00:00:00 2001 From: Nikhil Chawla Date: Mon, 6 Mar 2017 17:31:04 +0530 Subject: [PATCH 465/563] Fixed the typo in the code Signed-off-by: Nikhil Chawla --- cobra.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cobra.go b/cobra.go index 962b31441..b01774f04 100644 --- a/cobra.go +++ b/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 d2d48f3f6993012c03580285c25c86a2e899b685 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 19 Jan 2017 16:48:30 -0500 Subject: [PATCH 466/563] Add expanded mount syntax to Compose schema and types. Signed-off-by: Daniel Nephin --- compose/schema/bindata.go | 2 +- compose/schema/data/config_schema_v3.1.json | 32 ++++++++++- compose/schema/schema.go | 62 +++++++++++++++------ compose/types/types.go | 22 +++++++- 4 files changed, 97 insertions(+), 21 deletions(-) diff --git a/compose/schema/bindata.go b/compose/schema/bindata.go index 0b5aa18b7..e4ef29bc7 100644 --- a/compose/schema/bindata.go +++ b/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/compose/schema/data/config_schema_v3.1.json b/compose/schema/data/config_schema_v3.1.json index b9d422199..c5e48968e 100644 --- a/compose/schema/data/config_schema_v3.1.json +++ b/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/compose/schema/schema.go b/compose/schema/schema.go index ae33c77fb..063956e3a 100644 --- a/compose/schema/schema.go +++ b/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/compose/types/types.go b/compose/types/types.go index ba11faa13..d5454ec2f 100644 --- a/compose/types/types.go +++ b/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 a442213b9229fac39d096133c5e8a2da1102ea3c Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 24 Jan 2017 16:53:36 -0500 Subject: [PATCH 467/563] Parse a volume spec on the client, with support for windows drives Signed-off-by: Daniel Nephin --- compose/loader/volume.go | 119 ++++++++++++++++++++++++++++++ compose/loader/volume_test.go | 134 ++++++++++++++++++++++++++++++++++ compose/types/types.go | 2 +- 3 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 compose/loader/volume.go create mode 100644 compose/loader/volume_test.go diff --git a/compose/loader/volume.go b/compose/loader/volume.go new file mode 100644 index 000000000..3f33492ea --- /dev/null +++ b/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/compose/loader/volume_test.go b/compose/loader/volume_test.go new file mode 100644 index 000000000..0735d5a54 --- /dev/null +++ b/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/compose/types/types.go b/compose/types/types.go index d5454ec2f..307b5fd90 100644 --- a/compose/types/types.go +++ b/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 29f39ea24432d63c413cda75214db7eef8e409eb Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 24 Jan 2017 12:09:53 -0500 Subject: [PATCH 468/563] Support expanded mounts in Compose loader Add a test for loading expanded mount format. Signed-off-by: Daniel Nephin --- compose/loader/loader.go | 38 ++++++++++++++++++------------- compose/loader/loader_test.go | 42 +++++++++++++++++++++++++++++------ compose/types/types.go | 2 +- 3 files changed, 58 insertions(+), 24 deletions(-) diff --git a/compose/loader/loader.go b/compose/loader/loader.go index 2ccef7198..bdc837d9b 100644 --- a/compose/loader/loader.go +++ b/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/compose/loader/loader_test.go b/compose/loader/loader_test.go index 53f4280b6..126832a3b 100644 --- a/compose/loader/loader_test.go +++ b/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/compose/types/types.go b/compose/types/types.go index 307b5fd90..dce13c928 100644 --- a/compose/types/types.go +++ b/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 63c3221dd3a3b15ec1111dec4500cad99312e8a0 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 27 Jan 2017 16:56:45 -0500 Subject: [PATCH 469/563] Convert new compose volume type to swarm mount type Signed-off-by: Daniel Nephin --- compose/convert/volume.go | 145 ++++++++++++--------------------- compose/convert/volume_test.go | 111 ++++++++++++++++--------- compose/schema/schema.go | 7 +- 3 files changed, 128 insertions(+), 135 deletions(-) diff --git a/compose/convert/volume.go b/compose/convert/volume.go index 53c50958f..682b44377 100644 --- a/compose/convert/volume.go +++ b/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/compose/convert/volume_test.go b/compose/convert/volume_test.go index d218e7c2f..705f03f40 100644 --- a/compose/convert/volume_test.go +++ b/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/compose/schema/schema.go b/compose/schema/schema.go index 063956e3a..9a70dc2aa 100644 --- a/compose/schema/schema.go +++ b/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 2238492c513152f7f97c05082ca68d44b33c1e97 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 6 Mar 2017 15:07:20 -0500 Subject: [PATCH 470/563] Some things just need to be line wrapped. Signed-off-by: Daniel Nephin --- command/image/build.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/command/image/build.go b/command/image/build.go index 4639833a9..9fde67141 100644 --- a/command/image/build.go +++ b/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 e43a97cd386bd5dab092b19ebbd037a8202d353d Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 6 Mar 2017 16:01:04 -0500 Subject: [PATCH 471/563] Use opts.MemBytes for flags. Signed-off-by: Daniel Nephin --- command/container/opts.go | 79 +++++++--------------------------- command/container/opts_test.go | 33 +++++++------- command/container/update.go | 62 ++++++-------------------- command/image/build.go | 34 +++------------ 4 files changed, 49 insertions(+), 159 deletions(-) diff --git a/command/container/opts.go b/command/container/opts.go index 0413ae5f7..72d416379 100644 --- a/command/container/opts.go +++ b/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/command/container/opts_test.go b/command/container/opts_test.go index 6ba83c29d..1448dae8d 100644 --- a/command/container/opts_test.go +++ b/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/command/container/update.go b/command/container/update.go index 4a1220a26..b2a44975b 100644 --- a/command/container/update.go +++ b/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/command/image/build.go b/command/image/build.go index 9fde67141..5f7d5d07a 100644 --- a/command/image/build.go +++ b/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 b6f45eb18ea1ad9c7ebc87e684b08dad22be4cc4 Mon Sep 17 00:00:00 2001 From: James Nesbitt Date: Wed, 1 Mar 2017 10:52:00 +0200 Subject: [PATCH 472/563] exported cli compose loader parsing methods Signed-off-by: James Nesbitt --- compose/loader/loader.go | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/compose/loader/loader.go b/compose/loader/loader.go index 2ccef7198..58757354a 100644 --- a/compose/loader/loader.go +++ b/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 789652c41a175767b2e2ff9f3fd14535c43c8354 Mon Sep 17 00:00:00 2001 From: Arash Deshmeh Date: Mon, 20 Feb 2017 01:12:36 -0500 Subject: [PATCH 473/563] stack deploy exits with error if both 'external' and any other options are specified for volumes Signed-off-by: Arash Deshmeh --- compose/loader/loader.go | 18 +++++++++++--- compose/loader/loader_test.go | 44 +++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/compose/loader/loader.go b/compose/loader/loader.go index 2ccef7198..7fbcde672 100644 --- a/compose/loader/loader.go +++ b/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/compose/loader/loader_test.go b/compose/loader/loader_test.go index 53f4280b6..afa2882c3 100644 --- a/compose/loader/loader_test.go +++ b/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 1d379b9691e8339020c463d4eee93b897aa8dc6c Mon Sep 17 00:00:00 2001 From: Andrea Luzzardi Date: Tue, 6 Dec 2016 18:57:22 -0800 Subject: [PATCH 474/563] 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 --- command/idresolver/idresolver.go | 22 +----- command/service/logs.go | 130 +++++++++++++++++++++++++------ 2 files changed, 107 insertions(+), 45 deletions(-) diff --git a/command/idresolver/idresolver.go b/command/idresolver/idresolver.go index 511b1a8f5..ad0d96735 100644 --- a/command/idresolver/idresolver.go +++ b/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/command/service/logs.go b/command/service/logs.go index 19d3d9a48..2f3e6ca90 100644 --- a/command/service/logs.go +++ b/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 3cda347b3de0cbaa9bfe814a7052a7b9847bbceb Mon Sep 17 00:00:00 2001 From: Drew Erny Date: Wed, 8 Mar 2017 16:28:21 -0800 Subject: [PATCH 475/563] Fixed concerns, updated, and rebased PR. Signed-off-by: Drew Erny --- command/service/logs.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/command/service/logs.go b/command/service/logs.go index 2f3e6ca90..5e7cce3e2 100644 --- a/command/service/logs.go +++ b/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 50a10e9bf44fbab40cef95a391d0cac9ed565928 Mon Sep 17 00:00:00 2001 From: Harald Albers Date: Thu, 9 Mar 2017 09:28:19 +0100 Subject: [PATCH 476/563] Fix description of `docker run|create --stop-signal` in help message Signed-off-by: Harald Albers --- command/container/opts.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/container/opts.go b/command/container/opts.go index 0413ae5f7..564a49367 100644 --- a/command/container/opts.go +++ b/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 63bb7d89adb40d8658b42b9c6337ab247c76ebc4 Mon Sep 17 00:00:00 2001 From: Ying Li Date: Thu, 9 Mar 2017 10:45:15 -0800 Subject: [PATCH 477/563] 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 --- command/cli.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/command/cli.go b/command/cli.go index 782c3a507..be38b8acf 100644 --- a/command/cli.go +++ b/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 49570cf783033135dde52c3001f6eafee1446792 Mon Sep 17 00:00:00 2001 From: allencloud Date: Thu, 2 Feb 2017 04:03:58 +0800 Subject: [PATCH 478/563] do not fail fast when executing inspect command Signed-off-by: allencloud --- command/inspect/inspector.go | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/command/inspect/inspector.go b/command/inspect/inspector.go index 1e53671f8..a899da065 100644 --- a/command/inspect/inspector.go +++ b/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 54a5077ca584bf860031d9d1e3a8cffdeda9d6c1 Mon Sep 17 00:00:00 2001 From: Ian Campbell Date: Mon, 6 Mar 2017 17:29:09 +0000 Subject: [PATCH 479/563] 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 --- command/container/stats_helpers.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/command/container/stats_helpers.go b/command/container/stats_helpers.go index 5fad74043..3dc939a13 100644 --- a/command/container/stats_helpers.go +++ b/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 7ce96255fdfeb5d8dfda76b3cc895bd6824f5050 Mon Sep 17 00:00:00 2001 From: Drew Erny Date: Wed, 1 Mar 2017 16:37:25 -0800 Subject: [PATCH 480/563] 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 --- command/service/logs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/service/logs.go b/command/service/logs.go index 19d3d9a48..40e7bd41f 100644 --- a/command/service/logs.go +++ b/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 371839ee5cad72119af20f206a1b5b2118750943 Mon Sep 17 00:00:00 2001 From: Jeremy Chambers Date: Sun, 12 Feb 2017 13:22:01 -0600 Subject: [PATCH 481/563] 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 --- command/formatter/history.go | 113 ++++++++++++++++ command/formatter/history_test.go | 213 ++++++++++++++++++++++++++++++ command/image/history.go | 57 ++------ 3 files changed, 337 insertions(+), 46 deletions(-) create mode 100644 command/formatter/history.go create mode 100644 command/formatter/history_test.go diff --git a/command/formatter/history.go b/command/formatter/history.go new file mode 100644 index 000000000..2b7de399a --- /dev/null +++ b/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/command/formatter/history_test.go b/command/formatter/history_test.go new file mode 100644 index 000000000..299fb1135 --- /dev/null +++ b/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/command/image/history.go b/command/image/history.go index 91c8f75a6..4d964b4d4 100644 --- a/command/image/history.go +++ b/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 c7dd91faf5ac03ecd7f8632a719036517489fe1b Mon Sep 17 00:00:00 2001 From: Boaz Shuster Date: Tue, 7 Feb 2017 14:52:20 +0200 Subject: [PATCH 482/563] Hide command options that are related to Windows Signed-off-by: Boaz Shuster --- command/cli.go | 7 +++++++ command/container/opts.go | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/command/cli.go b/command/cli.go index 782c3a507..783e516f3 100644 --- a/command/cli.go +++ b/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/command/container/opts.go b/command/container/opts.go index 16bb1aa43..4ce872b55 100644 --- a/command/container/opts.go +++ b/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 cd1cde6e77af142e69be02067c2f92bf531e2b86 Mon Sep 17 00:00:00 2001 From: allencloud Date: Fri, 17 Feb 2017 13:34:49 +0800 Subject: [PATCH 483/563] support both endpoint modes in stack Signed-off-by: allencloud --- compose/convert/service.go | 10 +++++++--- compose/convert/service_test.go | 3 ++- compose/types/types.go | 1 + 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/compose/convert/service.go b/compose/convert/service.go index 93b910967..55368e241 100644 --- a/compose/convert/service.go +++ b/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/compose/convert/service_test.go b/compose/convert/service_test.go index 64ccfd038..69fa90dbc 100644 --- a/compose/convert/service_test.go +++ b/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/compose/types/types.go b/compose/types/types.go index dce13c928..d1d762900 100644 --- a/compose/types/types.go +++ b/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 33bfb1e5e5c5f492c1ec5677def64a4b052b6222 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 13 Mar 2017 14:53:13 -0400 Subject: [PATCH 484/563] Move endpoint_mode under deploy and add it to the schema. Signed-off-by: Daniel Nephin --- compose/convert/service.go | 2 +- compose/schema/data/config_schema_v3.1.json | 1 + compose/types/types.go | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/compose/convert/service.go b/compose/convert/service.go index 55368e241..ece6d5c0f 100644 --- a/compose/convert/service.go +++ b/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/compose/schema/data/config_schema_v3.1.json b/compose/schema/data/config_schema_v3.1.json index c5e48968e..72e1d61bb 100644 --- a/compose/schema/data/config_schema_v3.1.json +++ b/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/compose/types/types.go b/compose/types/types.go index d1d762900..e91b5a7ac 100644 --- a/compose/types/types.go +++ b/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 4e388c22d3c721093d844a95da2c4a97ef9a300b Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 7 Mar 2017 17:19:54 -0500 Subject: [PATCH 485/563] Refactor container run cli command. Signed-off-by: Daniel Nephin --- command/container/create.go | 11 ++- command/container/opts.go | 58 +++++++----- command/container/opts_test.go | 7 +- command/container/run.go | 166 ++++++++++++++++++--------------- 4 files changed, 138 insertions(+), 104 deletions(-) diff --git a/command/container/create.go b/command/container/create.go index 9559ba0c0..ef894bad5 100644 --- a/command/container/create.go +++ b/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/command/container/opts.go b/command/container/opts.go index 4ce872b55..febddbc5d 100644 --- a/command/container/opts.go +++ b/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/command/container/opts_test.go b/command/container/opts_test.go index 1448dae8d..3c7753cd0 100644 --- a/command/container/opts_test.go +++ b/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/command/container/run.go b/command/container/run.go index e805ca1a5..fe869f795 100644 --- a/command/container/run.go +++ b/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 2e9b15143a6373d2882c05be88bd14992730814d Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 13 Mar 2017 15:05:30 -0400 Subject: [PATCH 486/563] Add compose file version 3.2 Signed-off-by: Daniel Nephin --- compose/loader/loader_test.go | 18 +- compose/schema/bindata.go | 25 +- compose/schema/data/config_schema_v3.1.json | 50 +-- compose/schema/data/config_schema_v3.2.json | 473 ++++++++++++++++++++ 4 files changed, 512 insertions(+), 54 deletions(-) create mode 100644 compose/schema/data/config_schema_v3.2.json diff --git a/compose/loader/loader_test.go b/compose/loader/loader_test.go index 126832a3b..dba87e5a5 100644 --- a/compose/loader/loader_test.go +++ b/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/compose/schema/bindata.go b/compose/schema/bindata.go index e4ef29bc7..8857e36a8 100644 --- a/compose/schema/bindata.go +++ b/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/compose/schema/data/config_schema_v3.1.json b/compose/schema/data/config_schema_v3.1.json index 72e1d61bb..b7037485f 100644 --- a/compose/schema/data/config_schema_v3.1.json +++ b/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/compose/schema/data/config_schema_v3.2.json b/compose/schema/data/config_schema_v3.2.json new file mode 100644 index 000000000..e47c879a4 --- /dev/null +++ b/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 6c7da0ca57b54f8e30b927c26765fb884e6666ce Mon Sep 17 00:00:00 2001 From: Santhosh Manohar Date: Thu, 9 Mar 2017 11:42:10 -0800 Subject: [PATCH 487/563] Enhance network inspect to show all tasks, local & non-local, in swarm mode Signed-off-by: Santhosh Manohar --- command/network/inspect.go | 8 +++++--- command/stack/deploy_composefile.go | 2 +- command/system/inspect.go | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/command/network/inspect.go b/command/network/inspect.go index 1a86855f7..e58d66b77 100644 --- a/command/network/inspect.go +++ b/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/command/stack/deploy_composefile.go b/command/stack/deploy_composefile.go index 72f9b8aac..3e6249432 100644 --- a/command/stack/deploy_composefile.go +++ b/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/command/system/inspect.go b/command/system/inspect.go index c86e858a2..6bb9cbe04 100644 --- a/command/system/inspect.go +++ b/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 a972d43e481b1a8b0c2215d8c4e52d9b84300272 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Mon, 13 Mar 2017 18:31:48 -0700 Subject: [PATCH 488/563] bump API to 1.28 Signed-off-by: Victor Vieux --- command/service/create.go | 2 +- command/service/opts.go | 14 +++++++------- command/service/update.go | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/command/service/create.go b/command/service/create.go index c2eb81727..fc1ecbd9f 100644 --- a/command/service/create.go +++ b/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/command/service/opts.go b/command/service/opts.go index baaa58e1f..46fe91960 100644 --- a/command/service/opts.go +++ b/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/command/service/update.go b/command/service/update.go index ab8391e03..fc6a229fa 100644 --- a/command/service/update.go +++ b/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 b7ffa960bf8650757daa516b9493b22252b314cf Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Tue, 7 Feb 2017 09:44:47 +0000 Subject: [PATCH 489/563] 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 --- command/stack/deploy_composefile.go | 10 ++++ compose/loader/example2.env | 3 ++ compose/loader/loader.go | 73 ++++++++++++++++--------- compose/loader/loader_test.go | 82 ++++++++++++++++------------- 4 files changed, 104 insertions(+), 64 deletions(-) diff --git a/command/stack/deploy_composefile.go b/command/stack/deploy_composefile.go index 3e6249432..b176b47e0 100644 --- a/command/stack/deploy_composefile.go +++ b/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/compose/loader/example2.env b/compose/loader/example2.env index 0920d5ab0..642334e9f 100644 --- a/compose/loader/example2.env +++ b/compose/loader/example2.env @@ -1 +1,4 @@ BAR=2 + +# overridden in configDetails.Environment +QUX=1 diff --git a/compose/loader/loader.go b/compose/loader/loader.go index 995047e8c..7c8bfa0a2 100644 --- a/compose/loader/loader.go +++ b/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/compose/loader/loader_test.go b/compose/loader/loader_test.go index b9fb10f22..4f424d612 100644 --- a/compose/loader/loader_test.go +++ b/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 146d3eb3049d16058d2ae52464ec944d5ebeb18b Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 14 Mar 2017 12:39:26 -0400 Subject: [PATCH 490/563] 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 --- command/stack/deploy_composefile.go | 18 +++-- compose/convert/service.go | 9 ++- compose/convert/service_test.go | 10 ++- compose/loader/example1.env | 6 +- compose/loader/example2.env | 4 +- compose/loader/full-example.yml | 6 +- compose/loader/loader.go | 79 ++++++++++--------- compose/loader/loader_test.go | 118 +++++++++++++++------------- compose/types/types.go | 15 ++-- 9 files changed, 147 insertions(+), 118 deletions(-) diff --git a/command/stack/deploy_composefile.go b/command/stack/deploy_composefile.go index b176b47e0..f415f42f8 100644 --- a/command/stack/deploy_composefile.go +++ b/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/compose/convert/service.go b/compose/convert/service.go index ab90d7319..6b542f770 100644 --- a/compose/convert/service.go +++ b/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/compose/convert/service_test.go b/compose/convert/service_test.go index 56f495df3..352e9a61b 100644 --- a/compose/convert/service_test.go +++ b/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/compose/loader/example1.env b/compose/loader/example1.env index 3e7a05961..f19ec0df4 100644 --- a/compose/loader/example1.env +++ b/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/compose/loader/example2.env b/compose/loader/example2.env index 642334e9f..f47d1e614 100644 --- a/compose/loader/example2.env +++ b/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/compose/loader/full-example.yml b/compose/loader/full-example.yml index fb5686a38..e8f371601 100644 --- a/compose/loader/full-example.yml +++ b/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/compose/loader/loader.go b/compose/loader/loader.go index 7c8bfa0a2..3edcd8166 100644 --- a/compose/loader/loader.go +++ b/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/compose/loader/loader_test.go b/compose/loader/loader_test.go index 4f424d612..661e2c615 100644 --- a/compose/loader/loader_test.go +++ b/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/compose/types/types.go b/compose/types/types.go index e91b5a7ac..bb12f8497 100644 --- a/compose/types/types.go +++ b/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 b1a98b55af8d3840820037d89dac252860918db0 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 22 Feb 2017 15:43:13 -0500 Subject: [PATCH 491/563] Add --prune to stack deploy. Add to command line reference. Signed-off-by: Daniel Nephin --- command/stack/deploy.go | 24 +++++++++++++ command/stack/deploy_bundlefile.go | 8 +++++ command/stack/deploy_composefile.go | 9 ++++- command/stack/deploy_test.go | 54 +++++++++++++++++++++++++++++ command/stack/remove.go | 6 ++-- compose/convert/compose.go | 6 ++++ internal/test/cli.go | 2 +- 7 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 command/stack/deploy_test.go diff --git a/command/stack/deploy.go b/command/stack/deploy.go index 22557fc45..46af5f63b 100644 --- a/command/stack/deploy.go +++ b/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/command/stack/deploy_bundlefile.go b/command/stack/deploy_bundlefile.go index 5a178c4ab..14e627caf 100644 --- a/command/stack/deploy_bundlefile.go +++ b/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/command/stack/deploy_composefile.go b/command/stack/deploy_composefile.go index 3e6249432..f8951e06e 100644 --- a/command/stack/deploy_composefile.go +++ b/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/command/stack/deploy_test.go b/command/stack/deploy_test.go new file mode 100644 index 000000000..dac135054 --- /dev/null +++ b/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/command/stack/remove.go b/command/stack/remove.go index 966c1aa6b..d466caf2b 100644 --- a/command/stack/remove.go +++ b/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/compose/convert/compose.go b/compose/convert/compose.go index a4571df02..d7208bfc5 100644 --- a/compose/convert/compose.go +++ b/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/internal/test/cli.go b/internal/test/cli.go index 72de42586..610918a65 100644 --- a/internal/test/cli.go +++ b/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 88a99ae70e3cbf3b812302ae6b4f9bca9cd28aeb Mon Sep 17 00:00:00 2001 From: erxian Date: Mon, 13 Mar 2017 15:28:23 +0800 Subject: [PATCH 492/563] misleading default for --update-monitor duration Signed-off-by: erxian --- command/service/opts.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/service/opts.go b/command/service/opts.go index baaa58e1f..0c4d41de1 100644 --- a/command/service/opts.go +++ b/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 bc771127d8cdf3142b48f7bf4bbfd0a391a47796 Mon Sep 17 00:00:00 2001 From: Victor Vieux Date: Wed, 15 Mar 2017 10:25:36 -0700 Subject: [PATCH 493/563] Revert "Planned 1.13 deprecation: email from login" This reverts commit a66efbddb8eaa837cf42aae20b76c08274271dcf. Signed-off-by: Victor Vieux --- command/registry/login.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/command/registry/login.go b/command/registry/login.go index bdcc9a103..5194c7e8c 100644 --- a/command/registry/login.go +++ b/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 59c79325bcc6adb2f620cc877b970cbc73f198dd Mon Sep 17 00:00:00 2001 From: Gaetan de Villele Date: Wed, 15 Mar 2017 13:49:52 -0700 Subject: [PATCH 494/563] improve semantics of utility function in cli/command/service Signed-off-by: Gaetan de Villele --- command/service/create.go | 2 +- command/service/opts.go | 6 ++---- command/service/update.go | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/command/service/create.go b/command/service/create.go index fc1ecbd9f..7fd088493 100644 --- a/command/service/create.go +++ b/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/command/service/opts.go b/command/service/opts.go index 46fe91960..79126217a 100644 --- a/command/service/opts.go +++ b/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/command/service/update.go b/command/service/update.go index fc6a229fa..7c0ef2a81 100644 --- a/command/service/update.go +++ b/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 d5d0d7795bd27cfbb7ca3488ac5e1b8f1d913f0e Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Fri, 3 Mar 2017 13:42:43 +0100 Subject: [PATCH 495/563] Add missing API version annotations to commands Signed-off-by: Sebastiaan van Stijn --- command/node/cmd.go | 1 + command/plugin/cmd.go | 1 + command/plugin/upgrade.go | 1 + command/secret/cmd.go | 1 + command/service/cmd.go | 1 + command/swarm/cmd.go | 1 + command/volume/cmd.go | 1 + 7 files changed, 7 insertions(+) diff --git a/command/node/cmd.go b/command/node/cmd.go index e71b9199a..6bb6c3b28 100644 --- a/command/node/cmd.go +++ b/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/command/plugin/cmd.go b/command/plugin/cmd.go index 92c990a97..33046d2cb 100644 --- a/command/plugin/cmd.go +++ b/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/command/plugin/upgrade.go b/command/plugin/upgrade.go index 07f0c7bb9..46efb096f 100644 --- a/command/plugin/upgrade.go +++ b/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/command/secret/cmd.go b/command/secret/cmd.go index 79e669858..acaef4dca 100644 --- a/command/secret/cmd.go +++ b/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/command/service/cmd.go b/command/service/cmd.go index 796fe926c..51208b80c 100644 --- a/command/service/cmd.go +++ b/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/command/swarm/cmd.go b/command/swarm/cmd.go index 632679c4b..659dbcdf7 100644 --- a/command/swarm/cmd.go +++ b/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/command/volume/cmd.go b/command/volume/cmd.go index 4ef838133..9086c9924 100644 --- a/command/volume/cmd.go +++ b/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 7fe0d2d64d3234904c8ad964f6d69f6ef055e5d9 Mon Sep 17 00:00:00 2001 From: Pure White Date: Thu, 16 Mar 2017 22:33:24 +0800 Subject: [PATCH 496/563] 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 --- command/registry/search.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/registry/search.go b/command/registry/search.go index bbcedbdd9..f534082d3 100644 --- a/command/registry/search.go +++ b/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 d0fb25319b07fa577d14c320fbf40b2d0d826efc Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 16 Mar 2017 13:53:49 -0400 Subject: [PATCH 497/563] Fix compose schema id for v3.2 Signed-off-by: Daniel Nephin --- compose/schema/bindata.go | 2 +- compose/schema/data/config_schema_v3.2.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/compose/schema/bindata.go b/compose/schema/bindata.go index 8857e36a8..e6ce0bfec 100644 --- a/compose/schema/bindata.go +++ b/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/compose/schema/data/config_schema_v3.2.json b/compose/schema/data/config_schema_v3.2.json index e47c879a4..945102f84 100644 --- a/compose/schema/data/config_schema_v3.2.json +++ b/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 395081fc6b10bd55add9058fbbcc07407ef927d8 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Thu, 16 Mar 2017 10:54:18 -0700 Subject: [PATCH 498/563] 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 --- command/service/parse.go | 24 ++++++++---------------- compose/convert/service.go | 19 ++++++++++--------- 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/command/service/parse.go b/command/service/parse.go index ce9b454ed..baf5e2454 100644 --- a/command/service/parse.go +++ b/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/compose/convert/service.go b/compose/convert/service.go index ab90d7319..f7e539ca6 100644 --- a/compose/convert/service.go +++ b/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 2fc6cd4b71b902a65e56b7e0264356fc0d70f8ed Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 17 Mar 2017 06:21:55 +0000 Subject: [PATCH 499/563] compose: update the comment about MappingWithEquals Signed-off-by: Akihiro Suda --- compose/types/types.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/compose/types/types.go b/compose/types/types.go index bb12f8497..3e6651fd3 100644 --- a/compose/types/types.go +++ b/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 1bac314da583ece87b0788e712e95af85dbd6933 Mon Sep 17 00:00:00 2001 From: Boaz Shuster Date: Sun, 5 Mar 2017 13:11:04 +0200 Subject: [PATCH 500/563] Add format to secret ls Signed-off-by: Boaz Shuster --- command/formatter/secret.go | 101 +++++++++++++++++++++++++++++++ command/formatter/secret_test.go | 63 +++++++++++++++++++ command/secret/ls.go | 40 +++++------- config/configfile/file.go | 1 + 4 files changed, 180 insertions(+), 25 deletions(-) create mode 100644 command/formatter/secret.go create mode 100644 command/formatter/secret_test.go diff --git a/command/formatter/secret.go b/command/formatter/secret.go new file mode 100644 index 000000000..7ec6f9a62 --- /dev/null +++ b/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/command/formatter/secret_test.go b/command/formatter/secret_test.go new file mode 100644 index 000000000..722b65056 --- /dev/null +++ b/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/command/secret/ls.go b/command/secret/ls.go index faeab314b..211ebceb5 100644 --- a/command/secret/ls.go +++ b/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/config/configfile/file.go b/config/configfile/file.go index d83434676..e97fbe47b 100644 --- a/config/configfile/file.go +++ b/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 b8d5b0f675526ee002b7cecd26062692bafaf7a3 Mon Sep 17 00:00:00 2001 From: Boaz Shuster Date: Mon, 13 Mar 2017 23:19:46 +0200 Subject: [PATCH 501/563] Use formatter in docker checkpoint ls Signed-off-by: Boaz Shuster --- command/checkpoint/list.go | 18 +++------ command/formatter/checkpoint.go | 52 ++++++++++++++++++++++++++ command/formatter/checkpoint_test.go | 55 ++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 13 deletions(-) create mode 100644 command/formatter/checkpoint.go create mode 100644 command/formatter/checkpoint_test.go diff --git a/command/checkpoint/list.go b/command/checkpoint/list.go index daf834999..20e7d6d73 100644 --- a/command/checkpoint/list.go +++ b/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/command/formatter/checkpoint.go b/command/formatter/checkpoint.go new file mode 100644 index 000000000..041fcafb7 --- /dev/null +++ b/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/command/formatter/checkpoint_test.go b/command/formatter/checkpoint_test.go new file mode 100644 index 000000000..e88c4d013 --- /dev/null +++ b/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 cc44dec589e45755a7fcccc053aff0267d2b9cf0 Mon Sep 17 00:00:00 2001 From: liker12134 Date: Mon, 20 Mar 2017 16:27:51 +0800 Subject: [PATCH 502/563] fixed:go vetting warning unkeyed fields Signed-off-by: Aaron.L.Xu --- command/container/stats_unit_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/container/stats_unit_test.go b/command/container/stats_unit_test.go index 828d634c8..612914c9c 100644 --- a/command/container/stats_unit_test.go +++ b/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 4826a5c3af189b6a5d82ee11973e7bf1bbeddb3e Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 20 Mar 2017 15:39:57 +0100 Subject: [PATCH 503/563] Fixing a small typo in compose loader package Signed-off-by: Vincent Demeester --- compose/loader/loader.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose/loader/loader.go b/compose/loader/loader.go index 3edcd8166..0653691cd 100644 --- a/compose/loader/loader.go +++ b/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 4ab8463fed4f97bed60a2bd2902f755ea3a36fa8 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 14 Mar 2017 17:53:29 -0400 Subject: [PATCH 504/563] Create a new ServerType struct for storing details about the server on the client. Signed-off-by: Daniel Nephin --- command/cli.go | 50 +++++++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/command/cli.go b/command/cli.go index 783e516f3..74d0fa4f7 100644 --- a/command/cli.go +++ b/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 e8be542957cb23dcb23f428c0fb7e649909a02e5 Mon Sep 17 00:00:00 2001 From: "John Howard (VM)" Date: Tue, 21 Mar 2017 15:55:18 -0700 Subject: [PATCH 505/563] Windows: Don't close client stdin handle to avoid hang Signed-off-by: John Howard (VM) --- command/container/hijack.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/command/container/hijack.go b/command/container/hijack.go index ca136f0e4..11acf114f 100644 --- a/command/container/hijack.go +++ b/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 d59f6d09339edfe4295767a435df68de700bd5c7 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Tue, 21 Mar 2017 16:19:59 -0700 Subject: [PATCH 506/563] 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 --- command/node/cmd.go | 13 +++++++++++++ command/node/inspect_test.go | 2 +- command/node/ps.go | 6 ++++-- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/command/node/cmd.go b/command/node/cmd.go index 6bb6c3b28..ea8b40a9a 100644 --- a/command/node/cmd.go +++ b/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/command/node/inspect_test.go b/command/node/inspect_test.go index 91bd41e16..59f7049bd 100644 --- a/command/node/inspect_test.go +++ b/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/command/node/ps.go b/command/node/ps.go index cb0f3efdf..da5725576 100644 --- a/command/node/ps.go +++ b/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 82b04969b74054f53c078112f452024e389c58a7 Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Tue, 21 Mar 2017 19:09:02 -0700 Subject: [PATCH 507/563] Return proper exit code on builder panic Signed-off-by: Tonis Tiigi --- command/image/build.go | 1 + 1 file changed, 1 insertion(+) diff --git a/command/image/build.go b/command/image/build.go index 5f7d5d07a..040a2c229 100644 --- a/command/image/build.go +++ b/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 a1e1ab78d0cda366e3dfda9099b8c5d74a2a3f10 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Wed, 22 Mar 2017 15:42:03 +0100 Subject: [PATCH 508/563] 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 --- compose/interpolation/interpolation.go | 17 +++--- compose/interpolation/interpolation_test.go | 22 ++++---- compose/loader/loader.go | 57 +++++++++------------ compose/loader/loader_test.go | 30 +++++------ compose/types/types.go | 5 +- 5 files changed, 57 insertions(+), 74 deletions(-) diff --git a/compose/interpolation/interpolation.go b/compose/interpolation/interpolation.go index 29c2e0e27..2a89d5748 100644 --- a/compose/interpolation/interpolation.go +++ b/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/compose/interpolation/interpolation_test.go b/compose/interpolation/interpolation_test.go index 1852b9eb4..9b055f470 100644 --- a/compose/interpolation/interpolation_test.go +++ b/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/compose/loader/loader.go b/compose/loader/loader.go index 0653691cd..9085cf65c 100644 --- a/compose/loader/loader.go +++ b/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/compose/loader/loader_test.go b/compose/loader/loader_test.go index 661e2c615..e7e2992ad 100644 --- a/compose/loader/loader_test.go +++ b/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/compose/types/types.go b/compose/types/types.go index 3e6651fd3..19500e195 100644 --- a/compose/types/types.go +++ b/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 fe19bc6891936fc5bb79e9e774c3d6f5e8880c84 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Thu, 23 Mar 2017 16:05:24 +0100 Subject: [PATCH 509/563] 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 --- compose/interpolation/interpolation.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/compose/interpolation/interpolation.go b/compose/interpolation/interpolation.go index 2a89d5748..c8e962b49 100644 --- a/compose/interpolation/interpolation.go +++ b/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 c8f2ef1b1e0c7a609b51b4c98b7c27f5ec601697 Mon Sep 17 00:00:00 2001 From: "John Howard (VM)" Date: Tue, 21 Mar 2017 10:02:16 -0700 Subject: [PATCH 510/563] Windows: Remove --credentialspec flag Signed-off-by: John Howard (VM) --- command/container/opts.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/command/container/opts.go b/command/container/opts.go index febddbc5d..73cde873b 100644 --- a/command/container/opts.go +++ b/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 c70387aebc116fba8092cb383b0cc2eb36356460 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 24 Mar 2017 10:43:28 -0400 Subject: [PATCH 511/563] Cleanup compose convert error messages. Signed-off-by: Daniel Nephin --- compose/convert/service.go | 15 +++++++++------ compose/convert/volume.go | 2 +- compose/convert/volume_test.go | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/compose/convert/service.go b/compose/convert/service.go index 497dbe004..8e31cbe8f 100644 --- a/compose/convert/service.go +++ b/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/compose/convert/volume.go b/compose/convert/volume.go index 682b44377..d6b14283a 100644 --- a/compose/convert/volume.go +++ b/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/compose/convert/volume_test.go b/compose/convert/volume_test.go index 705f03f40..73d642e5f 100644 --- a/compose/convert/volume_test.go +++ b/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 c1b2fad9aa791f8f1f80d2871f64e99fdec1db97 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 24 Mar 2017 12:24:58 -0400 Subject: [PATCH 512/563] Fix external volume error to pass validation. Signed-off-by: Daniel Nephin --- compose/loader/loader.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/compose/loader/loader.go b/compose/loader/loader.go index 9085cf65c..821097bbf 100644 --- a/compose/loader/loader.go +++ b/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 e9d6193dfd90d994ac9ddc84d8cd40d0b80389b1 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 9 Mar 2017 13:23:45 -0500 Subject: [PATCH 513/563] Replace fmt.Errorf() with errors.Errorf() in the cli Signed-off-by: Daniel Nephin --- cobra.go | 3 +- command/bundlefile/bundlefile.go | 7 ++-- command/cli.go | 2 +- command/container/attach.go | 2 +- command/container/cp.go | 5 +-- command/container/create.go | 9 +++-- command/container/diff.go | 2 +- command/container/export.go | 2 +- command/container/kill.go | 2 +- command/container/opts.go | 51 +++++++++++++------------ command/container/opts_test.go | 9 +++-- command/container/pause.go | 2 +- command/container/port.go | 3 +- command/container/rename.go | 4 +- command/container/restart.go | 2 +- command/container/rm.go | 2 +- command/container/run.go | 2 +- command/container/start.go | 4 +- command/container/stats.go | 2 +- command/container/stats_helpers.go | 2 +- command/container/stop.go | 2 +- command/container/unpause.go | 2 +- command/container/update.go | 2 +- command/container/wait.go | 2 +- command/formatter/formatter.go | 6 +-- command/formatter/reflect.go | 11 +++--- command/formatter/service.go | 4 +- command/idresolver/idresolver.go | 5 +-- command/image/build.go | 9 +++-- command/image/build/context.go | 35 ++++++++--------- command/image/load.go | 4 +- command/image/pull.go | 2 +- command/image/remove.go | 3 +- command/image/save.go | 2 +- command/image/trust.go | 12 +++--- command/in.go | 2 +- command/inspect/inspector.go | 10 ++--- command/network/create.go | 19 ++++----- command/node/demote_test.go | 10 ++--- command/node/inspect_test.go | 11 +++--- command/node/list_test.go | 6 +-- command/node/promote_test.go | 10 ++--- command/node/ps.go | 4 +- command/node/ps_test.go | 7 ++-- command/node/remove.go | 3 +- command/node/remove_test.go | 4 +- command/node/update.go | 4 +- command/node/update_test.go | 16 ++++---- command/plugin/create.go | 3 +- command/plugin/enable.go | 3 +- command/plugin/install.go | 6 +-- command/plugin/push.go | 5 +-- command/plugin/upgrade.go | 4 +- command/registry.go | 7 ++-- command/registry/login.go | 3 +- command/secret/create.go | 3 +- command/secret/remove.go | 3 +- command/service/inspect.go | 8 ++-- command/service/logs.go | 11 +++--- command/service/opts.go | 12 +++--- command/service/parse.go | 7 ++-- command/service/ps.go | 4 +- command/service/remove.go | 3 +- command/service/scale.go | 15 ++++---- command/service/trust.go | 3 +- command/service/update.go | 10 ++--- command/stack/deploy.go | 4 +- command/stack/deploy_composefile.go | 6 +-- command/stack/list.go | 3 +- command/stack/opts.go | 5 ++- command/stack/remove.go | 3 +- command/swarm/init.go | 2 +- command/swarm/init_test.go | 11 +++--- command/swarm/join.go | 3 +- command/swarm/join_test.go | 6 +-- command/swarm/join_token.go | 2 +- command/swarm/join_token_test.go | 11 +++--- command/swarm/leave_test.go | 4 +- command/swarm/opts.go | 6 +-- command/swarm/unlock.go | 2 +- command/swarm/unlock_key_test.go | 7 ++-- command/swarm/unlock_test.go | 6 +-- command/swarm/update_test.go | 23 +++++------ command/system/inspect.go | 5 ++- command/volume/create.go | 3 +- command/volume/create_test.go | 14 +++---- command/volume/inspect_test.go | 7 ++-- command/volume/list_test.go | 4 +- command/volume/prune_test.go | 3 +- command/volume/remove.go | 3 +- command/volume/remove_test.go | 4 +- compose/convert/service.go | 8 ++-- compose/loader/loader.go | 30 +++++++-------- compose/schema/bindata.go | 21 +++++----- config/config.go | 12 +++--- config/configfile/file.go | 12 +++--- config/credentials/native_store_test.go | 3 +- required.go | 14 +++---- trust/trust.go | 26 ++++++------- 99 files changed, 370 insertions(+), 337 deletions(-) diff --git a/cobra.go b/cobra.go index b01774f04..c7bb39c43 100644 --- a/cobra.go +++ b/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/command/bundlefile/bundlefile.go b/command/bundlefile/bundlefile.go index 7fd1e4f6c..07e2c8b08 100644 --- a/command/bundlefile/bundlefile.go +++ b/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/command/cli.go b/command/cli.go index 77b05d583..9db5d8d0f 100644 --- a/command/cli.go +++ b/command/cli.go @@ -1,8 +1,8 @@ package command import ( - "errors" "fmt" + "github.com/pkg/errors" "io" "net/http" "os" diff --git a/command/container/attach.go b/command/container/attach.go index 073914dc3..7d2869f76 100644 --- a/command/container/attach.go +++ b/command/container/attach.go @@ -1,7 +1,7 @@ package container import ( - "errors" + "github.com/pkg/errors" "io" "net/http/httputil" diff --git a/command/container/cp.go b/command/container/cp.go index 8df850b36..a1d7110a6 100644 --- a/command/container/cp.go +++ b/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/command/container/create.go b/command/container/create.go index ef894bad5..9222b4060 100644 --- a/command/container/create.go +++ b/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/command/container/diff.go b/command/container/diff.go index 81260b05b..c279c4849 100644 --- a/command/container/diff.go +++ b/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/command/container/export.go b/command/container/export.go index 42f90bbaa..dfb514440 100644 --- a/command/container/export.go +++ b/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/command/container/kill.go b/command/container/kill.go index 5c7f7ba14..32eea6c0b 100644 --- a/command/container/kill.go +++ b/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/command/container/opts.go b/command/container/opts.go index febddbc5d..f7472a398 100644 --- a/command/container/opts.go +++ b/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/command/container/opts_test.go b/command/container/opts_test.go index 3c7753cd0..b628c0b62 100644 --- a/command/container/opts_test.go +++ b/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/command/container/pause.go b/command/container/pause.go index 7d42ca571..742d6d556 100644 --- a/command/container/pause.go +++ b/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/command/container/port.go b/command/container/port.go index dd1a6b245..2793f6bc6 100644 --- a/command/container/port.go +++ b/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/command/container/rename.go b/command/container/rename.go index a24711ad3..07b4852f4 100644 --- a/command/container/rename.go +++ b/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/command/container/restart.go b/command/container/restart.go index 0a3dd9218..7cfc9c0ea 100644 --- a/command/container/restart.go +++ b/command/container/restart.go @@ -1,8 +1,8 @@ package container import ( - "errors" "fmt" + "github.com/pkg/errors" "strings" "time" diff --git a/command/container/rm.go b/command/container/rm.go index c02533d78..7e6fd4588 100644 --- a/command/container/rm.go +++ b/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/command/container/run.go b/command/container/run.go index fe869f795..4fd05c74b 100644 --- a/command/container/run.go +++ b/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/command/container/start.go b/command/container/start.go index f5d8ca0bc..7702cd4a7 100644 --- a/command/container/start.go +++ b/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/command/container/stats.go b/command/container/stats.go index 940a03914..9d2d59a5b 100644 --- a/command/container/stats.go +++ b/command/container/stats.go @@ -1,8 +1,8 @@ package container import ( - "errors" "fmt" + "github.com/pkg/errors" "io" "strings" "sync" diff --git a/command/container/stats_helpers.go b/command/container/stats_helpers.go index 3dc939a13..8f7a924f2 100644 --- a/command/container/stats_helpers.go +++ b/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/command/container/stop.go b/command/container/stop.go index 48fd63a9f..cba20c77a 100644 --- a/command/container/stop.go +++ b/command/container/stop.go @@ -1,8 +1,8 @@ package container import ( - "errors" "fmt" + "github.com/pkg/errors" "strings" "time" diff --git a/command/container/unpause.go b/command/container/unpause.go index 5f342da0d..184299154 100644 --- a/command/container/unpause.go +++ b/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/command/container/update.go b/command/container/update.go index b2a44975b..22b286397 100644 --- a/command/container/update.go +++ b/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/command/container/wait.go b/command/container/wait.go index d8dce6ef1..9b46318f5 100644 --- a/command/container/wait.go +++ b/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/command/formatter/formatter.go b/command/formatter/formatter.go index a151e9c28..3f07aee96 100644 --- a/command/formatter/formatter.go +++ b/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/command/formatter/reflect.go b/command/formatter/reflect.go index 9692bbce7..fd59404d0 100644 --- a/command/formatter/reflect.go +++ b/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/command/formatter/service.go b/command/formatter/service.go index 98c760ed7..4a4bae2cf 100644 --- a/command/formatter/service.go +++ b/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/command/idresolver/idresolver.go b/command/idresolver/idresolver.go index ad0d96735..25c51a27e 100644 --- a/command/idresolver/idresolver.go +++ b/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/command/image/build.go b/command/image/build.go index 040a2c229..b14b0356c 100644 --- a/command/image/build.go +++ b/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/command/image/build/context.go b/command/image/build/context.go index 9ea065adf..85d319e0b 100644 --- a/command/image/build/context.go +++ b/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/command/image/load.go b/command/image/load.go index 988f5106e..24346f126 100644 --- a/command/image/load.go +++ b/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/command/image/pull.go b/command/image/pull.go index 7152fdc52..2c702e898 100644 --- a/command/image/pull.go +++ b/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/command/image/remove.go b/command/image/remove.go index c79ceba7a..48e8d2c2a 100644 --- a/command/image/remove.go +++ b/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/command/image/save.go b/command/image/save.go index bbe82d2a0..f475f17ff 100644 --- a/command/image/save.go +++ b/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/command/image/trust.go b/command/image/trust.go index 8332dd7de..75bae2eb5 100644 --- a/command/image/trust.go +++ b/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/command/in.go b/command/in.go index 7204b7ad0..d12af6fd9 100644 --- a/command/in.go +++ b/command/in.go @@ -1,7 +1,7 @@ package command import ( - "errors" + "github.com/pkg/errors" "io" "os" "runtime" diff --git a/command/inspect/inspector.go b/command/inspect/inspector.go index a899da065..13e584ab4 100644 --- a/command/inspect/inspector.go +++ b/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/command/network/create.go b/command/network/create.go index 21300d783..b2916f6a0 100644 --- a/command/network/create.go +++ b/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/command/node/demote_test.go b/command/node/demote_test.go index 3ba88f41c..710455ff5 100644 --- a/command/node/demote_test.go +++ b/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/command/node/inspect_test.go b/command/node/inspect_test.go index 91bd41e16..004cc0e82 100644 --- a/command/node/inspect_test.go +++ b/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/command/node/list_test.go b/command/node/list_test.go index 237c4be9c..7b657cd73 100644 --- a/command/node/list_test.go +++ b/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/command/node/promote_test.go b/command/node/promote_test.go index ef4666321..9b646724d 100644 --- a/command/node/promote_test.go +++ b/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/command/node/ps.go b/command/node/ps.go index cb0f3efdf..b12f34a3a 100644 --- a/command/node/ps.go +++ b/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/command/node/ps_test.go b/command/node/ps_test.go index 1a1022d21..de6ff7d57 100644 --- a/command/node/ps_test.go +++ b/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/command/node/remove.go b/command/node/remove.go index 0e4963aca..bd429ee45 100644 --- a/command/node/remove.go +++ b/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/command/node/remove_test.go b/command/node/remove_test.go index 54930a276..d7e742aa4 100644 --- a/command/node/remove_test.go +++ b/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/command/node/update.go b/command/node/update.go index aecb88c4a..82668595a 100644 --- a/command/node/update.go +++ b/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/command/node/update_test.go b/command/node/update_test.go index 439ba9443..493a38627 100644 --- a/command/node/update_test.go +++ b/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/command/plugin/create.go b/command/plugin/create.go index e1e6f74ee..b51f1933d 100644 --- a/command/plugin/create.go +++ b/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/command/plugin/enable.go b/command/plugin/enable.go index 77762f402..b1ca48f8f 100644 --- a/command/plugin/enable.go +++ b/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/command/plugin/install.go b/command/plugin/install.go index ed874e17b..18b3fa373 100644 --- a/command/plugin/install.go +++ b/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/command/plugin/push.go b/command/plugin/push.go index f3643b7f1..de4f95cce 100644 --- a/command/plugin/push.go +++ b/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/command/plugin/upgrade.go b/command/plugin/upgrade.go index 46efb096f..cbcbe17ec 100644 --- a/command/plugin/upgrade.go +++ b/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/command/registry.go b/command/registry.go index 411310fa3..e13bba775 100644 --- a/command/registry.go +++ b/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/command/registry/login.go b/command/registry/login.go index f7c7f05da..343d107dc 100644 --- a/command/registry/login.go +++ b/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/command/secret/create.go b/command/secret/create.go index a3248e5df..11a85a22c 100644 --- a/command/secret/create.go +++ b/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/command/secret/remove.go b/command/secret/remove.go index 91ca4388f..9115550d4 100644 --- a/command/secret/remove.go +++ b/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/command/service/inspect.go b/command/service/inspect.go index 7af9b98c3..8247d45af 100644 --- a/command/service/inspect.go +++ b/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/command/service/logs.go b/command/service/logs.go index 5f5090585..1bf5723ae 100644 --- a/command/service/logs.go +++ b/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/command/service/opts.go b/command/service/opts.go index 68981bec3..2afae80c5 100644 --- a/command/service/opts.go +++ b/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/command/service/parse.go b/command/service/parse.go index baf5e2454..f86bebe87 100644 --- a/command/service/parse.go +++ b/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/command/service/ps.go b/command/service/ps.go index c4ff1b9e3..3a53a545d 100644 --- a/command/service/ps.go +++ b/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/command/service/remove.go b/command/service/remove.go index c3fbbabbc..a7b010708 100644 --- a/command/service/remove.go +++ b/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/command/service/scale.go b/command/service/scale.go index cf89e9027..ed76c862f 100644 --- a/command/service/scale.go +++ b/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/command/service/trust.go b/command/service/trust.go index 3fd80ae87..eba52a9dd 100644 --- a/command/service/trust.go +++ b/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/command/service/update.go b/command/service/update.go index 7c0ef2a81..6470d2598 100644 --- a/command/service/update.go +++ b/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/command/stack/deploy.go b/command/stack/deploy.go index 46af5f63b..678917170 100644 --- a/command/stack/deploy.go +++ b/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/command/stack/deploy_composefile.go b/command/stack/deploy_composefile.go index fde1beaa2..10963d184 100644 --- a/command/stack/deploy_composefile.go +++ b/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/command/stack/list.go b/command/stack/list.go index 3d81242b7..f27d5009e 100644 --- a/command/stack/list.go +++ b/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/command/stack/opts.go b/command/stack/opts.go index 996ff68f2..0d7214e96 100644 --- a/command/stack/opts.go +++ b/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/command/stack/remove.go b/command/stack/remove.go index d466caf2b..e976eccda 100644 --- a/command/stack/remove.go +++ b/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/command/swarm/init.go b/command/swarm/init.go index 57dc87380..37d96de11 100644 --- a/command/swarm/init.go +++ b/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/command/swarm/init_test.go b/command/swarm/init_test.go index 4f56de357..c21433bdb 100644 --- a/command/swarm/init_test.go +++ b/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/command/swarm/join.go b/command/swarm/join.go index 3022f6e89..873eaaefa 100644 --- a/command/swarm/join.go +++ b/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/command/swarm/join_test.go b/command/swarm/join_test.go index 66dd6d66b..6d92f0c4f 100644 --- a/command/swarm/join_test.go +++ b/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/command/swarm/join_token.go b/command/swarm/join_token.go index 5c84c7a31..006ea07c3 100644 --- a/command/swarm/join_token.go +++ b/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/command/swarm/join_token_test.go b/command/swarm/join_token_test.go index 624401641..9b10369ad 100644 --- a/command/swarm/join_token_test.go +++ b/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/command/swarm/leave_test.go b/command/swarm/leave_test.go index 09b41b251..93a58887a 100644 --- a/command/swarm/leave_test.go +++ b/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/command/swarm/opts.go b/command/swarm/opts.go index b32cc9210..6eddddcca 100644 --- a/command/swarm/opts.go +++ b/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/command/swarm/unlock.go b/command/swarm/unlock.go index 45dd6e79e..bb3068f1e 100644 --- a/command/swarm/unlock.go +++ b/command/swarm/unlock.go @@ -2,8 +2,8 @@ package swarm import ( "bufio" - "errors" "fmt" + "github.com/pkg/errors" "io" "strings" diff --git a/command/swarm/unlock_key_test.go b/command/swarm/unlock_key_test.go index 17a07d3fb..7b644f70e 100644 --- a/command/swarm/unlock_key_test.go +++ b/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/command/swarm/unlock_test.go b/command/swarm/unlock_test.go index abf858a28..620fecafe 100644 --- a/command/swarm/unlock_test.go +++ b/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/command/swarm/update_test.go b/command/swarm/update_test.go index c8a2860a0..0450c0297 100644 --- a/command/swarm/update_test.go +++ b/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/command/system/inspect.go b/command/system/inspect.go index 6bb9cbe04..b937ea5b9 100644 --- a/command/system/inspect.go +++ b/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/command/volume/create.go b/command/volume/create.go index f7ca36215..8392cf002 100644 --- a/command/volume/create.go +++ b/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/command/volume/create_test.go b/command/volume/create_test.go index b7d5a443a..ccb7ac75b 100644 --- a/command/volume/create_test.go +++ b/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/command/volume/inspect_test.go b/command/volume/inspect_test.go index e2ea7b35d..7c4cce39d 100644 --- a/command/volume/inspect_test.go +++ b/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/command/volume/list_test.go b/command/volume/list_test.go index 2f4a36633..b2306a5d8 100644 --- a/command/volume/list_test.go +++ b/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/command/volume/prune_test.go b/command/volume/prune_test.go index c07834675..dab997f62 100644 --- a/command/volume/prune_test.go +++ b/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/command/volume/remove.go b/command/volume/remove.go index c1267f1ea..683fe8139 100644 --- a/command/volume/remove.go +++ b/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/command/volume/remove_test.go b/command/volume/remove_test.go index b2a106c22..0154a5d55 100644 --- a/command/volume/remove_test.go +++ b/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/compose/convert/service.go b/compose/convert/service.go index 8e31cbe8f..fe9c281ae 100644 --- a/compose/convert/service.go +++ b/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/compose/loader/loader.go b/compose/loader/loader.go index 821097bbf..d69b530e6 100644 --- a/compose/loader/loader.go +++ b/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/compose/schema/bindata.go b/compose/schema/bindata.go index e6ce0bfec..0c6f8340f 100644 --- a/compose/schema/bindata.go +++ b/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/config/config.go b/config/config.go index ab0fa5451..9b21a2c90 100644 --- a/config/config.go +++ b/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/config/configfile/file.go b/config/configfile/file.go index e97fbe47b..cc1c3d0d5 100644 --- a/config/configfile/file.go +++ b/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/config/credentials/native_store_test.go b/config/credentials/native_store_test.go index 7664faf9e..360cc20ef 100644 --- a/config/credentials/native_store_test.go +++ b/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/required.go b/required.go index 8ee02c842..d28af86be 100644 --- a/required.go +++ b/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/trust/trust.go b/trust/trust.go index 777a61118..3c75e485c 100644 --- a/trust/trust.go +++ b/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 0f6dd9c2e8da7a502b4e4306927b12e45de7dcfa Mon Sep 17 00:00:00 2001 From: Alessandro Boch Date: Thu, 9 Mar 2017 11:52:25 -0800 Subject: [PATCH 514/563] Allow user to modify ingress network Signed-off-by: Alessandro Boch --- command/network/create.go | 4 ++++ command/network/remove.go | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/command/network/create.go b/command/network/create.go index 21300d783..2de64c196 100644 --- a/command/network/create.go +++ b/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/command/network/remove.go b/command/network/remove.go index 2034b8709..b5f074a98 100644 --- a/command/network/remove.go +++ b/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 d6490e5de964157aa34c360468a7c872689f7520 Mon Sep 17 00:00:00 2001 From: allencloud Date: Tue, 6 Dec 2016 00:08:43 +0800 Subject: [PATCH 515/563] make secret ls support filters in CLI Signed-off-by: allencloud --- command/secret/ls.go | 7 +++++-- command/service/parse.go | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/command/secret/ls.go b/command/secret/ls.go index 211ebceb5..1d60ff7c4 100644 --- a/command/secret/ls.go +++ b/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/command/service/parse.go b/command/service/parse.go index baf5e2454..77dfb25fb 100644 --- a/command/service/parse.go +++ b/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 aaf865edb5a10a4a5909100d4136b97c04c258f0 Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 27 Mar 2017 11:42:15 +0200 Subject: [PATCH 516/563] 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 --- command/stack/deploy_bundlefile.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/stack/deploy_bundlefile.go b/command/stack/deploy_bundlefile.go index 14e627caf..0f8f8d040 100644 --- a/command/stack/deploy_bundlefile.go +++ b/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 d26a23ceb834757e4ef522625809af4be7ed447f Mon Sep 17 00:00:00 2001 From: Tibor Vass Date: Mon, 27 Mar 2017 18:21:59 -0700 Subject: [PATCH 517/563] Manually reorganize import paths to segregate stdlib and 3rd-party packages Signed-off-by: Tibor Vass --- command/cli.go | 2 +- command/container/attach.go | 2 +- command/container/diff.go | 2 +- command/container/export.go | 2 +- command/container/kill.go | 2 +- command/container/pause.go | 2 +- command/container/restart.go | 2 +- command/container/rm.go | 2 +- command/container/run.go | 2 +- command/container/stats.go | 2 +- command/container/stats_helpers.go | 2 +- command/container/stop.go | 2 +- command/container/unpause.go | 2 +- command/container/update.go | 2 +- command/container/wait.go | 2 +- command/image/pull.go | 5 ++--- command/image/save.go | 5 ++--- command/in.go | 2 +- command/swarm/join_token.go | 5 ++--- command/swarm/unlock.go | 7 +++---- 20 files changed, 25 insertions(+), 29 deletions(-) diff --git a/command/cli.go b/command/cli.go index 9db5d8d0f..e2a89eb0b 100644 --- a/command/cli.go +++ b/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/command/container/attach.go b/command/container/attach.go index 7d2869f76..d37cc7360 100644 --- a/command/container/attach.go +++ b/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/command/container/diff.go b/command/container/diff.go index c279c4849..95926f586 100644 --- a/command/container/diff.go +++ b/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/command/container/export.go b/command/container/export.go index dfb514440..cb0ddfe7a 100644 --- a/command/container/export.go +++ b/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/command/container/kill.go b/command/container/kill.go index 32eea6c0b..4cc3ee0fc 100644 --- a/command/container/kill.go +++ b/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/command/container/pause.go b/command/container/pause.go index 742d6d556..095a0db2c 100644 --- a/command/container/pause.go +++ b/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/command/container/restart.go b/command/container/restart.go index 7cfc9c0ea..73cd2507e 100644 --- a/command/container/restart.go +++ b/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/command/container/rm.go b/command/container/rm.go index 7e6fd4588..887b5c5d3 100644 --- a/command/container/rm.go +++ b/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/command/container/run.go b/command/container/run.go index 4fd05c74b..bab6a9cf1 100644 --- a/command/container/run.go +++ b/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/command/container/stats.go b/command/container/stats.go index 9d2d59a5b..c420e8151 100644 --- a/command/container/stats.go +++ b/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/command/container/stats_helpers.go b/command/container/stats_helpers.go index 8f7a924f2..5cbcf03e4 100644 --- a/command/container/stats_helpers.go +++ b/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/command/container/stop.go b/command/container/stop.go index cba20c77a..32729e1ea 100644 --- a/command/container/stop.go +++ b/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/command/container/unpause.go b/command/container/unpause.go index 184299154..8105b1755 100644 --- a/command/container/unpause.go +++ b/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/command/container/update.go b/command/container/update.go index 22b286397..283cd3314 100644 --- a/command/container/update.go +++ b/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/command/container/wait.go b/command/container/wait.go index 9b46318f5..f978207b9 100644 --- a/command/container/wait.go +++ b/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/command/image/pull.go b/command/image/pull.go index 2c702e898..5dd523c6d 100644 --- a/command/image/pull.go +++ b/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/command/image/save.go b/command/image/save.go index f475f17ff..e01d2c730 100644 --- a/command/image/save.go +++ b/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/command/in.go b/command/in.go index d12af6fd9..50de77ee9 100644 --- a/command/in.go +++ b/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/command/swarm/join_token.go b/command/swarm/join_token.go index 006ea07c3..dc69e909e 100644 --- a/command/swarm/join_token.go +++ b/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/command/swarm/unlock.go b/command/swarm/unlock.go index bb3068f1e..c1d9b9918 100644 --- a/command/swarm/unlock.go +++ b/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 96e610e67a463cdf076c83f1ee23c2d7d0008194 Mon Sep 17 00:00:00 2001 From: Tibor Vass Date: Mon, 27 Mar 2017 18:33:41 -0700 Subject: [PATCH 518/563] Do not replace fmt.Errorf in generated file Signed-off-by: Tibor Vass --- compose/schema/bindata.go | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/compose/schema/bindata.go b/compose/schema/bindata.go index 0c6f8340f..e6ce0bfec 100644 --- a/compose/schema/bindata.go +++ b/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 ba785f32f88bdda396a43ebc5972536b5a0177dc Mon Sep 17 00:00:00 2001 From: Vincent Demeester Date: Mon, 27 Mar 2017 09:58:09 +0200 Subject: [PATCH 519/563] Add support for `--type=secret` in `docker inspect` Signed-off-by: Vincent Demeester --- command/system/inspect.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/command/system/inspect.go b/command/system/inspect.go index 6bb9cbe04..2b5ac2224 100644 --- a/command/system/inspect.go +++ b/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 951fdd11cda0ed76fd435b1d034118b2ab017761 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 7 Dec 2016 14:37:55 -0500 Subject: [PATCH 520/563] Add entrypoint flags to service cli. Signed-off-by: Daniel Nephin --- command/service/opts.go | 29 +++++++++++++++++++++++++++++ command/service/update.go | 13 +++++-------- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/command/service/opts.go b/command/service/opts.go index 2afae80c5..1ff6575c0 100644 --- a/command/service/opts.go +++ b/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/command/service/update.go b/command/service/update.go index 6470d2598..77b980f59 100644 --- a/command/service/update.go +++ b/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 e6445629d7f76897b426257f640b1a78cd00e5b4 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Tue, 28 Mar 2017 14:20:25 -0700 Subject: [PATCH 521/563] 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 --- command/system/info.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/system/info.go b/command/system/info.go index 448fc3051..8498dd8c5 100644 --- a/command/system/info.go +++ b/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 ce972716be1a5a593aaca7b40de705d74d5f359d Mon Sep 17 00:00:00 2001 From: Daniel Zhang Date: Wed, 15 Feb 2017 08:21:40 +0800 Subject: [PATCH 522/563] Docker version output is not consistent when there are downgrades or incompatibilities. Signed-off-by: Daniel Zhang --- command/system/version.go | 50 ++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/command/system/version.go b/command/system/version.go index 569da2188..468db7d03 100644 --- a/command/system/version.go +++ b/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 6fd69bd855df7d24cc4b54acc0184d34e5f0e793 Mon Sep 17 00:00:00 2001 From: Boaz Shuster Date: Mon, 6 Mar 2017 22:45:12 +0200 Subject: [PATCH 523/563] Use formatter in docker diff Signed-off-by: Boaz Shuster --- command/container/diff.go | 22 +++-------- command/formatter/diff.go | 72 ++++++++++++++++++++++++++++++++++ command/formatter/diff_test.go | 59 ++++++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 17 deletions(-) create mode 100644 command/formatter/diff.go create mode 100644 command/formatter/diff_test.go diff --git a/command/container/diff.go b/command/container/diff.go index 95926f586..816a0a56a 100644 --- a/command/container/diff.go +++ b/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/command/formatter/diff.go b/command/formatter/diff.go new file mode 100644 index 000000000..9b4681934 --- /dev/null +++ b/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/command/formatter/diff_test.go b/command/formatter/diff_test.go new file mode 100644 index 000000000..52080354f --- /dev/null +++ b/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 081ac522bd0e9fdc1f3e0f57159d6ea6613860dd Mon Sep 17 00:00:00 2001 From: Misty Stanley-Jones Date: Fri, 31 Mar 2017 13:22:21 -0700 Subject: [PATCH 524/563] Clarify meaning of docker attach Signed-off-by: Misty Stanley-Jones --- command/container/attach.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/container/attach.go b/command/container/attach.go index d37cc7360..0564bdcd0 100644 --- a/command/container/attach.go +++ b/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 d8ab3840e02bb1f0e1f1df4767eb492fcddaac76 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Thu, 16 Feb 2017 17:05:36 -0800 Subject: [PATCH 525/563] 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 --- command/service/create.go | 16 +- command/service/helpers.go | 39 +++ command/service/opts.go | 6 + command/service/progress/progress.go | 409 +++++++++++++++++++++++++++ command/service/update.go | 15 +- 5 files changed, 479 insertions(+), 6 deletions(-) create mode 100644 command/service/helpers.go create mode 100644 command/service/progress/progress.go diff --git a/command/service/create.go b/command/service/create.go index 7fd088493..76b61f6c2 100644 --- a/command/service/create.go +++ b/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/command/service/helpers.go b/command/service/helpers.go new file mode 100644 index 000000000..228936990 --- /dev/null +++ b/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/command/service/opts.go b/command/service/opts.go index 1ff6575c0..cdfe51317 100644 --- a/command/service/opts.go +++ b/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/command/service/progress/progress.go b/command/service/progress/progress.go new file mode 100644 index 000000000..ccc7e60cf --- /dev/null +++ b/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/command/service/update.go b/command/service/update.go index 77b980f59..afa0f807e 100644 --- a/command/service/update.go +++ b/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 64c6b9a938df7b6d32ee4a4dda36b88a46e3f087 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 3 Apr 2017 17:42:16 -0400 Subject: [PATCH 526/563] Fix endpoint mode in Compose format. Signed-off-by: Daniel Nephin --- compose/loader/full-example.yml | 3 ++- compose/loader/loader.go | 4 +--- compose/loader/loader_test.go | 1 + compose/types/types.go | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/compose/loader/full-example.yml b/compose/loader/full-example.yml index e8f371601..3ffbcc3ea 100644 --- a/compose/loader/full-example.yml +++ b/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/compose/loader/loader.go b/compose/loader/loader.go index d69b530e6..2394ff8e2 100644 --- a/compose/loader/loader.go +++ b/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/compose/loader/loader_test.go b/compose/loader/loader_test.go index e7e2992ad..9e042d0a1 100644 --- a/compose/loader/loader_test.go +++ b/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/compose/types/types.go b/compose/types/types.go index 19500e195..1b4c4015e 100644 --- a/compose/types/types.go +++ b/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 e0d4e672a1cb8cce18fa956be52dc2fb0c149673 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Tue, 7 Feb 2017 22:51:33 -0800 Subject: [PATCH 527/563] 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 --- command/formatter/service.go | 23 ++++++++- command/formatter/service_test.go | 80 +++++++++++++++++++++++++++---- 2 files changed, 92 insertions(+), 11 deletions(-) diff --git a/command/formatter/service.go b/command/formatter/service.go index 4a4bae2cf..740bd8d53 100644 --- a/command/formatter/service.go +++ b/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/command/formatter/service_test.go b/command/formatter/service_test.go index d4474297d..93ffc92a3 100644 --- a/command/formatter/service_test.go +++ b/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 b4ca6ebb098b78da7e4a697232ac9eaa4ddc568c Mon Sep 17 00:00:00 2001 From: Drew Erny Date: Tue, 21 Mar 2017 11:35:55 -0700 Subject: [PATCH 528/563] 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 --- command/service/logs.go | 69 ++++++++++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 21 deletions(-) diff --git a/command/service/logs.go b/command/service/logs.go index 1bf5723ae..da2374f9d 100644 --- a/command/service/logs.go +++ b/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 4fc1d6782ce86b0e783aaee34b5540a5b446e3ba Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Tue, 24 Jan 2017 13:17:40 -0800 Subject: [PATCH 529/563] 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 --- command/formatter/node.go | 99 +++++++++++++++++ command/formatter/node_test.go | 188 +++++++++++++++++++++++++++++++++ command/node/list.go | 70 +++--------- command/node/list_test.go | 129 ++++++++++++++++------ config/configfile/file.go | 1 + 5 files changed, 397 insertions(+), 90 deletions(-) create mode 100644 command/formatter/node.go create mode 100644 command/formatter/node_test.go diff --git a/command/formatter/node.go b/command/formatter/node.go new file mode 100644 index 000000000..bd478e57f --- /dev/null +++ b/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/command/formatter/node_test.go b/command/formatter/node_test.go new file mode 100644 index 000000000..e3e341fc8 --- /dev/null +++ b/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/command/node/list.go b/command/node/list.go index d166401ab..9c6224dd1 100644 --- a/command/node/list.go +++ b/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/command/node/list_test.go b/command/node/list_test.go index 7b657cd73..13a21d1b5 100644 --- a/command/node/list_test.go +++ b/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/config/configfile/file.go b/config/configfile/file.go index cc1c3d0d5..f0f692404 100644 --- a/config/configfile/file.go +++ b/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 d5dca7c687ae81c4bec73a5dfdf1049b6e5ed3d8 Mon Sep 17 00:00:00 2001 From: Arash Deshmeh Date: Sat, 1 Apr 2017 03:07:22 -0400 Subject: [PATCH 530/563] added unit tests for package cli/command/secret Signed-off-by: Arash Deshmeh --- command/secret/client_test.go | 44 +++++ command/secret/create.go | 4 +- command/secret/create_test.go | 126 +++++++++++++ command/secret/inspect.go | 4 +- command/secret/inspect_test.go | 149 +++++++++++++++ command/secret/ls.go | 4 +- command/secret/ls_test.go | 172 ++++++++++++++++++ command/secret/remove.go | 4 +- 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 + command/secret/testdata/secret-list.golden | 3 + internal/test/builders/secret.go | 61 +++++++ 20 files changed, 694 insertions(+), 8 deletions(-) create mode 100644 command/secret/client_test.go create mode 100644 command/secret/create_test.go create mode 100644 command/secret/inspect_test.go create mode 100644 command/secret/ls_test.go create mode 100644 command/secret/remove_test.go create mode 100644 command/secret/testdata/secret-create-with-name.golden create mode 100644 command/secret/testdata/secret-inspect-with-format.json-template.golden create mode 100644 command/secret/testdata/secret-inspect-with-format.simple-template.golden create mode 100644 command/secret/testdata/secret-inspect-without-format.multiple-secrets-with-labels.golden create mode 100644 command/secret/testdata/secret-inspect-without-format.single-secret.golden create mode 100644 command/secret/testdata/secret-list-with-config-format.golden create mode 100644 command/secret/testdata/secret-list-with-filter.golden create mode 100644 command/secret/testdata/secret-list-with-format.golden create mode 100644 command/secret/testdata/secret-list-with-quiet-option.golden create mode 100644 command/secret/testdata/secret-list.golden create mode 100644 internal/test/builders/secret.go diff --git a/command/secret/client_test.go b/command/secret/client_test.go new file mode 100644 index 000000000..bb4b412fc --- /dev/null +++ b/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/command/secret/create.go b/command/secret/create.go index 11a85a22c..59b079817 100644 --- a/command/secret/create.go +++ b/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/command/secret/create_test.go b/command/secret/create_test.go new file mode 100644 index 000000000..cbdfd6333 --- /dev/null +++ b/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/command/secret/inspect.go b/command/secret/inspect.go index fb694c5fb..8b3c3c682 100644 --- a/command/secret/inspect.go +++ b/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/command/secret/inspect_test.go b/command/secret/inspect_test.go new file mode 100644 index 000000000..558e23d7c --- /dev/null +++ b/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/command/secret/ls.go b/command/secret/ls.go index 1d60ff7c4..384ee2650 100644 --- a/command/secret/ls.go +++ b/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/command/secret/ls_test.go b/command/secret/ls_test.go new file mode 100644 index 000000000..d9a4324b7 --- /dev/null +++ b/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/command/secret/remove.go b/command/secret/remove.go index 9115550d4..a4b501d17 100644 --- a/command/secret/remove.go +++ b/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/command/secret/remove_test.go b/command/secret/remove_test.go new file mode 100644 index 000000000..92ca9b9b9 --- /dev/null +++ b/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/command/secret/testdata/secret-create-with-name.golden b/command/secret/testdata/secret-create-with-name.golden new file mode 100644 index 000000000..788642a93 --- /dev/null +++ b/command/secret/testdata/secret-create-with-name.golden @@ -0,0 +1 @@ +secret_foo_bar diff --git a/command/secret/testdata/secret-inspect-with-format.json-template.golden b/command/secret/testdata/secret-inspect-with-format.json-template.golden new file mode 100644 index 000000000..aab678f85 --- /dev/null +++ b/command/secret/testdata/secret-inspect-with-format.json-template.golden @@ -0,0 +1 @@ +{"label1":"label-foo"} diff --git a/command/secret/testdata/secret-inspect-with-format.simple-template.golden b/command/secret/testdata/secret-inspect-with-format.simple-template.golden new file mode 100644 index 000000000..257cc5642 --- /dev/null +++ b/command/secret/testdata/secret-inspect-with-format.simple-template.golden @@ -0,0 +1 @@ +foo diff --git a/command/secret/testdata/secret-inspect-without-format.multiple-secrets-with-labels.golden b/command/secret/testdata/secret-inspect-without-format.multiple-secrets-with-labels.golden new file mode 100644 index 000000000..6887c185f --- /dev/null +++ b/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/command/secret/testdata/secret-inspect-without-format.single-secret.golden b/command/secret/testdata/secret-inspect-without-format.single-secret.golden new file mode 100644 index 000000000..ea42ec6f4 --- /dev/null +++ b/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/command/secret/testdata/secret-list-with-config-format.golden b/command/secret/testdata/secret-list-with-config-format.golden new file mode 100644 index 000000000..9a4753880 --- /dev/null +++ b/command/secret/testdata/secret-list-with-config-format.golden @@ -0,0 +1,2 @@ +foo +bar label=label-bar diff --git a/command/secret/testdata/secret-list-with-filter.golden b/command/secret/testdata/secret-list-with-filter.golden new file mode 100644 index 000000000..29983de8e --- /dev/null +++ b/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/command/secret/testdata/secret-list-with-format.golden b/command/secret/testdata/secret-list-with-format.golden new file mode 100644 index 000000000..9a4753880 --- /dev/null +++ b/command/secret/testdata/secret-list-with-format.golden @@ -0,0 +1,2 @@ +foo +bar label=label-bar diff --git a/command/secret/testdata/secret-list-with-quiet-option.golden b/command/secret/testdata/secret-list-with-quiet-option.golden new file mode 100644 index 000000000..83fb6e897 --- /dev/null +++ b/command/secret/testdata/secret-list-with-quiet-option.golden @@ -0,0 +1,2 @@ +ID-foo +ID-bar diff --git a/command/secret/testdata/secret-list.golden b/command/secret/testdata/secret-list.golden new file mode 100644 index 000000000..29983de8e --- /dev/null +++ b/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/internal/test/builders/secret.go b/internal/test/builders/secret.go new file mode 100644 index 000000000..9e0f910e9 --- /dev/null +++ b/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 b807d24e56d6affcad722c5238c6bda1639c9455 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Wed, 8 Mar 2017 10:29:15 -0800 Subject: [PATCH 531/563] 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 --- command/formatter/node.go | 14 ++++++++------ command/formatter/node_test.go | 4 ++-- command/node/list_test.go | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/command/formatter/node.go b/command/formatter/node.go index bd478e57f..6a6fb43c1 100644 --- a/command/formatter/node.go +++ b/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/command/formatter/node_test.go b/command/formatter/node_test.go index e3e341fc8..86f4979d3 100644 --- a/command/formatter/node_test.go +++ b/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/command/node/list_test.go b/command/node/list_test.go index 13a21d1b5..4b8d906c3 100644 --- a/command/node/list_test.go +++ b/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 71d1b0507ea5bfd46ca0f99c1575de53ca049193 Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Tue, 4 Apr 2017 18:16:57 -0700 Subject: [PATCH 532/563] 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 --- command/service/update.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/command/service/update.go b/command/service/update.go index afa0f807e..1933ff38e 100644 --- a/command/service/update.go +++ b/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 02b904588a479f7a101fe6ed6388eeeec38ca8d4 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Fri, 31 Mar 2017 06:41:45 +0000 Subject: [PATCH 533/563] cli: add `--mount` to `docker run` Signed-off-by: Akihiro Suda --- command/container/opts.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/command/container/opts.go b/command/container/opts.go index c8ba4cd25..fc4ac855d 100644 --- a/command/container/opts.go +++ b/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 585e5a000122538d89beb4f050ca0628905c006b Mon Sep 17 00:00:00 2001 From: Arash Deshmeh Date: Sun, 26 Mar 2017 02:23:24 -0400 Subject: [PATCH 534/563] stack rm should accept multiple arguments Signed-off-by: Arash Deshmeh --- command/stack/client_test.go | 153 +++++++++++++++++++++++++++++++++++ command/stack/deploy_test.go | 31 +------ command/stack/remove.go | 72 +++++++++-------- command/stack/remove_test.go | 107 ++++++++++++++++++++++++ 4 files changed, 302 insertions(+), 61 deletions(-) create mode 100644 command/stack/client_test.go create mode 100644 command/stack/remove_test.go diff --git a/command/stack/client_test.go b/command/stack/client_test.go new file mode 100644 index 000000000..0cd8612b6 --- /dev/null +++ b/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/command/stack/deploy_test.go b/command/stack/deploy_test.go index dac135054..328222af5 100644 --- a/command/stack/deploy_test.go +++ b/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/command/stack/remove.go b/command/stack/remove.go index e976eccda..7df4e4c0e 100644 --- a/command/stack/remove.go +++ b/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/command/stack/remove_test.go b/command/stack/remove_test.go new file mode 100644 index 000000000..7f64fb550 --- /dev/null +++ b/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 924af54d98c46d3a5d521d1854def994a8064536 Mon Sep 17 00:00:00 2001 From: David Sheets Date: Tue, 21 Feb 2017 12:07:45 -0800 Subject: [PATCH 535/563] 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 --- command/image/build.go | 66 ++++++++++++++++++++++++++++++---- command/image/build/context.go | 31 ++++++++++------ 2 files changed, 80 insertions(+), 17 deletions(-) diff --git a/command/image/build.go b/command/image/build.go index b14b0356c..f6984619c 100644 --- a/command/image/build.go +++ b/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/command/image/build/context.go b/command/image/build/context.go index 85d319e0b..348c72193 100644 --- a/command/image/build/context.go +++ b/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 596cd38a6e3415bcf5ec12b7bec2dda36770c72c Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 5 Apr 2017 12:09:26 -0400 Subject: [PATCH 536/563] Factor out adding dockerfile from stdin. Signed-off-by: Daniel Nephin --- command/image/build.go | 83 +++++++++++++++++++++++------------------- 1 file changed, 46 insertions(+), 37 deletions(-) diff --git a/command/image/build.go b/command/image/build.go index f6984619c..965acb4b5 100644 --- a/command/image/build.go +++ b/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 a58f798fdf87a6986e3c50538342e3b713421e9a 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 537/563] 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ö --- command/container/opts.go | 16 ++++++++++++---- command/container/opts_test.go | 4 ++-- command/service/opts.go | 18 +++++++++++++----- command/service/opts_test.go | 18 ++++++++++-------- command/service/update.go | 8 ++++++-- command/service/update_test.go | 5 +++++ compose/convert/service.go | 21 ++++++++++++++------- compose/types/types.go | 11 ++++++----- 8 files changed, 68 insertions(+), 33 deletions(-) diff --git a/command/container/opts.go b/command/container/opts.go index fc4ac855d..7480bface 100644 --- a/command/container/opts.go +++ b/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/command/container/opts_test.go b/command/container/opts_test.go index b628c0b62..575b214ed 100644 --- a/command/container/opts_test.go +++ b/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/command/service/opts.go b/command/service/opts.go index cdfe51317..3300f34d8 100644 --- a/command/service/opts.go +++ b/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/command/service/opts_test.go b/command/service/opts_test.go index ac5106793..46db5fc83 100644 --- a/command/service/opts_test.go +++ b/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/command/service/update.go b/command/service/update.go index afa0f807e..b2d77e6ba 100644 --- a/command/service/update.go +++ b/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/command/service/update_test.go b/command/service/update_test.go index d71e065f9..7a588d7fe 100644 --- a/command/service/update_test.go +++ b/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/compose/convert/service.go b/compose/convert/service.go index fe9c281ae..7af24b2ec 100644 --- a/compose/convert/service.go +++ b/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/compose/types/types.go b/compose/types/types.go index 1b4c4015e..1a3772dad 100644 --- a/compose/types/types.go +++ b/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 95b81eb68467897fdfb3cf68187403bfe4e1aa5a Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 6 Apr 2017 10:32:35 -0400 Subject: [PATCH 538/563] Support rw as a volume option in compose file. Signed-off-by: Daniel Nephin --- compose/loader/volume.go | 2 ++ compose/loader/volume_test.go | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/compose/loader/volume.go b/compose/loader/volume.go index 3f33492ea..4dce1b2ef 100644 --- a/compose/loader/volume.go +++ b/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/compose/loader/volume_test.go b/compose/loader/volume_test.go index 0735d5a54..19d19f230 100644 --- a/compose/loader/volume_test.go +++ b/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 41471dfe1cb0432871812deecde650888e401d26 Mon Sep 17 00:00:00 2001 From: Yong Tang Date: Sat, 4 Feb 2017 09:10:05 -0800 Subject: [PATCH 539/563] 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 --- command/container/prune.go | 2 +- command/image/prune.go | 1 + command/network/prune.go | 2 +- command/prune/prune.go | 2 +- command/utils.go | 32 ++++++++++++++++++++++++++++++++ command/volume/prune.go | 16 ++++++++++------ config/configfile/file.go | 1 + 7 files changed, 47 insertions(+), 9 deletions(-) diff --git a/command/container/prune.go b/command/container/prune.go index ca50e2e15..cf12dc71f 100644 --- a/command/container/prune.go +++ b/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/command/image/prune.go b/command/image/prune.go index f17aed741..f86bae39c 100644 --- a/command/image/prune.go +++ b/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/command/network/prune.go b/command/network/prune.go index c5c535992..ec363ab91 100644 --- a/command/network/prune.go +++ b/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/command/prune/prune.go b/command/prune/prune.go index 6314718c6..26153ed7c 100644 --- a/command/prune/prune.go +++ b/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/command/utils.go b/command/utils.go index 4c52ce61b..853fe11c7 100644 --- a/command/utils.go +++ b/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/command/volume/prune.go b/command/volume/prune.go index 7e78c66e0..f7d823ffa 100644 --- a/command/volume/prune.go +++ b/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=