Skip to content

Commit

Permalink
Merge pull request moby#28199 from yongtang/11062016-service-ls-format
Browse files Browse the repository at this point in the history
Add `--format` to `docker service ls`
  • Loading branch information
vdemeester authored Feb 2, 2017
2 parents 1ce67df + 31fb756 commit 16c1a50
Show file tree
Hide file tree
Showing 5 changed files with 327 additions and 69 deletions.
92 changes: 92 additions & 0 deletions command/formatter/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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 changes: 177 additions & 0 deletions command/formatter/service_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading

0 comments on commit 16c1a50

Please sign in to comment.