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>
| ... | ... |
@@ -5,9 +5,11 @@ import ( |
| 5 | 5 |
"strings" |
| 6 | 6 |
"time" |
| 7 | 7 |
|
| 8 |
+ distreference "github.com/docker/distribution/reference" |
|
| 8 | 9 |
mounttypes "github.com/docker/docker/api/types/mount" |
| 9 | 10 |
"github.com/docker/docker/api/types/swarm" |
| 10 | 11 |
"github.com/docker/docker/cli/command/inspect" |
| 12 |
+ "github.com/docker/docker/pkg/stringid" |
|
| 11 | 13 |
units "github.com/docker/go-units" |
| 12 | 14 |
) |
| 13 | 15 |
|
| ... | ... |
@@ -327,3 +329,93 @@ func (ctx *serviceInspectContext) EndpointMode() string {
|
| 327 | 327 |
func (ctx *serviceInspectContext) Ports() []swarm.PortConfig {
|
| 328 | 328 |
return ctx.Service.Endpoint.Ports |
| 329 | 329 |
} |
| 330 |
+ |
|
| 331 |
+const ( |
|
| 332 |
+ defaultServiceTableFormat = "table {{.ID}}\t{{.Name}}\t{{.Mode}}\t{{.Replicas}}\t{{.Image}}"
|
|
| 333 |
+ |
|
| 334 |
+ serviceIDHeader = "ID" |
|
| 335 |
+ modeHeader = "MODE" |
|
| 336 |
+ replicasHeader = "REPLICAS" |
|
| 337 |
+) |
|
| 338 |
+ |
|
| 339 |
+// NewServiceListFormat returns a Format for rendering using a service Context |
|
| 340 |
+func NewServiceListFormat(source string, quiet bool) Format {
|
|
| 341 |
+ switch source {
|
|
| 342 |
+ case TableFormatKey: |
|
| 343 |
+ if quiet {
|
|
| 344 |
+ return defaultQuietFormat |
|
| 345 |
+ } |
|
| 346 |
+ return defaultServiceTableFormat |
|
| 347 |
+ case RawFormatKey: |
|
| 348 |
+ if quiet {
|
|
| 349 |
+ return `id: {{.ID}}`
|
|
| 350 |
+ } |
|
| 351 |
+ return `id: {{.ID}}\nname: {{.Name}}\nmode: {{.Mode}}\nreplicas: {{.Replicas}}\nimage: {{.Image}}\n`
|
|
| 352 |
+ } |
|
| 353 |
+ return Format(source) |
|
| 354 |
+} |
|
| 355 |
+ |
|
| 356 |
+// ServiceListInfo stores the information about mode and replicas to be used by template |
|
| 357 |
+type ServiceListInfo struct {
|
|
| 358 |
+ Mode string |
|
| 359 |
+ Replicas string |
|
| 360 |
+} |
|
| 361 |
+ |
|
| 362 |
+// ServiceListWrite writes the context |
|
| 363 |
+func ServiceListWrite(ctx Context, services []swarm.Service, info map[string]ServiceListInfo) error {
|
|
| 364 |
+ render := func(format func(subContext subContext) error) error {
|
|
| 365 |
+ for _, service := range services {
|
|
| 366 |
+ serviceCtx := &serviceContext{service: service, mode: info[service.ID].Mode, replicas: info[service.ID].Replicas}
|
|
| 367 |
+ if err := format(serviceCtx); err != nil {
|
|
| 368 |
+ return err |
|
| 369 |
+ } |
|
| 370 |
+ } |
|
| 371 |
+ return nil |
|
| 372 |
+ } |
|
| 373 |
+ return ctx.Write(&serviceContext{}, render)
|
|
| 374 |
+} |
|
| 375 |
+ |
|
| 376 |
+type serviceContext struct {
|
|
| 377 |
+ HeaderContext |
|
| 378 |
+ service swarm.Service |
|
| 379 |
+ mode string |
|
| 380 |
+ replicas string |
|
| 381 |
+} |
|
| 382 |
+ |
|
| 383 |
+func (c *serviceContext) MarshalJSON() ([]byte, error) {
|
|
| 384 |
+ return marshalJSON(c) |
|
| 385 |
+} |
|
| 386 |
+ |
|
| 387 |
+func (c *serviceContext) ID() string {
|
|
| 388 |
+ c.AddHeader(serviceIDHeader) |
|
| 389 |
+ return stringid.TruncateID(c.service.ID) |
|
| 390 |
+} |
|
| 391 |
+ |
|
| 392 |
+func (c *serviceContext) Name() string {
|
|
| 393 |
+ c.AddHeader(nameHeader) |
|
| 394 |
+ return c.service.Spec.Name |
|
| 395 |
+} |
|
| 396 |
+ |
|
| 397 |
+func (c *serviceContext) Mode() string {
|
|
| 398 |
+ c.AddHeader(modeHeader) |
|
| 399 |
+ return c.mode |
|
| 400 |
+} |
|
| 401 |
+ |
|
| 402 |
+func (c *serviceContext) Replicas() string {
|
|
| 403 |
+ c.AddHeader(replicasHeader) |
|
| 404 |
+ return c.replicas |
|
| 405 |
+} |
|
| 406 |
+ |
|
| 407 |
+func (c *serviceContext) Image() string {
|
|
| 408 |
+ c.AddHeader(imageHeader) |
|
| 409 |
+ image := c.service.Spec.TaskTemplate.ContainerSpec.Image |
|
| 410 |
+ if ref, err := distreference.ParseNamed(image); err == nil {
|
|
| 411 |
+ // update image string for display |
|
| 412 |
+ namedTagged, ok := ref.(distreference.NamedTagged) |
|
| 413 |
+ if ok {
|
|
| 414 |
+ image = namedTagged.Name() + ":" + namedTagged.Tag() |
|
| 415 |
+ } |
|
| 416 |
+ } |
|
| 417 |
+ |
|
| 418 |
+ return image |
|
| 419 |
+} |
| 330 | 420 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,177 @@ |
| 0 |
+package formatter |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "bytes" |
|
| 4 |
+ "encoding/json" |
|
| 5 |
+ "strings" |
|
| 6 |
+ "testing" |
|
| 7 |
+ |
|
| 8 |
+ "github.com/docker/docker/api/types/swarm" |
|
| 9 |
+ "github.com/docker/docker/pkg/testutil/assert" |
|
| 10 |
+) |
|
| 11 |
+ |
|
| 12 |
+func TestServiceContextWrite(t *testing.T) {
|
|
| 13 |
+ cases := []struct {
|
|
| 14 |
+ context Context |
|
| 15 |
+ expected string |
|
| 16 |
+ }{
|
|
| 17 |
+ // Errors |
|
| 18 |
+ {
|
|
| 19 |
+ Context{Format: "{{InvalidFunction}}"},
|
|
| 20 |
+ `Template parsing error: template: :1: function "InvalidFunction" not defined |
|
| 21 |
+`, |
|
| 22 |
+ }, |
|
| 23 |
+ {
|
|
| 24 |
+ Context{Format: "{{nil}}"},
|
|
| 25 |
+ `Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command |
|
| 26 |
+`, |
|
| 27 |
+ }, |
|
| 28 |
+ // Table format |
|
| 29 |
+ {
|
|
| 30 |
+ Context{Format: NewServiceListFormat("table", false)},
|
|
| 31 |
+ `ID NAME MODE REPLICAS IMAGE |
|
| 32 |
+id_baz baz global 2/4 |
|
| 33 |
+id_bar bar replicated 2/4 |
|
| 34 |
+`, |
|
| 35 |
+ }, |
|
| 36 |
+ {
|
|
| 37 |
+ Context{Format: NewServiceListFormat("table", true)},
|
|
| 38 |
+ `id_baz |
|
| 39 |
+id_bar |
|
| 40 |
+`, |
|
| 41 |
+ }, |
|
| 42 |
+ {
|
|
| 43 |
+ Context{Format: NewServiceListFormat("table {{.Name}}", false)},
|
|
| 44 |
+ `NAME |
|
| 45 |
+baz |
|
| 46 |
+bar |
|
| 47 |
+`, |
|
| 48 |
+ }, |
|
| 49 |
+ {
|
|
| 50 |
+ Context{Format: NewServiceListFormat("table {{.Name}}", true)},
|
|
| 51 |
+ `NAME |
|
| 52 |
+baz |
|
| 53 |
+bar |
|
| 54 |
+`, |
|
| 55 |
+ }, |
|
| 56 |
+ // Raw Format |
|
| 57 |
+ {
|
|
| 58 |
+ Context{Format: NewServiceListFormat("raw", false)},
|
|
| 59 |
+ `id: id_baz |
|
| 60 |
+name: baz |
|
| 61 |
+mode: global |
|
| 62 |
+replicas: 2/4 |
|
| 63 |
+image: |
|
| 64 |
+ |
|
| 65 |
+id: id_bar |
|
| 66 |
+name: bar |
|
| 67 |
+mode: replicated |
|
| 68 |
+replicas: 2/4 |
|
| 69 |
+image: |
|
| 70 |
+ |
|
| 71 |
+`, |
|
| 72 |
+ }, |
|
| 73 |
+ {
|
|
| 74 |
+ Context{Format: NewServiceListFormat("raw", true)},
|
|
| 75 |
+ `id: id_baz |
|
| 76 |
+id: id_bar |
|
| 77 |
+`, |
|
| 78 |
+ }, |
|
| 79 |
+ // Custom Format |
|
| 80 |
+ {
|
|
| 81 |
+ Context{Format: NewServiceListFormat("{{.Name}}", false)},
|
|
| 82 |
+ `baz |
|
| 83 |
+bar |
|
| 84 |
+`, |
|
| 85 |
+ }, |
|
| 86 |
+ } |
|
| 87 |
+ |
|
| 88 |
+ for _, testcase := range cases {
|
|
| 89 |
+ services := []swarm.Service{
|
|
| 90 |
+ {ID: "id_baz", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "baz"}}},
|
|
| 91 |
+ {ID: "id_bar", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "bar"}}},
|
|
| 92 |
+ } |
|
| 93 |
+ info := map[string]ServiceListInfo{
|
|
| 94 |
+ "id_baz": {
|
|
| 95 |
+ Mode: "global", |
|
| 96 |
+ Replicas: "2/4", |
|
| 97 |
+ }, |
|
| 98 |
+ "id_bar": {
|
|
| 99 |
+ Mode: "replicated", |
|
| 100 |
+ Replicas: "2/4", |
|
| 101 |
+ }, |
|
| 102 |
+ } |
|
| 103 |
+ out := bytes.NewBufferString("")
|
|
| 104 |
+ testcase.context.Output = out |
|
| 105 |
+ err := ServiceListWrite(testcase.context, services, info) |
|
| 106 |
+ if err != nil {
|
|
| 107 |
+ assert.Error(t, err, testcase.expected) |
|
| 108 |
+ } else {
|
|
| 109 |
+ assert.Equal(t, out.String(), testcase.expected) |
|
| 110 |
+ } |
|
| 111 |
+ } |
|
| 112 |
+} |
|
| 113 |
+ |
|
| 114 |
+func TestServiceContextWriteJSON(t *testing.T) {
|
|
| 115 |
+ services := []swarm.Service{
|
|
| 116 |
+ {ID: "id_baz", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "baz"}}},
|
|
| 117 |
+ {ID: "id_bar", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "bar"}}},
|
|
| 118 |
+ } |
|
| 119 |
+ info := map[string]ServiceListInfo{
|
|
| 120 |
+ "id_baz": {
|
|
| 121 |
+ Mode: "global", |
|
| 122 |
+ Replicas: "2/4", |
|
| 123 |
+ }, |
|
| 124 |
+ "id_bar": {
|
|
| 125 |
+ Mode: "replicated", |
|
| 126 |
+ Replicas: "2/4", |
|
| 127 |
+ }, |
|
| 128 |
+ } |
|
| 129 |
+ expectedJSONs := []map[string]interface{}{
|
|
| 130 |
+ {"ID": "id_baz", "Name": "baz", "Mode": "global", "Replicas": "2/4", "Image": ""},
|
|
| 131 |
+ {"ID": "id_bar", "Name": "bar", "Mode": "replicated", "Replicas": "2/4", "Image": ""},
|
|
| 132 |
+ } |
|
| 133 |
+ |
|
| 134 |
+ out := bytes.NewBufferString("")
|
|
| 135 |
+ err := ServiceListWrite(Context{Format: "{{json .}}", Output: out}, services, info)
|
|
| 136 |
+ if err != nil {
|
|
| 137 |
+ t.Fatal(err) |
|
| 138 |
+ } |
|
| 139 |
+ for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
|
| 140 |
+ t.Logf("Output: line %d: %s", i, line)
|
|
| 141 |
+ var m map[string]interface{}
|
|
| 142 |
+ if err := json.Unmarshal([]byte(line), &m); err != nil {
|
|
| 143 |
+ t.Fatal(err) |
|
| 144 |
+ } |
|
| 145 |
+ assert.DeepEqual(t, m, expectedJSONs[i]) |
|
| 146 |
+ } |
|
| 147 |
+} |
|
| 148 |
+func TestServiceContextWriteJSONField(t *testing.T) {
|
|
| 149 |
+ services := []swarm.Service{
|
|
| 150 |
+ {ID: "id_baz", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "baz"}}},
|
|
| 151 |
+ {ID: "id_bar", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "bar"}}},
|
|
| 152 |
+ } |
|
| 153 |
+ info := map[string]ServiceListInfo{
|
|
| 154 |
+ "id_baz": {
|
|
| 155 |
+ Mode: "global", |
|
| 156 |
+ Replicas: "2/4", |
|
| 157 |
+ }, |
|
| 158 |
+ "id_bar": {
|
|
| 159 |
+ Mode: "replicated", |
|
| 160 |
+ Replicas: "2/4", |
|
| 161 |
+ }, |
|
| 162 |
+ } |
|
| 163 |
+ out := bytes.NewBufferString("")
|
|
| 164 |
+ err := ServiceListWrite(Context{Format: "{{json .Name}}", Output: out}, services, info)
|
|
| 165 |
+ if err != nil {
|
|
| 166 |
+ t.Fatal(err) |
|
| 167 |
+ } |
|
| 168 |
+ for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
|
| 169 |
+ t.Logf("Output: line %d: %s", i, line)
|
|
| 170 |
+ var s string |
|
| 171 |
+ if err := json.Unmarshal([]byte(line), &s); err != nil {
|
|
| 172 |
+ t.Fatal(err) |
|
| 173 |
+ } |
|
| 174 |
+ assert.Equal(t, s, services[i].Spec.Name) |
|
| 175 |
+ } |
|
| 176 |
+} |
| ... | ... |
@@ -2,27 +2,21 @@ package service |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 | 4 |
"fmt" |
| 5 |
- "io" |
|
| 6 |
- "text/tabwriter" |
|
| 7 | 5 |
|
| 8 |
- distreference "github.com/docker/distribution/reference" |
|
| 9 | 6 |
"github.com/docker/docker/api/types" |
| 10 | 7 |
"github.com/docker/docker/api/types/filters" |
| 11 | 8 |
"github.com/docker/docker/api/types/swarm" |
| 12 | 9 |
"github.com/docker/docker/cli" |
| 13 | 10 |
"github.com/docker/docker/cli/command" |
| 11 |
+ "github.com/docker/docker/cli/command/formatter" |
|
| 14 | 12 |
"github.com/docker/docker/opts" |
| 15 |
- "github.com/docker/docker/pkg/stringid" |
|
| 16 | 13 |
"github.com/spf13/cobra" |
| 17 | 14 |
"golang.org/x/net/context" |
| 18 | 15 |
) |
| 19 | 16 |
|
| 20 |
-const ( |
|
| 21 |
- listItemFmt = "%s\t%s\t%s\t%s\t%s\n" |
|
| 22 |
-) |
|
| 23 |
- |
|
| 24 | 17 |
type listOptions struct {
|
| 25 | 18 |
quiet bool |
| 19 |
+ format string |
|
| 26 | 20 |
filter opts.FilterOpt |
| 27 | 21 |
} |
| 28 | 22 |
|
| ... | ... |
@@ -41,6 +35,7 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
|
| 41 | 41 |
|
| 42 | 42 |
flags := cmd.Flags() |
| 43 | 43 |
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs") |
| 44 |
+ flags.StringVar(&opts.format, "format", "", "Pretty-print services using a Go template") |
|
| 44 | 45 |
flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") |
| 45 | 46 |
|
| 46 | 47 |
return cmd |
| ... | ... |
@@ -49,13 +44,13 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
|
| 49 | 49 |
func runList(dockerCli *command.DockerCli, opts listOptions) error {
|
| 50 | 50 |
ctx := context.Background() |
| 51 | 51 |
client := dockerCli.Client() |
| 52 |
- out := dockerCli.Out() |
|
| 53 | 52 |
|
| 54 | 53 |
services, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: opts.filter.Value()})
|
| 55 | 54 |
if err != nil {
|
| 56 | 55 |
return err |
| 57 | 56 |
} |
| 58 | 57 |
|
| 58 |
+ info := map[string]formatter.ServiceListInfo{}
|
|
| 59 | 59 |
if len(services) > 0 && !opts.quiet {
|
| 60 | 60 |
// only non-empty services and not quiet, should we call TaskList and NodeList api |
| 61 | 61 |
taskFilter := filters.NewArgs() |
| ... | ... |
@@ -73,20 +68,30 @@ func runList(dockerCli *command.DockerCli, opts listOptions) error {
|
| 73 | 73 |
return err |
| 74 | 74 |
} |
| 75 | 75 |
|
| 76 |
- PrintNotQuiet(out, services, nodes, tasks) |
|
| 77 |
- } else if !opts.quiet {
|
|
| 78 |
- // no services and not quiet, print only one line with columns ID, NAME, MODE, REPLICAS... |
|
| 79 |
- PrintNotQuiet(out, services, []swarm.Node{}, []swarm.Task{})
|
|
| 80 |
- } else {
|
|
| 81 |
- PrintQuiet(out, services) |
|
| 76 |
+ info = GetServicesStatus(services, nodes, tasks) |
|
| 77 |
+ } |
|
| 78 |
+ |
|
| 79 |
+ format := opts.format |
|
| 80 |
+ if len(format) == 0 {
|
|
| 81 |
+ if len(dockerCli.ConfigFile().ServicesFormat) > 0 && !opts.quiet {
|
|
| 82 |
+ format = dockerCli.ConfigFile().ServicesFormat |
|
| 83 |
+ } else {
|
|
| 84 |
+ format = formatter.TableFormatKey |
|
| 85 |
+ } |
|
| 82 | 86 |
} |
| 83 | 87 |
|
| 84 |
- return nil |
|
| 88 |
+ servicesCtx := formatter.Context{
|
|
| 89 |
+ Output: dockerCli.Out(), |
|
| 90 |
+ Format: formatter.NewServiceListFormat(format, opts.quiet), |
|
| 91 |
+ } |
|
| 92 |
+ return formatter.ServiceListWrite(servicesCtx, services, info) |
|
| 85 | 93 |
} |
| 86 | 94 |
|
| 87 |
-// PrintNotQuiet shows service list in a non-quiet way. |
|
| 88 |
-// Besides this, command `docker stack services xxx` will call this, too. |
|
| 89 |
-func PrintNotQuiet(out io.Writer, services []swarm.Service, nodes []swarm.Node, tasks []swarm.Task) {
|
|
| 95 |
+// GetServicesStatus returns a map of mode and replicas |
|
| 96 |
+func GetServicesStatus(services []swarm.Service, nodes []swarm.Node, tasks []swarm.Task) map[string]formatter.ServiceListInfo {
|
|
| 97 |
+ running := map[string]int{}
|
|
| 98 |
+ tasksNoShutdown := map[string]int{}
|
|
| 99 |
+ |
|
| 90 | 100 |
activeNodes := make(map[string]struct{})
|
| 91 | 101 |
for _, n := range nodes {
|
| 92 | 102 |
if n.Status.State != swarm.NodeStateDown {
|
| ... | ... |
@@ -94,9 +99,6 @@ func PrintNotQuiet(out io.Writer, services []swarm.Service, nodes []swarm.Node, |
| 94 | 94 |
} |
| 95 | 95 |
} |
| 96 | 96 |
|
| 97 |
- running := map[string]int{}
|
|
| 98 |
- tasksNoShutdown := map[string]int{}
|
|
| 99 |
- |
|
| 100 | 97 |
for _, task := range tasks {
|
| 101 | 98 |
if task.DesiredState != swarm.TaskStateShutdown {
|
| 102 | 99 |
tasksNoShutdown[task.ServiceID]++ |
| ... | ... |
@@ -107,52 +109,20 @@ func PrintNotQuiet(out io.Writer, services []swarm.Service, nodes []swarm.Node, |
| 107 | 107 |
} |
| 108 | 108 |
} |
| 109 | 109 |
|
| 110 |
- printTable(out, services, running, tasksNoShutdown) |
|
| 111 |
-} |
|
| 112 |
- |
|
| 113 |
-func printTable(out io.Writer, services []swarm.Service, running, tasksNoShutdown map[string]int) {
|
|
| 114 |
- writer := tabwriter.NewWriter(out, 0, 4, 2, ' ', 0) |
|
| 115 |
- |
|
| 116 |
- // Ignore flushing errors |
|
| 117 |
- defer writer.Flush() |
|
| 118 |
- |
|
| 119 |
- fmt.Fprintf(writer, listItemFmt, "ID", "NAME", "MODE", "REPLICAS", "IMAGE") |
|
| 120 |
- |
|
| 110 |
+ info := map[string]formatter.ServiceListInfo{}
|
|
| 121 | 111 |
for _, service := range services {
|
| 122 |
- mode := "" |
|
| 123 |
- replicas := "" |
|
| 112 |
+ info[service.ID] = formatter.ServiceListInfo{}
|
|
| 124 | 113 |
if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil {
|
| 125 |
- mode = "replicated" |
|
| 126 |
- replicas = fmt.Sprintf("%d/%d", running[service.ID], *service.Spec.Mode.Replicated.Replicas)
|
|
| 114 |
+ info[service.ID] = formatter.ServiceListInfo{
|
|
| 115 |
+ Mode: "replicated", |
|
| 116 |
+ Replicas: fmt.Sprintf("%d/%d", running[service.ID], *service.Spec.Mode.Replicated.Replicas),
|
|
| 117 |
+ } |
|
| 127 | 118 |
} else if service.Spec.Mode.Global != nil {
|
| 128 |
- mode = "global" |
|
| 129 |
- replicas = fmt.Sprintf("%d/%d", running[service.ID], tasksNoShutdown[service.ID])
|
|
| 130 |
- } |
|
| 131 |
- image := service.Spec.TaskTemplate.ContainerSpec.Image |
|
| 132 |
- ref, err := distreference.ParseNamed(image) |
|
| 133 |
- if err == nil {
|
|
| 134 |
- // update image string for display |
|
| 135 |
- namedTagged, ok := ref.(distreference.NamedTagged) |
|
| 136 |
- if ok {
|
|
| 137 |
- image = namedTagged.Name() + ":" + namedTagged.Tag() |
|
| 119 |
+ info[service.ID] = formatter.ServiceListInfo{
|
|
| 120 |
+ Mode: "global", |
|
| 121 |
+ Replicas: fmt.Sprintf("%d/%d", running[service.ID], tasksNoShutdown[service.ID]),
|
|
| 138 | 122 |
} |
| 139 | 123 |
} |
| 140 |
- |
|
| 141 |
- fmt.Fprintf( |
|
| 142 |
- writer, |
|
| 143 |
- listItemFmt, |
|
| 144 |
- stringid.TruncateID(service.ID), |
|
| 145 |
- service.Spec.Name, |
|
| 146 |
- mode, |
|
| 147 |
- replicas, |
|
| 148 |
- image) |
|
| 149 |
- } |
|
| 150 |
-} |
|
| 151 |
- |
|
| 152 |
-// PrintQuiet shows service list in a quiet way. |
|
| 153 |
-// Besides this, command `docker stack services xxx` will call this, too. |
|
| 154 |
-func PrintQuiet(out io.Writer, services []swarm.Service) {
|
|
| 155 |
- for _, service := range services {
|
|
| 156 |
- fmt.Fprintln(out, service.ID) |
|
| 157 | 124 |
} |
| 125 |
+ return info |
|
| 158 | 126 |
} |
| ... | ... |
@@ -9,6 +9,7 @@ import ( |
| 9 | 9 |
"github.com/docker/docker/api/types/filters" |
| 10 | 10 |
"github.com/docker/docker/cli" |
| 11 | 11 |
"github.com/docker/docker/cli/command" |
| 12 |
+ "github.com/docker/docker/cli/command/formatter" |
|
| 12 | 13 |
"github.com/docker/docker/cli/command/service" |
| 13 | 14 |
"github.com/docker/docker/opts" |
| 14 | 15 |
"github.com/spf13/cobra" |
| ... | ... |
@@ -16,6 +17,7 @@ import ( |
| 16 | 16 |
|
| 17 | 17 |
type servicesOptions struct {
|
| 18 | 18 |
quiet bool |
| 19 |
+ format string |
|
| 19 | 20 |
filter opts.FilterOpt |
| 20 | 21 |
namespace string |
| 21 | 22 |
} |
| ... | ... |
@@ -34,6 +36,7 @@ func newServicesCommand(dockerCli *command.DockerCli) *cobra.Command {
|
| 34 | 34 |
} |
| 35 | 35 |
flags := cmd.Flags() |
| 36 | 36 |
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Only display IDs") |
| 37 |
+ flags.StringVar(&opts.format, "format", "", "Pretty-print services using a Go template") |
|
| 37 | 38 |
flags.VarP(&opts.filter, "filter", "f", "Filter output based on conditions provided") |
| 38 | 39 |
|
| 39 | 40 |
return cmd |
| ... | ... |
@@ -57,9 +60,8 @@ func runServices(dockerCli *command.DockerCli, opts servicesOptions) error {
|
| 57 | 57 |
return nil |
| 58 | 58 |
} |
| 59 | 59 |
|
| 60 |
- if opts.quiet {
|
|
| 61 |
- service.PrintQuiet(out, services) |
|
| 62 |
- } else {
|
|
| 60 |
+ info := map[string]formatter.ServiceListInfo{}
|
|
| 61 |
+ if !opts.quiet {
|
|
| 63 | 62 |
taskFilter := filters.NewArgs() |
| 64 | 63 |
for _, service := range services {
|
| 65 | 64 |
taskFilter.Add("service", service.ID)
|
| ... | ... |
@@ -69,11 +71,27 @@ func runServices(dockerCli *command.DockerCli, opts servicesOptions) error {
|
| 69 | 69 |
if err != nil {
|
| 70 | 70 |
return err |
| 71 | 71 |
} |
| 72 |
+ |
|
| 72 | 73 |
nodes, err := client.NodeList(ctx, types.NodeListOptions{})
|
| 73 | 74 |
if err != nil {
|
| 74 | 75 |
return err |
| 75 | 76 |
} |
| 76 |
- service.PrintNotQuiet(out, services, nodes, tasks) |
|
| 77 |
+ |
|
| 78 |
+ info = service.GetServicesStatus(services, nodes, tasks) |
|
| 79 |
+ } |
|
| 80 |
+ |
|
| 81 |
+ format := opts.format |
|
| 82 |
+ if len(format) == 0 {
|
|
| 83 |
+ if len(dockerCli.ConfigFile().ServicesFormat) > 0 && !opts.quiet {
|
|
| 84 |
+ format = dockerCli.ConfigFile().ServicesFormat |
|
| 85 |
+ } else {
|
|
| 86 |
+ format = formatter.TableFormatKey |
|
| 87 |
+ } |
|
| 88 |
+ } |
|
| 89 |
+ |
|
| 90 |
+ servicesCtx := formatter.Context{
|
|
| 91 |
+ Output: dockerCli.Out(), |
|
| 92 |
+ Format: formatter.NewServiceListFormat(format, opts.quiet), |
|
| 77 | 93 |
} |
| 78 |
- return nil |
|
| 94 |
+ return formatter.ServiceListWrite(servicesCtx, services, info) |
|
| 79 | 95 |
} |
| ... | ... |
@@ -35,6 +35,7 @@ type ConfigFile struct {
|
| 35 | 35 |
CredentialHelpers map[string]string `json:"credHelpers,omitempty"` |
| 36 | 36 |
Filename string `json:"-"` // Note: for internal use only |
| 37 | 37 |
ServiceInspectFormat string `json:"serviceInspectFormat,omitempty"` |
| 38 |
+ ServicesFormat string `json:"servicesFormat,omitempty"` |
|
| 38 | 39 |
} |
| 39 | 40 |
|
| 40 | 41 |
// 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 |
| 137 | 137 |
falls back to the default table format. For a list of supported formatting |
| 138 | 138 |
directives, see the [**Formatting** section in the `docker plugin ls` documentation](plugin_ls.md) |
| 139 | 139 |
|
| 140 |
+The property `servicesFormat` specifies the default format for `docker |
|
| 141 |
+service ls` output. When the `--format` flag is not provided with the |
|
| 142 |
+`docker service ls` command, Docker's client uses this property. If this |
|
| 143 |
+property is not set, the client falls back to the default json format. For a |
|
| 144 |
+list of supported formatting directives, see the |
|
| 145 |
+[**Formatting** section in the `docker service ls` documentation](service_ls.md) |
|
| 146 |
+ |
|
| 140 | 147 |
The property `serviceInspectFormat` specifies the default format for `docker |
| 141 | 148 |
service inspect` output. When the `--format` flag is not provided with the |
| 142 | 149 |
`docker service inspect` command, Docker's client uses this property. If this |
| ... | ... |
@@ -194,6 +201,7 @@ Following is a sample `config.json` file: |
| 194 | 194 |
"imagesFormat": "table {{.ID}}\\t{{.Repository}}\\t{{.Tag}}\\t{{.CreatedAt}}",
|
| 195 | 195 |
"pluginsFormat": "table {{.ID}}\t{{.Name}}\t{{.Enabled}}",
|
| 196 | 196 |
"statsFormat": "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}",
|
| 197 |
+ "servicesFormat": "table {{.ID}}\t{{.Name}}\t{{.Mode}}",
|
|
| 197 | 198 |
"serviceInspectFormat": "pretty", |
| 198 | 199 |
"detachKeys": "ctrl-e,e", |
| 199 | 200 |
"credsStore": "secretservice", |
| ... | ... |
@@ -24,9 +24,10 @@ Aliases: |
| 24 | 24 |
ls, list |
| 25 | 25 |
|
| 26 | 26 |
Options: |
| 27 |
- -f, --filter value Filter output based on conditions provided |
|
| 28 |
- --help Print usage |
|
| 29 |
- -q, --quiet Only display IDs |
|
| 27 |
+ -f, --filter filter Filter output based on conditions provided |
|
| 28 |
+ --format string Pretty-print services using a Go template |
|
| 29 |
+ --help Print usage |
|
| 30 |
+ -q, --quiet Only display IDs |
|
| 30 | 31 |
``` |
| 31 | 32 |
|
| 32 | 33 |
This command when run targeting a manager, lists services are running in the |
| ... | ... |
@@ -103,6 +104,34 @@ ID NAME MODE REPLICAS IMAGE |
| 103 | 103 |
0bcjwfh8ychr redis replicated 1/1 redis:3.0.6 |
| 104 | 104 |
``` |
| 105 | 105 |
|
| 106 |
+## Formatting |
|
| 107 |
+ |
|
| 108 |
+The formatting options (`--format`) pretty-prints services output |
|
| 109 |
+using a Go template. |
|
| 110 |
+ |
|
| 111 |
+Valid placeholders for the Go template are listed below: |
|
| 112 |
+ |
|
| 113 |
+Placeholder | Description |
|
| 114 |
+------------|------------------------------------------------------------------------------------------ |
|
| 115 |
+`.ID` | Service ID |
|
| 116 |
+`.Name` | Service name |
|
| 117 |
+`.Mode` | Service mode (replicated, global) |
|
| 118 |
+`.Replicas` | Service replicas |
|
| 119 |
+`.Image` | Service image |
|
| 120 |
+ |
|
| 121 |
+When using the `--format` option, the `service ls` command will either |
|
| 122 |
+output the data exactly as the template declares or, when using the |
|
| 123 |
+`table` directive, includes column headers as well. |
|
| 124 |
+ |
|
| 125 |
+The following example uses a template without headers and outputs the |
|
| 126 |
+`ID`, `Mode`, and `Replicas` entries separated by a colon for all services: |
|
| 127 |
+ |
|
| 128 |
+```bash |
|
| 129 |
+$ docker service ls --format "{{.ID}}: {{.Mode}} {{.Replicas}}"
|
|
| 130 |
+0zmvwuiu3vue: replicated 10/10 |
|
| 131 |
+fm6uf97exkul: global 5/5 |
|
| 132 |
+``` |
|
| 133 |
+ |
|
| 106 | 134 |
## Related information |
| 107 | 135 |
|
| 108 | 136 |
* [service create](service_create.md) |
| ... | ... |
@@ -22,9 +22,10 @@ Usage: docker stack services [OPTIONS] STACK |
| 22 | 22 |
List the services in the stack |
| 23 | 23 |
|
| 24 | 24 |
Options: |
| 25 |
- -f, --filter value Filter output based on conditions provided |
|
| 26 |
- --help Print usage |
|
| 27 |
- -q, --quiet Only display IDs |
|
| 25 |
+ -f, --filter filter Filter output based on conditions provided |
|
| 26 |
+ --format string Pretty-print services using a Go template |
|
| 27 |
+ --help Print usage |
|
| 28 |
+ -q, --quiet Only display IDs |
|
| 28 | 29 |
``` |
| 29 | 30 |
|
| 30 | 31 |
Lists the services that are running as part of the specified stack. This |
| ... | ... |
@@ -62,6 +63,35 @@ The currently supported filters are: |
| 62 | 62 |
* name (`--filter name=myapp_web`) |
| 63 | 63 |
* label (`--filter label=key=value`) |
| 64 | 64 |
|
| 65 |
+## Formatting |
|
| 66 |
+ |
|
| 67 |
+The formatting options (`--format`) pretty-prints services output |
|
| 68 |
+using a Go template. |
|
| 69 |
+ |
|
| 70 |
+Valid placeholders for the Go template are listed below: |
|
| 71 |
+ |
|
| 72 |
+Placeholder | Description |
|
| 73 |
+------------|------------------------------------------------------------------------------------------ |
|
| 74 |
+`.ID` | Service ID |
|
| 75 |
+`.Name` | Service name |
|
| 76 |
+`.Mode` | Service mode (replicated, global) |
|
| 77 |
+`.Replicas` | Service replicas |
|
| 78 |
+`.Image` | Service image |
|
| 79 |
+ |
|
| 80 |
+When using the `--format` option, the `stack services` command will either |
|
| 81 |
+output the data exactly as the template declares or, when using the |
|
| 82 |
+`table` directive, includes column headers as well. |
|
| 83 |
+ |
|
| 84 |
+The following example uses a template without headers and outputs the |
|
| 85 |
+`ID`, `Mode`, and `Replicas` entries separated by a colon for all services: |
|
| 86 |
+ |
|
| 87 |
+```bash |
|
| 88 |
+$ docker stack services --format "{{.ID}}: {{.Mode}} {{.Replicas}}"
|
|
| 89 |
+0zmvwuiu3vue: replicated 10/10 |
|
| 90 |
+fm6uf97exkul: global 5/5 |
|
| 91 |
+``` |
|
| 92 |
+ |
|
| 93 |
+ |
|
| 65 | 94 |
## Related information |
| 66 | 95 |
|
| 67 | 96 |
* [stack deploy](stack_deploy.md) |