mirror of
				https://github.com/moby/moby.git
				synced 2022-11-09 12:21:53 -05:00 
			
		
		
		
	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 <yong.tang.github@outlook.com>
This commit is contained in:
		
							parent
							
								
									354bd4aadd
								
							
						
					
					
						commit
						000f0403d9
					
				
					 8 changed files with 400 additions and 75 deletions
				
			
		| 
						 | 
				
			
			@ -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
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										177
									
								
								cli/command/formatter/service_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								cli/command/formatter/service_test.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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>: 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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -137,6 +137,13 @@ Docker's client uses this property. If this property is not set, the client
 | 
			
		|||
falls back to the default table format. For a list of supported formatting
 | 
			
		||||
directives, see the [**Formatting** section in the `docker plugin ls` documentation](plugin_ls.md)
 | 
			
		||||
 | 
			
		||||
The property `servicesFormat` specifies the default format for `docker
 | 
			
		||||
service ls` output. When the `--format` flag is not provided with the
 | 
			
		||||
`docker service ls` command, Docker's client uses this property. If this
 | 
			
		||||
property is not set, the client falls back to the default json format. For a
 | 
			
		||||
list of supported formatting directives, see the
 | 
			
		||||
[**Formatting** section in the `docker service ls` documentation](service_ls.md)
 | 
			
		||||
 | 
			
		||||
The property `serviceInspectFormat` specifies the default format for `docker
 | 
			
		||||
service inspect` output. When the `--format` flag is not provided with the
 | 
			
		||||
`docker service inspect` command, Docker's client uses this property. If this
 | 
			
		||||
| 
						 | 
				
			
			@ -194,6 +201,7 @@ Following is a sample `config.json` file:
 | 
			
		|||
      "imagesFormat": "table {{.ID}}\\t{{.Repository}}\\t{{.Tag}}\\t{{.CreatedAt}}",
 | 
			
		||||
      "pluginsFormat": "table {{.ID}}\t{{.Name}}\t{{.Enabled}}",
 | 
			
		||||
      "statsFormat": "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}",
 | 
			
		||||
      "servicesFormat": "table {{.ID}}\t{{.Name}}\t{{.Mode}}",
 | 
			
		||||
      "serviceInspectFormat": "pretty",
 | 
			
		||||
      "detachKeys": "ctrl-e,e",
 | 
			
		||||
      "credsStore": "secretservice",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,7 +24,8 @@ Aliases:
 | 
			
		|||
  ls, list
 | 
			
		||||
 | 
			
		||||
Options:
 | 
			
		||||
  -f, --filter value   Filter output based on conditions provided
 | 
			
		||||
  -f, --filter filter   Filter output based on conditions provided
 | 
			
		||||
      --format string   Pretty-print services using a Go template
 | 
			
		||||
      --help            Print usage
 | 
			
		||||
  -q, --quiet           Only display IDs
 | 
			
		||||
```
 | 
			
		||||
| 
						 | 
				
			
			@ -103,6 +104,34 @@ ID            NAME   MODE        REPLICAS  IMAGE
 | 
			
		|||
0bcjwfh8ychr  redis  replicated  1/1       redis:3.0.6
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Formatting
 | 
			
		||||
 | 
			
		||||
The formatting options (`--format`) pretty-prints services output
 | 
			
		||||
using a Go template.
 | 
			
		||||
 | 
			
		||||
Valid placeholders for the Go template are listed below:
 | 
			
		||||
 | 
			
		||||
Placeholder | Description
 | 
			
		||||
------------|------------------------------------------------------------------------------------------
 | 
			
		||||
`.ID`       | Service ID
 | 
			
		||||
`.Name`     | Service name
 | 
			
		||||
`.Mode`     | Service mode (replicated, global)
 | 
			
		||||
`.Replicas` | Service replicas
 | 
			
		||||
`.Image`    | Service image
 | 
			
		||||
 | 
			
		||||
When using the `--format` option, the `service ls` command will either
 | 
			
		||||
output the data exactly as the template declares or, when using the
 | 
			
		||||
`table` directive, includes column headers as well.
 | 
			
		||||
 | 
			
		||||
The following example uses a template without headers and outputs the
 | 
			
		||||
`ID`, `Mode`, and `Replicas` entries separated by a colon for all services:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
$ docker service ls --format "{{.ID}}: {{.Mode}} {{.Replicas}}"
 | 
			
		||||
0zmvwuiu3vue: replicated 10/10
 | 
			
		||||
fm6uf97exkul: global 5/5
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Related information
 | 
			
		||||
 | 
			
		||||
* [service create](service_create.md)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,7 +22,8 @@ Usage:	docker stack services [OPTIONS] STACK
 | 
			
		|||
List the services in the stack
 | 
			
		||||
 | 
			
		||||
Options:
 | 
			
		||||
  -f, --filter value   Filter output based on conditions provided
 | 
			
		||||
  -f, --filter filter   Filter output based on conditions provided
 | 
			
		||||
      --format string   Pretty-print services using a Go template
 | 
			
		||||
      --help            Print usage
 | 
			
		||||
  -q, --quiet           Only display IDs
 | 
			
		||||
```
 | 
			
		||||
| 
						 | 
				
			
			@ -62,6 +63,35 @@ The currently supported filters are:
 | 
			
		|||
* name (`--filter name=myapp_web`)
 | 
			
		||||
* label (`--filter label=key=value`)
 | 
			
		||||
 | 
			
		||||
## Formatting
 | 
			
		||||
 | 
			
		||||
The formatting options (`--format`) pretty-prints services output
 | 
			
		||||
using a Go template.
 | 
			
		||||
 | 
			
		||||
Valid placeholders for the Go template are listed below:
 | 
			
		||||
 | 
			
		||||
Placeholder | Description
 | 
			
		||||
------------|------------------------------------------------------------------------------------------
 | 
			
		||||
`.ID`       | Service ID
 | 
			
		||||
`.Name`     | Service name
 | 
			
		||||
`.Mode`     | Service mode (replicated, global)
 | 
			
		||||
`.Replicas` | Service replicas
 | 
			
		||||
`.Image`    | Service image
 | 
			
		||||
 | 
			
		||||
When using the `--format` option, the `stack services` command will either
 | 
			
		||||
output the data exactly as the template declares or, when using the
 | 
			
		||||
`table` directive, includes column headers as well.
 | 
			
		||||
 | 
			
		||||
The following example uses a template without headers and outputs the
 | 
			
		||||
`ID`, `Mode`, and `Replicas` entries separated by a colon for all services:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
$ docker stack services --format "{{.ID}}: {{.Mode}} {{.Replicas}}"
 | 
			
		||||
0zmvwuiu3vue: replicated 10/10
 | 
			
		||||
fm6uf97exkul: global 5/5
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## Related information
 | 
			
		||||
 | 
			
		||||
* [stack deploy](stack_deploy.md)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue