From 60092c38899dced0e6a893e0fee5bf0a7d38a8fc Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Thu, 2 Mar 2017 18:26:41 -0800 Subject: [PATCH 1/2] Add support for UpToDate filter, for internal use Signed-off-by: Aaron Lehmann Upstream-commit: 91c86c7e26c40ff3e422adcdd88d1649dd9dbc9b Component: engine --- components/engine/daemon/cluster/filters.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/components/engine/daemon/cluster/filters.go b/components/engine/daemon/cluster/filters.go index 554694da2c..d356a449a1 100644 --- a/components/engine/daemon/cluster/filters.go +++ b/components/engine/daemon/cluster/filters.go @@ -53,6 +53,10 @@ func newListTasksFilters(filter filters.Args, transformFunc func(filters.Args) e "service": true, "node": true, "desired-state": true, + // UpToDate is not meant to be exposed to users. It's for + // internal use in checking create/update progress. Therefore, + // we prefix it with a '_'. + "_up-to-date": true, } if err := filter.Validate(accepted); err != nil { return nil, err @@ -68,6 +72,7 @@ func newListTasksFilters(filter filters.Args, transformFunc func(filters.Args) e Labels: runconfigopts.ConvertKVStringsToMap(filter.Get("label")), ServiceIDs: filter.Get("service"), NodeIDs: filter.Get("node"), + UpToDate: len(filter.Get("_up-to-date")) != 0, } for _, s := range filter.Get("desired-state") { From e84d96f81f2f8dd7a6d0013c1ece656a492c1ace Mon Sep 17 00:00:00 2001 From: Aaron Lehmann Date: Thu, 16 Feb 2017 17:05:36 -0800 Subject: [PATCH 2/2] Synchronous service create and service update Change "service create" and "service update" to wait until the creation or update finishes, when --detach=false is specified. Show progress bars for the overall operation and for each individual task (when there are a small enough number of tasks), unless "-q" / "--quiet" is specified. Signed-off-by: Aaron Lehmann Upstream-commit: 330a0035334871d92207b583c1c36d52a244753f Component: engine --- .../engine/cli/command/service/create.go | 16 +- .../engine/cli/command/service/helpers.go | 39 ++ components/engine/cli/command/service/opts.go | 6 + .../cli/command/service/progress/progress.go | 409 ++++++++++++++++++ .../engine/cli/command/service/update.go | 15 +- .../reference/commandline/service_create.md | 2 + .../reference/commandline/service_update.md | 2 + .../docker_cli_service_create_test.go | 4 +- .../docker_cli_service_health_test.go | 4 +- .../integration-cli/docker_cli_swarm_test.go | 6 +- .../engine/pkg/jsonmessage/jsonmessage.go | 12 +- components/engine/pkg/progress/progress.go | 3 + .../pkg/streamformatter/streamformatter.go | 2 +- 13 files changed, 502 insertions(+), 18 deletions(-) create mode 100644 components/engine/cli/command/service/helpers.go create mode 100644 components/engine/cli/command/service/progress/progress.go diff --git a/components/engine/cli/command/service/create.go b/components/engine/cli/command/service/create.go index 7fd0884930..76b61f6c2e 100644 --- a/components/engine/cli/command/service/create.go +++ b/components/engine/cli/command/service/create.go @@ -7,6 +7,7 @@ import ( "github.com/docker/docker/cli" "github.com/docker/docker/cli/command" "github.com/spf13/cobra" + "github.com/spf13/pflag" "golang.org/x/net/context" ) @@ -22,7 +23,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { if len(args) > 1 { opts.args = args[1:] } - return runCreate(dockerCli, opts) + return runCreate(dockerCli, cmd.Flags(), opts) }, } flags := cmd.Flags() @@ -58,7 +59,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command { return cmd } -func runCreate(dockerCli *command.DockerCli, opts *serviceOptions) error { +func runCreate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *serviceOptions) error { apiClient := dockerCli.Client() createOpts := types.ServiceCreateOptions{} @@ -104,5 +105,14 @@ func runCreate(dockerCli *command.DockerCli, opts *serviceOptions) error { } fmt.Fprintf(dockerCli.Out(), "%s\n", response.ID) - return nil + + if opts.detach { + if !flags.Changed("detach") { + fmt.Fprintln(dockerCli.Err(), "Since --detach=false was not specified, tasks will be created in the background.\n"+ + "In a future release, --detach=false will become the default.") + } + return nil + } + + return waitOnService(ctx, dockerCli, response.ID, opts) } diff --git a/components/engine/cli/command/service/helpers.go b/components/engine/cli/command/service/helpers.go new file mode 100644 index 0000000000..2289369908 --- /dev/null +++ b/components/engine/cli/command/service/helpers.go @@ -0,0 +1,39 @@ +package service + +import ( + "io" + + "github.com/docker/docker/cli/command" + "github.com/docker/docker/cli/command/service/progress" + "github.com/docker/docker/pkg/jsonmessage" + "golang.org/x/net/context" +) + +// waitOnService waits for the service to converge. It outputs a progress bar, +// if appopriate based on the CLI flags. +func waitOnService(ctx context.Context, dockerCli *command.DockerCli, serviceID string, opts *serviceOptions) error { + errChan := make(chan error, 1) + pipeReader, pipeWriter := io.Pipe() + + go func() { + errChan <- progress.ServiceProgress(ctx, dockerCli.Client(), serviceID, pipeWriter) + }() + + if opts.quiet { + go func() { + for { + var buf [1024]byte + if _, err := pipeReader.Read(buf[:]); err != nil { + return + } + } + }() + return <-errChan + } + + err := jsonmessage.DisplayJSONMessagesToStream(pipeReader, dockerCli.Out(), nil) + if err == nil { + err = <-errChan + } + return err +} diff --git a/components/engine/cli/command/service/opts.go b/components/engine/cli/command/service/opts.go index 1ff6575c09..cdfe513177 100644 --- a/components/engine/cli/command/service/opts.go +++ b/components/engine/cli/command/service/opts.go @@ -333,6 +333,9 @@ func convertExtraHostsToSwarmHosts(extraHosts []string) []string { } type serviceOptions struct { + detach bool + quiet bool + name string labels opts.ListOpts containerLabels opts.ListOpts @@ -496,6 +499,9 @@ func (opts *serviceOptions) ToService() (swarm.ServiceSpec, error) { // addServiceFlags adds all flags that are common to both `create` and `update`. // Any flags that are not common are added separately in the individual command func addServiceFlags(flags *pflag.FlagSet, opts *serviceOptions) { + flags.BoolVarP(&opts.detach, "detach", "d", true, "Exit immediately instead of waiting for the service to converge") + flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress progress output") + flags.StringVarP(&opts.workdir, flagWorkdir, "w", "", "Working directory inside the container") flags.StringVarP(&opts.user, flagUser, "u", "", "Username or UID (format: [:])") flags.StringVar(&opts.hostname, flagHostname, "", "Container hostname") diff --git a/components/engine/cli/command/service/progress/progress.go b/components/engine/cli/command/service/progress/progress.go new file mode 100644 index 0000000000..ccc7e60cfc --- /dev/null +++ b/components/engine/cli/command/service/progress/progress.go @@ -0,0 +1,409 @@ +package progress + +import ( + "errors" + "fmt" + "io" + "os" + "os/signal" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/client" + "github.com/docker/docker/pkg/progress" + "github.com/docker/docker/pkg/streamformatter" + "github.com/docker/docker/pkg/stringid" + "golang.org/x/net/context" +) + +var ( + numberedStates = map[swarm.TaskState]int64{ + swarm.TaskStateNew: 1, + swarm.TaskStateAllocated: 2, + swarm.TaskStatePending: 3, + swarm.TaskStateAssigned: 4, + swarm.TaskStateAccepted: 5, + swarm.TaskStatePreparing: 6, + swarm.TaskStateReady: 7, + swarm.TaskStateStarting: 8, + swarm.TaskStateRunning: 9, + } + + longestState int +) + +const ( + maxProgress = 9 + maxProgressBars = 20 +) + +type progressUpdater interface { + update(service swarm.Service, tasks []swarm.Task, activeNodes map[string]swarm.Node, rollback bool) (bool, error) +} + +func init() { + for state := range numberedStates { + if len(state) > longestState { + longestState = len(state) + } + } +} + +func stateToProgress(state swarm.TaskState, rollback bool) int64 { + if !rollback { + return numberedStates[state] + } + return int64(len(numberedStates)) - numberedStates[state] +} + +// ServiceProgress outputs progress information for convergence of a service. +func ServiceProgress(ctx context.Context, client client.APIClient, serviceID string, progressWriter io.WriteCloser) error { + defer progressWriter.Close() + + progressOut := streamformatter.NewJSONStreamFormatter().NewProgressOutput(progressWriter, false) + + sigint := make(chan os.Signal, 1) + signal.Notify(sigint, os.Interrupt) + defer signal.Stop(sigint) + + taskFilter := filters.NewArgs() + taskFilter.Add("service", serviceID) + taskFilter.Add("_up-to-date", "true") + + getUpToDateTasks := func() ([]swarm.Task, error) { + return client.TaskList(ctx, types.TaskListOptions{Filters: taskFilter}) + } + + var ( + updater progressUpdater + converged bool + convergedAt time.Time + monitor = 5 * time.Second + rollback bool + ) + + for { + service, _, err := client.ServiceInspectWithRaw(ctx, serviceID) + if err != nil { + return err + } + + if service.Spec.UpdateConfig != nil && service.Spec.UpdateConfig.Monitor != 0 { + monitor = service.Spec.UpdateConfig.Monitor + } + + if updater == nil { + updater, err = initializeUpdater(service, progressOut) + if err != nil { + return err + } + } + + if service.UpdateStatus != nil { + switch service.UpdateStatus.State { + case swarm.UpdateStateUpdating: + rollback = false + case swarm.UpdateStateCompleted: + if !converged { + return nil + } + case swarm.UpdateStatePaused: + return fmt.Errorf("service update paused: %s", service.UpdateStatus.Message) + case swarm.UpdateStateRollbackStarted: + if !rollback && service.UpdateStatus.Message != "" { + progressOut.WriteProgress(progress.Progress{ + ID: "rollback", + Action: service.UpdateStatus.Message, + }) + } + rollback = true + case swarm.UpdateStateRollbackPaused: + return fmt.Errorf("service rollback paused: %s", service.UpdateStatus.Message) + case swarm.UpdateStateRollbackCompleted: + if !converged { + return fmt.Errorf("service rolled back: %s", service.UpdateStatus.Message) + } + } + } + if converged && time.Since(convergedAt) >= monitor { + return nil + } + + tasks, err := getUpToDateTasks() + if err != nil { + return err + } + + activeNodes, err := getActiveNodes(ctx, client) + if err != nil { + return err + } + + converged, err = updater.update(service, tasks, activeNodes, rollback) + if err != nil { + return err + } + if converged { + if convergedAt.IsZero() { + convergedAt = time.Now() + } + wait := monitor - time.Since(convergedAt) + if wait >= 0 { + progressOut.WriteProgress(progress.Progress{ + // Ideally this would have no ID, but + // the progress rendering code behaves + // poorly on an "action" with no ID. It + // returns the cursor to the beginning + // of the line, so the first character + // may be difficult to read. Then the + // output is overwritten by the shell + // prompt when the command finishes. + ID: "verify", + Action: fmt.Sprintf("Waiting %d seconds to verify that tasks are stable...", wait/time.Second+1), + }) + } + } else { + if !convergedAt.IsZero() { + progressOut.WriteProgress(progress.Progress{ + ID: "verify", + Action: "Detected task failure", + }) + } + convergedAt = time.Time{} + } + + select { + case <-time.After(200 * time.Millisecond): + case <-sigint: + if !converged { + progress.Message(progressOut, "", "Operation continuing in background.") + progress.Messagef(progressOut, "", "Use `docker service ps %s` to check progress.", serviceID) + } + return nil + } + } +} + +func getActiveNodes(ctx context.Context, client client.APIClient) (map[string]swarm.Node, error) { + nodes, err := client.NodeList(ctx, types.NodeListOptions{}) + if err != nil { + return nil, err + } + + activeNodes := make(map[string]swarm.Node) + for _, n := range nodes { + if n.Status.State != swarm.NodeStateDown { + activeNodes[n.ID] = n + } + } + return activeNodes, nil +} + +func initializeUpdater(service swarm.Service, progressOut progress.Output) (progressUpdater, error) { + if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil { + return &replicatedProgressUpdater{ + progressOut: progressOut, + }, nil + } + if service.Spec.Mode.Global != nil { + return &globalProgressUpdater{ + progressOut: progressOut, + }, nil + } + return nil, errors.New("unrecognized service mode") +} + +func writeOverallProgress(progressOut progress.Output, numerator, denominator int, rollback bool) { + if rollback { + progressOut.WriteProgress(progress.Progress{ + ID: "overall progress", + Action: fmt.Sprintf("rolling back update: %d out of %d tasks", numerator, denominator), + }) + return + } + progressOut.WriteProgress(progress.Progress{ + ID: "overall progress", + Action: fmt.Sprintf("%d out of %d tasks", numerator, denominator), + }) +} + +type replicatedProgressUpdater struct { + progressOut progress.Output + + // used for maping slots to a contiguous space + // this also causes progress bars to appear in order + slotMap map[int]int + + initialized bool + done bool +} + +func (u *replicatedProgressUpdater) update(service swarm.Service, tasks []swarm.Task, activeNodes map[string]swarm.Node, rollback bool) (bool, error) { + if service.Spec.Mode.Replicated == nil || service.Spec.Mode.Replicated.Replicas == nil { + return false, errors.New("no replica count") + } + replicas := *service.Spec.Mode.Replicated.Replicas + + if !u.initialized { + u.slotMap = make(map[int]int) + + // Draw progress bars in order + writeOverallProgress(u.progressOut, 0, int(replicas), rollback) + + if replicas <= maxProgressBars { + for i := uint64(1); i <= replicas; i++ { + progress.Update(u.progressOut, fmt.Sprintf("%d/%d", i, replicas), " ") + } + } + u.initialized = true + } + + // If there are multiple tasks with the same slot number, favor the one + // with the *lowest* desired state. This can happen in restart + // scenarios. + tasksBySlot := make(map[int]swarm.Task) + for _, task := range tasks { + if numberedStates[task.DesiredState] == 0 { + continue + } + if existingTask, ok := tasksBySlot[task.Slot]; ok { + if numberedStates[existingTask.DesiredState] <= numberedStates[task.DesiredState] { + continue + } + } + if _, nodeActive := activeNodes[task.NodeID]; nodeActive { + tasksBySlot[task.Slot] = task + } + } + + // If we had reached a converged state, check if we are still converged. + if u.done { + for _, task := range tasksBySlot { + if task.Status.State != swarm.TaskStateRunning { + u.done = false + break + } + } + } + + running := uint64(0) + + for _, task := range tasksBySlot { + mappedSlot := u.slotMap[task.Slot] + if mappedSlot == 0 { + mappedSlot = len(u.slotMap) + 1 + u.slotMap[task.Slot] = mappedSlot + } + + if !u.done && replicas <= maxProgressBars && uint64(mappedSlot) <= replicas { + u.progressOut.WriteProgress(progress.Progress{ + ID: fmt.Sprintf("%d/%d", mappedSlot, replicas), + Action: fmt.Sprintf("%-[1]*s", longestState, task.Status.State), + Current: stateToProgress(task.Status.State, rollback), + Total: maxProgress, + HideCounts: true, + }) + } + if task.Status.State == swarm.TaskStateRunning { + running++ + } + } + + if !u.done { + writeOverallProgress(u.progressOut, int(running), int(replicas), rollback) + + if running == replicas { + u.done = true + } + } + + return running == replicas, nil +} + +type globalProgressUpdater struct { + progressOut progress.Output + + initialized bool + done bool +} + +func (u *globalProgressUpdater) update(service swarm.Service, tasks []swarm.Task, activeNodes map[string]swarm.Node, rollback bool) (bool, error) { + // If there are multiple tasks with the same node ID, favor the one + // with the *lowest* desired state. This can happen in restart + // scenarios. + tasksByNode := make(map[string]swarm.Task) + for _, task := range tasks { + if numberedStates[task.DesiredState] == 0 { + continue + } + if existingTask, ok := tasksByNode[task.NodeID]; ok { + if numberedStates[existingTask.DesiredState] <= numberedStates[task.DesiredState] { + continue + } + } + tasksByNode[task.NodeID] = task + } + + // We don't have perfect knowledge of how many nodes meet the + // constraints for this service. But the orchestrator creates tasks + // for all eligible nodes at the same time, so we should see all those + // nodes represented among the up-to-date tasks. + nodeCount := len(tasksByNode) + + if !u.initialized { + if nodeCount == 0 { + // Two possibilities: either the orchestrator hasn't created + // the tasks yet, or the service doesn't meet constraints for + // any node. Either way, we wait. + u.progressOut.WriteProgress(progress.Progress{ + ID: "overall progress", + Action: "waiting for new tasks", + }) + return false, nil + } + + writeOverallProgress(u.progressOut, 0, nodeCount, rollback) + u.initialized = true + } + + // If we had reached a converged state, check if we are still converged. + if u.done { + for _, task := range tasksByNode { + if task.Status.State != swarm.TaskStateRunning { + u.done = false + break + } + } + } + + running := 0 + + for _, task := range tasksByNode { + if node, nodeActive := activeNodes[task.NodeID]; nodeActive { + if !u.done && nodeCount <= maxProgressBars { + u.progressOut.WriteProgress(progress.Progress{ + ID: stringid.TruncateID(node.ID), + Action: fmt.Sprintf("%-[1]*s", longestState, task.Status.State), + Current: stateToProgress(task.Status.State, rollback), + Total: maxProgress, + HideCounts: true, + }) + } + if task.Status.State == swarm.TaskStateRunning { + running++ + } + } + } + + if !u.done { + writeOverallProgress(u.progressOut, running, nodeCount, rollback) + + if running == nodeCount { + u.done = true + } + } + + return running == nodeCount, nil +} diff --git a/components/engine/cli/command/service/update.go b/components/engine/cli/command/service/update.go index 77b980f599..afa0f807e9 100644 --- a/components/engine/cli/command/service/update.go +++ b/components/engine/cli/command/service/update.go @@ -31,7 +31,7 @@ func newUpdateCommand(dockerCli *command.DockerCli) *cobra.Command { Short: "Update a service", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return runUpdate(dockerCli, cmd.Flags(), args[0]) + return runUpdate(dockerCli, cmd.Flags(), serviceOpts, args[0]) }, } @@ -93,7 +93,7 @@ func newListOptsVar() *opts.ListOpts { return opts.NewListOptsRef(&[]string{}, nil) } -func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID string) error { +func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *serviceOptions, serviceID string) error { apiClient := dockerCli.Client() ctx := context.Background() @@ -195,7 +195,16 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID str } fmt.Fprintf(dockerCli.Out(), "%s\n", serviceID) - return nil + + if opts.detach { + if !flags.Changed("detach") { + fmt.Fprintln(dockerCli.Err(), "Since --detach=false was not specified, tasks will be updated in the background.\n"+ + "In a future release, --detach=false will become the default.") + } + return nil + } + + return waitOnService(ctx, dockerCli, serviceID, opts) } func updateService(flags *pflag.FlagSet, spec *swarm.ServiceSpec) error { diff --git a/components/engine/docs/reference/commandline/service_create.md b/components/engine/docs/reference/commandline/service_create.md index 4c72b3ac6d..1875f0aca7 100644 --- a/components/engine/docs/reference/commandline/service_create.md +++ b/components/engine/docs/reference/commandline/service_create.md @@ -23,6 +23,8 @@ Create a new service Options: --constraint list Placement constraints (default []) --container-label list Container labels (default []) + -d, --detach Exit immediately instead of waiting for the service to converge + (default true) --dns list Set custom DNS servers (default []) --dns-option list Set DNS options (default []) --dns-search list Set custom DNS search domains (default []) diff --git a/components/engine/docs/reference/commandline/service_update.md b/components/engine/docs/reference/commandline/service_update.md index 3c416d2e7a..040d5d95a1 100644 --- a/components/engine/docs/reference/commandline/service_update.md +++ b/components/engine/docs/reference/commandline/service_update.md @@ -26,6 +26,8 @@ Options: --constraint-rm list Remove a constraint (default []) --container-label-add list Add or update a container label (default []) --container-label-rm list Remove a container label by its key (default []) + -d, --detach Exit immediately instead of waiting for the service to converge + (default true) --dns-add list Add or update a custom DNS server (default []) --dns-option-add list Add or update a DNS option (default []) --dns-option-rm list Remove a DNS option (default []) diff --git a/components/engine/integration-cli/docker_cli_service_create_test.go b/components/engine/integration-cli/docker_cli_service_create_test.go index e37c34ca93..1ff9b482cf 100644 --- a/components/engine/integration-cli/docker_cli_service_create_test.go +++ b/components/engine/integration-cli/docker_cli_service_create_test.go @@ -16,7 +16,7 @@ import ( func (s *DockerSwarmSuite) TestServiceCreateMountVolume(c *check.C) { d := s.AddDaemon(c, true, true) - out, err := d.Cmd("service", "create", "--mount", "type=volume,source=foo,target=/foo,volume-nocopy", "busybox", "top") + out, err := d.Cmd("service", "create", "--detach=true", "--mount", "type=volume,source=foo,target=/foo,volume-nocopy", "busybox", "top") c.Assert(err, checker.IsNil, check.Commentf(out)) id := strings.TrimSpace(out) @@ -123,7 +123,7 @@ func (s *DockerSwarmSuite) TestServiceCreateWithSecretSourceTarget(c *check.C) { func (s *DockerSwarmSuite) TestServiceCreateMountTmpfs(c *check.C) { d := s.AddDaemon(c, true, true) - out, err := d.Cmd("service", "create", "--mount", "type=tmpfs,target=/foo,tmpfs-size=1MB", "busybox", "sh", "-c", "mount | grep foo; tail -f /dev/null") + out, err := d.Cmd("service", "create", "--detach=true", "--mount", "type=tmpfs,target=/foo,tmpfs-size=1MB", "busybox", "sh", "-c", "mount | grep foo; tail -f /dev/null") c.Assert(err, checker.IsNil, check.Commentf(out)) id := strings.TrimSpace(out) diff --git a/components/engine/integration-cli/docker_cli_service_health_test.go b/components/engine/integration-cli/docker_cli_service_health_test.go index f7500fa387..9aa619897e 100644 --- a/components/engine/integration-cli/docker_cli_service_health_test.go +++ b/components/engine/integration-cli/docker_cli_service_health_test.go @@ -31,7 +31,7 @@ func (s *DockerSwarmSuite) TestServiceHealthRun(c *check.C) { c.Check(err, check.IsNil) serviceName := "healthServiceRun" - out, err := d.Cmd("service", "create", "--name", serviceName, imageName, "top") + out, err := d.Cmd("service", "create", "--detach=true", "--name", serviceName, imageName, "top") c.Assert(err, checker.IsNil, check.Commentf(out)) id := strings.TrimSpace(out) @@ -92,7 +92,7 @@ func (s *DockerSwarmSuite) TestServiceHealthStart(c *check.C) { c.Check(err, check.IsNil) serviceName := "healthServiceStart" - out, err := d.Cmd("service", "create", "--name", serviceName, imageName, "top") + out, err := d.Cmd("service", "create", "--detach=true", "--name", serviceName, imageName, "top") c.Assert(err, checker.IsNil, check.Commentf(out)) id := strings.TrimSpace(out) diff --git a/components/engine/integration-cli/docker_cli_swarm_test.go b/components/engine/integration-cli/docker_cli_swarm_test.go index 35582fbd31..c079a3ae2f 100644 --- a/components/engine/integration-cli/docker_cli_swarm_test.go +++ b/components/engine/integration-cli/docker_cli_swarm_test.go @@ -1611,13 +1611,13 @@ func (s *DockerSwarmSuite) TestSwarmServicePsMultipleServiceIDs(c *check.C) { d := s.AddDaemon(c, true, true) name1 := "top1" - out, err := d.Cmd("service", "create", "--name", name1, "--replicas=3", "busybox", "top") + out, err := d.Cmd("service", "create", "--detach=true", "--name", name1, "--replicas=3", "busybox", "top") c.Assert(err, checker.IsNil) c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "") id1 := strings.TrimSpace(out) name2 := "top2" - out, err = d.Cmd("service", "create", "--name", name2, "--replicas=3", "busybox", "top") + out, err = d.Cmd("service", "create", "--detach=true", "--name", name2, "--replicas=3", "busybox", "top") c.Assert(err, checker.IsNil) c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "") id2 := strings.TrimSpace(out) @@ -1680,7 +1680,7 @@ func (s *DockerSwarmSuite) TestSwarmServicePsMultipleServiceIDs(c *check.C) { func (s *DockerSwarmSuite) TestSwarmPublishDuplicatePorts(c *check.C) { d := s.AddDaemon(c, true, true) - out, err := d.Cmd("service", "create", "--publish", "5005:80", "--publish", "5006:80", "--publish", "80", "--publish", "80", "busybox", "top") + out, err := d.Cmd("service", "create", "--detach=true", "--publish", "5005:80", "--publish", "5006:80", "--publish", "80", "--publish", "80", "busybox", "top") c.Assert(err, check.IsNil, check.Commentf(out)) id := strings.TrimSpace(out) diff --git a/components/engine/pkg/jsonmessage/jsonmessage.go b/components/engine/pkg/jsonmessage/jsonmessage.go index c6ad345ce4..c3b1371cde 100644 --- a/components/engine/pkg/jsonmessage/jsonmessage.go +++ b/components/engine/pkg/jsonmessage/jsonmessage.go @@ -35,6 +35,8 @@ type JSONProgress struct { Current int64 `json:"current,omitempty"` Total int64 `json:"total,omitempty"` Start int64 `json:"start,omitempty"` + // If true, don't show xB/yB + HideCounts bool `json:"hidecounts,omitempty"` } func (p *JSONProgress) String() string { @@ -71,11 +73,13 @@ func (p *JSONProgress) String() string { pbBox = fmt.Sprintf("[%s>%s] ", strings.Repeat("=", percentage), strings.Repeat(" ", numSpaces)) } - numbersBox = fmt.Sprintf("%8v/%v", current, total) + if !p.HideCounts { + numbersBox = fmt.Sprintf("%8v/%v", current, total) - if p.Current > p.Total { - // remove total display if the reported current is wonky. - numbersBox = fmt.Sprintf("%8v", current) + if p.Current > p.Total { + // remove total display if the reported current is wonky. + numbersBox = fmt.Sprintf("%8v", current) + } } if p.Current > 0 && p.Start > 0 && percentage < 50 { diff --git a/components/engine/pkg/progress/progress.go b/components/engine/pkg/progress/progress.go index fcf31173cf..e78fc120b6 100644 --- a/components/engine/pkg/progress/progress.go +++ b/components/engine/pkg/progress/progress.go @@ -16,6 +16,9 @@ type Progress struct { Current int64 Total int64 + // If true, don't show xB/yB + HideCounts bool + // Aux contains extra information not presented to the user, such as // digests for push signing. Aux interface{} diff --git a/components/engine/pkg/streamformatter/streamformatter.go b/components/engine/pkg/streamformatter/streamformatter.go index ce6ea79dee..f2868441ee 100644 --- a/components/engine/pkg/streamformatter/streamformatter.go +++ b/components/engine/pkg/streamformatter/streamformatter.go @@ -125,7 +125,7 @@ func (out *progressOutput) WriteProgress(prog progress.Progress) error { if prog.Message != "" { formatted = out.sf.FormatStatus(prog.ID, prog.Message) } else { - jsonProgress := jsonmessage.JSONProgress{Current: prog.Current, Total: prog.Total} + jsonProgress := jsonmessage.JSONProgress{Current: prog.Current, Total: prog.Total, HideCounts: prog.HideCounts} formatted = out.sf.FormatProgress(prog.ID, prog.Action, &jsonProgress, prog.Aux) } _, err := out.out.Write(formatted)