mirror of https://github.com/docker/cli.git
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
cb8723543e
commit
31fb756bb6
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue