Forráskód Böngészése

Merge pull request #31144 from aaronlehmann/synchronous-service-commands

Synchronous service create and service update
Victor Vieux 8 éve
szülő
commit
21ec12b967

+ 13 - 3
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)
 }

+ 39 - 0
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
+}

+ 6 - 0
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: <name|uid>[:<group|gid>])")
 	flags.StringVar(&opts.hostname, flagHostname, "", "Container hostname")

+ 409 - 0
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
+}

+ 12 - 3
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 {

+ 5 - 0
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") {

+ 2 - 0
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 [])

+ 2 - 0
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 [])

+ 2 - 2
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)
 

+ 2 - 2
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)
 

+ 3 - 3
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)
 

+ 8 - 4
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 {

+ 3 - 0
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{}

+ 1 - 1
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)