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"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
distreference "github.com/docker/distribution/reference"
|
||||||
mounttypes "github.com/docker/docker/api/types/mount"
|
mounttypes "github.com/docker/docker/api/types/mount"
|
||||||
"github.com/docker/docker/api/types/swarm"
|
"github.com/docker/docker/api/types/swarm"
|
||||||
"github.com/docker/docker/cli/command/inspect"
|
"github.com/docker/docker/cli/command/inspect"
|
||||||
|
"github.com/docker/docker/pkg/stringid"
|
||||||
units "github.com/docker/go-units"
|
units "github.com/docker/go-units"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -327,3 +329,93 @@ func (ctx *serviceInspectContext) EndpointMode() string {
|
||||||
func (ctx *serviceInspectContext) Ports() []swarm.PortConfig {
|
func (ctx *serviceInspectContext) Ports() []swarm.PortConfig {
|
||||||
return ctx.Service.Endpoint.Ports
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"text/tabwriter"
|
|
||||||
|
|
||||||
distreference "github.com/docker/distribution/reference"
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
"github.com/docker/docker/api/types/swarm"
|
"github.com/docker/docker/api/types/swarm"
|
||||||
"github.com/docker/docker/cli"
|
"github.com/docker/docker/cli"
|
||||||
"github.com/docker/docker/cli/command"
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/cli/command/formatter"
|
||||||
"github.com/docker/docker/opts"
|
"github.com/docker/docker/opts"
|
||||||
"github.com/docker/docker/pkg/stringid"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
listItemFmt = "%s\t%s\t%s\t%s\t%s\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
type listOptions struct {
|
type listOptions struct {
|
||||||
quiet bool
|
quiet bool
|
||||||
|
format string
|
||||||
filter opts.FilterOpt
|
filter opts.FilterOpt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,6 +35,7 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
|
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs")
|
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")
|
flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
@ -49,13 +44,13 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
func runList(dockerCli *command.DockerCli, opts listOptions) error {
|
func runList(dockerCli *command.DockerCli, opts listOptions) error {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
client := dockerCli.Client()
|
client := dockerCli.Client()
|
||||||
out := dockerCli.Out()
|
|
||||||
|
|
||||||
services, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: opts.filter.Value()})
|
services, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: opts.filter.Value()})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
info := map[string]formatter.ServiceListInfo{}
|
||||||
if len(services) > 0 && !opts.quiet {
|
if len(services) > 0 && !opts.quiet {
|
||||||
// only non-empty services and not quiet, should we call TaskList and NodeList api
|
// only non-empty services and not quiet, should we call TaskList and NodeList api
|
||||||
taskFilter := filters.NewArgs()
|
taskFilter := filters.NewArgs()
|
||||||
|
@ -73,20 +68,30 @@ func runList(dockerCli *command.DockerCli, opts listOptions) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
PrintNotQuiet(out, services, nodes, tasks)
|
info = GetServicesStatus(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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
// GetServicesStatus returns a map of mode and replicas
|
||||||
// Besides this, command `docker stack services xxx` will call this, too.
|
func GetServicesStatus(services []swarm.Service, nodes []swarm.Node, tasks []swarm.Task) map[string]formatter.ServiceListInfo {
|
||||||
func PrintNotQuiet(out io.Writer, services []swarm.Service, nodes []swarm.Node, tasks []swarm.Task) {
|
running := map[string]int{}
|
||||||
|
tasksNoShutdown := map[string]int{}
|
||||||
|
|
||||||
activeNodes := make(map[string]struct{})
|
activeNodes := make(map[string]struct{})
|
||||||
for _, n := range nodes {
|
for _, n := range nodes {
|
||||||
if n.Status.State != swarm.NodeStateDown {
|
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 {
|
for _, task := range tasks {
|
||||||
if task.DesiredState != swarm.TaskStateShutdown {
|
if task.DesiredState != swarm.TaskStateShutdown {
|
||||||
tasksNoShutdown[task.ServiceID]++
|
tasksNoShutdown[task.ServiceID]++
|
||||||
|
@ -107,52 +109,20 @@ func PrintNotQuiet(out io.Writer, services []swarm.Service, nodes []swarm.Node,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
printTable(out, services, running, tasksNoShutdown)
|
info := map[string]formatter.ServiceListInfo{}
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
|
|
||||||
for _, service := range services {
|
for _, service := range services {
|
||||||
mode := ""
|
info[service.ID] = formatter.ServiceListInfo{}
|
||||||
replicas := ""
|
|
||||||
if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil {
|
if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil {
|
||||||
mode = "replicated"
|
info[service.ID] = formatter.ServiceListInfo{
|
||||||
replicas = fmt.Sprintf("%d/%d", running[service.ID], *service.Spec.Mode.Replicated.Replicas)
|
Mode: "replicated",
|
||||||
|
Replicas: fmt.Sprintf("%d/%d", running[service.ID], *service.Spec.Mode.Replicated.Replicas),
|
||||||
|
}
|
||||||
} else if service.Spec.Mode.Global != nil {
|
} else if service.Spec.Mode.Global != nil {
|
||||||
mode = "global"
|
info[service.ID] = formatter.ServiceListInfo{
|
||||||
replicas = fmt.Sprintf("%d/%d", running[service.ID], tasksNoShutdown[service.ID])
|
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,
|
|
||||||
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/api/types/filters"
|
||||||
"github.com/docker/docker/cli"
|
"github.com/docker/docker/cli"
|
||||||
"github.com/docker/docker/cli/command"
|
"github.com/docker/docker/cli/command"
|
||||||
|
"github.com/docker/docker/cli/command/formatter"
|
||||||
"github.com/docker/docker/cli/command/service"
|
"github.com/docker/docker/cli/command/service"
|
||||||
"github.com/docker/docker/opts"
|
"github.com/docker/docker/opts"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
@ -16,6 +17,7 @@ import (
|
||||||
|
|
||||||
type servicesOptions struct {
|
type servicesOptions struct {
|
||||||
quiet bool
|
quiet bool
|
||||||
|
format string
|
||||||
filter opts.FilterOpt
|
filter opts.FilterOpt
|
||||||
namespace string
|
namespace string
|
||||||
}
|
}
|
||||||
|
@ -34,6 +36,7 @@ func newServicesCommand(dockerCli *command.DockerCli) *cobra.Command {
|
||||||
}
|
}
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs")
|
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")
|
flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
@ -57,9 +60,8 @@ func runServices(dockerCli *command.DockerCli, opts servicesOptions) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.quiet {
|
info := map[string]formatter.ServiceListInfo{}
|
||||||
service.PrintQuiet(out, services)
|
if !opts.quiet {
|
||||||
} else {
|
|
||||||
taskFilter := filters.NewArgs()
|
taskFilter := filters.NewArgs()
|
||||||
for _, service := range services {
|
for _, service := range services {
|
||||||
taskFilter.Add("service", service.ID)
|
taskFilter.Add("service", service.ID)
|
||||||
|
@ -69,11 +71,27 @@ func runServices(dockerCli *command.DockerCli, opts servicesOptions) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
nodes, err := client.NodeList(ctx, types.NodeListOptions{})
|
nodes, err := client.NodeList(ctx, types.NodeListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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"`
|
CredentialHelpers map[string]string `json:"credHelpers,omitempty"`
|
||||||
Filename string `json:"-"` // Note: for internal use only
|
Filename string `json:"-"` // Note: for internal use only
|
||||||
ServiceInspectFormat string `json:"serviceInspectFormat,omitempty"`
|
ServiceInspectFormat string `json:"serviceInspectFormat,omitempty"`
|
||||||
|
ServicesFormat string `json:"servicesFormat,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LegacyLoadFromReader reads the non-nested configuration data given and sets up the
|
// 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
|
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)
|
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
|
The property `serviceInspectFormat` specifies the default format for `docker
|
||||||
service inspect` output. When the `--format` flag is not provided with the
|
service inspect` output. When the `--format` flag is not provided with the
|
||||||
`docker service inspect` command, Docker's client uses this property. If this
|
`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}}",
|
"imagesFormat": "table {{.ID}}\\t{{.Repository}}\\t{{.Tag}}\\t{{.CreatedAt}}",
|
||||||
"pluginsFormat": "table {{.ID}}\t{{.Name}}\t{{.Enabled}}",
|
"pluginsFormat": "table {{.ID}}\t{{.Name}}\t{{.Enabled}}",
|
||||||
"statsFormat": "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}",
|
"statsFormat": "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}",
|
||||||
|
"servicesFormat": "table {{.ID}}\t{{.Name}}\t{{.Mode}}",
|
||||||
"serviceInspectFormat": "pretty",
|
"serviceInspectFormat": "pretty",
|
||||||
"detachKeys": "ctrl-e,e",
|
"detachKeys": "ctrl-e,e",
|
||||||
"credsStore": "secretservice",
|
"credsStore": "secretservice",
|
||||||
|
|
|
@ -24,9 +24,10 @@ Aliases:
|
||||||
ls, list
|
ls, list
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-f, --filter value Filter output based on conditions provided
|
-f, --filter filter Filter output based on conditions provided
|
||||||
--help Print usage
|
--format string Pretty-print services using a Go template
|
||||||
-q, --quiet Only display IDs
|
--help Print usage
|
||||||
|
-q, --quiet Only display IDs
|
||||||
```
|
```
|
||||||
|
|
||||||
This command when run targeting a manager, lists services are running in the
|
This command when run targeting a manager, lists services are running in the
|
||||||
|
@ -103,6 +104,34 @@ ID NAME MODE REPLICAS IMAGE
|
||||||
0bcjwfh8ychr redis replicated 1/1 redis:3.0.6
|
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
|
## Related information
|
||||||
|
|
||||||
* [service create](service_create.md)
|
* [service create](service_create.md)
|
||||||
|
|
|
@ -22,9 +22,10 @@ Usage: docker stack services [OPTIONS] STACK
|
||||||
List the services in the stack
|
List the services in the stack
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-f, --filter value Filter output based on conditions provided
|
-f, --filter filter Filter output based on conditions provided
|
||||||
--help Print usage
|
--format string Pretty-print services using a Go template
|
||||||
-q, --quiet Only display IDs
|
--help Print usage
|
||||||
|
-q, --quiet Only display IDs
|
||||||
```
|
```
|
||||||
|
|
||||||
Lists the services that are running as part of the specified stack. This
|
Lists the services that are running as part of the specified stack. This
|
||||||
|
@ -62,6 +63,35 @@ The currently supported filters are:
|
||||||
* name (`--filter name=myapp_web`)
|
* name (`--filter name=myapp_web`)
|
||||||
* label (`--filter label=key=value`)
|
* 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
|
## Related information
|
||||||
|
|
||||||
* [stack deploy](stack_deploy.md)
|
* [stack deploy](stack_deploy.md)
|
||||||
|
|
Loading…
Reference in a new issue