Skip to content

Commit

Permalink
Services: use ServiceStatus on API v1.41 and up
Browse files Browse the repository at this point in the history
API v1.41 adds a new option to get the number of desired
and running tasks when listing services. This patch enables
this functionality, and provides a fallback mechanism when
the ServiceStatus is not available, which would be when
using an older API version.

Now that the swarm.Service struct captures this information,
the `ListInfo` type is no longer needed, so it is removed,
and the related list- and formatting functions have been
modified accordingly.

To reduce repetition, sorting the services has been moved
to the formatter. This is a slight change in behavior, but
all calls to the formatter performed this sort first, so
the change will not lead to user-facing changes.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
  • Loading branch information
thaJeztah committed Oct 29, 2019
1 parent 228e0f5 commit 7405ac5
Show file tree
Hide file tree
Showing 9 changed files with 439 additions and 231 deletions.
46 changes: 33 additions & 13 deletions cli/command/service/formatter.go
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/docker/docker/pkg/stringid"
units "github.com/docker/go-units"
"github.com/pkg/errors"
"vbom.ml/util/sortorder"
)

const serviceInspectPrettyTemplate formatter.Format = `
Expand Down Expand Up @@ -520,17 +521,14 @@ func NewListFormat(source string, quiet bool) formatter.Format {
return formatter.Format(source)
}

// ListInfo stores the information about mode and replicas to be used by template
type ListInfo struct {
Mode string
Replicas string
}

// ListFormatWrite writes the context
func ListFormatWrite(ctx formatter.Context, services []swarm.Service, info map[string]ListInfo) error {
func ListFormatWrite(ctx formatter.Context, services []swarm.Service) error {
render := func(format func(subContext formatter.SubContext) error) error {
sort.Slice(services, func(i, j int) bool {
return sortorder.NaturalLess(services[i].Spec.Name, services[j].Spec.Name)
})
for _, service := range services {
serviceCtx := &serviceContext{service: service, mode: info[service.ID].Mode, replicas: info[service.ID].Replicas}
serviceCtx := &serviceContext{service: service}
if err := format(serviceCtx); err != nil {
return err
}
Expand All @@ -551,9 +549,7 @@ func ListFormatWrite(ctx formatter.Context, services []swarm.Service, info map[s

type serviceContext struct {
formatter.HeaderContext
service swarm.Service
mode string
replicas string
service swarm.Service
}

func (c *serviceContext) MarshalJSON() ([]byte, error) {
Expand All @@ -569,11 +565,35 @@ func (c *serviceContext) Name() string {
}

func (c *serviceContext) Mode() string {
return c.mode
switch {
case c.service.Spec.Mode.Global != nil:
return "global"
case c.service.Spec.Mode.Replicated != nil:
return "replicated"
default:
return ""
}
}

func (c *serviceContext) Replicas() string {
return c.replicas
s := &c.service

var running, desired uint64
if s.ServiceStatus != nil {
running = c.service.ServiceStatus.RunningTasks
desired = c.service.ServiceStatus.DesiredTasks
}
if r := c.maxReplicas(); r > 0 {
return fmt.Sprintf("%d/%d (max %d per node)", running, desired, r)
}
return fmt.Sprintf("%d/%d", running, desired)
}

func (c *serviceContext) maxReplicas() uint64 {
if c.Mode() != "replicated" || c.service.Spec.TaskTemplate.Placement == nil {
return 0
}
return c.service.Spec.TaskTemplate.Placement.MaxReplicas
}

func (c *serviceContext) Image() string {
Expand Down
172 changes: 119 additions & 53 deletions cli/command/service/formatter_test.go
Expand Up @@ -33,29 +33,37 @@ func TestServiceContextWrite(t *testing.T) {
// Table format
{
formatter.Context{Format: NewListFormat("table", false)},
`ID NAME MODE REPLICAS IMAGE PORTS
id_baz baz global 2/4 *:80->8080/tcp
id_bar bar replicated 2/4 *:80->8080/tcp
`ID NAME MODE REPLICAS IMAGE PORTS
02_bar bar replicated 2/4 *:80->8090/udp
01_baz baz global 1/3 *:80->8080/tcp
04_qux2 qux2 replicated 3/3 (max 2 per node)
03_qux10 qux10 replicated 2/3 (max 1 per node)
`,
},
{
formatter.Context{Format: NewListFormat("table", true)},
`id_baz
id_bar
`02_bar
01_baz
04_qux2
03_qux10
`,
},
{
formatter.Context{Format: NewListFormat("table {{.Name}}", false)},
`NAME
baz
bar
formatter.Context{Format: NewListFormat("table {{.Name}}\t{{.Mode}}", false)},
`NAME MODE
bar replicated
baz global
qux2 replicated
qux10 replicated
`,
},
{
formatter.Context{Format: NewListFormat("table {{.Name}}", true)},
`NAME
baz
bar
baz
qux2
qux10
`,
},
// Raw Format
Expand All @@ -65,25 +73,32 @@ bar
},
{
formatter.Context{Format: NewListFormat("raw", true)},
`id: id_baz
id: id_bar
`id: 02_bar
id: 01_baz
id: 04_qux2
id: 03_qux10
`,
},
// Custom Format
{
formatter.Context{Format: NewListFormat("{{.Name}}", false)},
`baz
bar
`bar
baz
qux2
qux10
`,
},
}

for _, testcase := range cases {
services := []swarm.Service{
{
ID: "id_baz",
ID: "01_baz",
Spec: swarm.ServiceSpec{
Annotations: swarm.Annotations{Name: "baz"},
Mode: swarm.ServiceMode{
Global: &swarm.GlobalService{},
},
},
Endpoint: swarm.Endpoint{
Ports: []swarm.PortConfig{
Expand All @@ -95,37 +110,70 @@ bar
},
},
},
ServiceStatus: &swarm.ServiceStatus{
RunningTasks: 1,
DesiredTasks: 3,
},
},
{
ID: "id_bar",
ID: "02_bar",
Spec: swarm.ServiceSpec{
Annotations: swarm.Annotations{Name: "bar"},
Mode: swarm.ServiceMode{
Replicated: &swarm.ReplicatedService{},
},
},
Endpoint: swarm.Endpoint{
Ports: []swarm.PortConfig{
{
PublishMode: "ingress",
PublishedPort: 80,
TargetPort: 8080,
Protocol: "tcp",
TargetPort: 8090,
Protocol: "udp",
},
},
},
ServiceStatus: &swarm.ServiceStatus{
RunningTasks: 2,
DesiredTasks: 4,
},
},
}
info := map[string]ListInfo{
"id_baz": {
Mode: "global",
Replicas: "2/4",
{
ID: "03_qux10",
Spec: swarm.ServiceSpec{
Annotations: swarm.Annotations{Name: "qux10"},
Mode: swarm.ServiceMode{
Replicated: &swarm.ReplicatedService{},
},
TaskTemplate: swarm.TaskSpec{
Placement: &swarm.Placement{MaxReplicas: 1},
},
},
ServiceStatus: &swarm.ServiceStatus{
RunningTasks: 2,
DesiredTasks: 3,
},
},
"id_bar": {
Mode: "replicated",
Replicas: "2/4",
{
ID: "04_qux2",
Spec: swarm.ServiceSpec{
Annotations: swarm.Annotations{Name: "qux2"},
Mode: swarm.ServiceMode{
Replicated: &swarm.ReplicatedService{},
},
TaskTemplate: swarm.TaskSpec{
Placement: &swarm.Placement{MaxReplicas: 2},
},
},
ServiceStatus: &swarm.ServiceStatus{
RunningTasks: 3,
DesiredTasks: 3,
},
},
}
out := bytes.NewBufferString("")
testcase.context.Output = out
err := ListFormatWrite(testcase.context, services, info)
err := ListFormatWrite(testcase.context, services)
if err != nil {
assert.Error(t, err, testcase.expected)
} else {
Expand All @@ -137,9 +185,12 @@ bar
func TestServiceContextWriteJSON(t *testing.T) {
services := []swarm.Service{
{
ID: "id_baz",
ID: "01_baz",
Spec: swarm.ServiceSpec{
Annotations: swarm.Annotations{Name: "baz"},
Mode: swarm.ServiceMode{
Global: &swarm.GlobalService{},
},
},
Endpoint: swarm.Endpoint{
Ports: []swarm.PortConfig{
Expand All @@ -151,11 +202,18 @@ func TestServiceContextWriteJSON(t *testing.T) {
},
},
},
ServiceStatus: &swarm.ServiceStatus{
RunningTasks: 1,
DesiredTasks: 3,
},
},
{
ID: "id_bar",
ID: "02_bar",
Spec: swarm.ServiceSpec{
Annotations: swarm.Annotations{Name: "bar"},
Mode: swarm.ServiceMode{
Replicated: &swarm.ReplicatedService{},
},
},
Endpoint: swarm.Endpoint{
Ports: []swarm.PortConfig{
Expand All @@ -167,25 +225,19 @@ func TestServiceContextWriteJSON(t *testing.T) {
},
},
},
},
}
info := map[string]ListInfo{
"id_baz": {
Mode: "global",
Replicas: "2/4",
},
"id_bar": {
Mode: "replicated",
Replicas: "2/4",
ServiceStatus: &swarm.ServiceStatus{
RunningTasks: 2,
DesiredTasks: 4,
},
},
}
expectedJSONs := []map[string]interface{}{
{"ID": "id_baz", "Name": "baz", "Mode": "global", "Replicas": "2/4", "Image": "", "Ports": "*:80->8080/tcp"},
{"ID": "id_bar", "Name": "bar", "Mode": "replicated", "Replicas": "2/4", "Image": "", "Ports": "*:80->8080/tcp"},
{"ID": "02_bar", "Name": "bar", "Mode": "replicated", "Replicas": "2/4", "Image": "", "Ports": "*:80->8080/tcp"},
{"ID": "01_baz", "Name": "baz", "Mode": "global", "Replicas": "1/3", "Image": "", "Ports": "*:80->8080/tcp"},
}

out := bytes.NewBufferString("")
err := ListFormatWrite(formatter.Context{Format: "{{json .}}", Output: out}, services, info)
err := ListFormatWrite(formatter.Context{Format: "{{json .}}", Output: out}, services)
if err != nil {
t.Fatal(err)
}
Expand All @@ -199,21 +251,35 @@ func TestServiceContextWriteJSON(t *testing.T) {
}
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]ListInfo{
"id_baz": {
Mode: "global",
Replicas: "2/4",
{
ID: "01_baz",
Spec: swarm.ServiceSpec{
Annotations: swarm.Annotations{Name: "baz"},
Mode: swarm.ServiceMode{
Global: &swarm.GlobalService{},
},
},
ServiceStatus: &swarm.ServiceStatus{
RunningTasks: 2,
DesiredTasks: 4,
},
},
"id_bar": {
Mode: "replicated",
Replicas: "2/4",
{
ID: "24_bar",
Spec: swarm.ServiceSpec{
Annotations: swarm.Annotations{Name: "bar"},
Mode: swarm.ServiceMode{
Replicated: &swarm.ReplicatedService{},
},
},
ServiceStatus: &swarm.ServiceStatus{
RunningTasks: 2,
DesiredTasks: 4,
},
},
}
out := bytes.NewBufferString("")
err := ListFormatWrite(formatter.Context{Format: "{{json .Name}}", Output: out}, services, info)
err := ListFormatWrite(formatter.Context{Format: "{{json .Name}}", Output: out}, services)
if err != nil {
t.Fatal(err)
}
Expand Down

0 comments on commit 7405ac5

Please sign in to comment.