Skip to content

Commit dafd247

Browse files
committed
Services: use ServiceStatus on API v1.41 and up
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 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. Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
1 parent 2ed01eb commit dafd247

7 files changed

Lines changed: 279 additions & 183 deletions

File tree

cli/command/service/formatter.go

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -520,17 +520,27 @@ func NewListFormat(source string, quiet bool) formatter.Format {
520520
return formatter.Format(source)
521521
}
522522

523-
// ListInfo stores the information about mode and replicas to be used by template
524-
type ListInfo struct {
525-
Mode string
526-
Replicas string
527-
}
528-
529523
// ListFormatWrite writes the context
530-
func ListFormatWrite(ctx formatter.Context, services []swarm.Service, info map[string]ListInfo) error {
524+
func ListFormatWrite(ctx formatter.Context, services []swarm.Service) error {
531525
render := func(format func(subContext formatter.SubContext) error) error {
532526
for _, service := range services {
533-
serviceCtx := &serviceContext{service: service, mode: info[service.ID].Mode, replicas: info[service.ID].Replicas}
527+
serviceCtx := &serviceContext{
528+
service: service,
529+
mode: serviceMode(service),
530+
replicas: fmt.Sprintf(
531+
"%d/%d",
532+
service.ServiceStatus.RunningTasks,
533+
service.ServiceStatus.DesiredTasks,
534+
),
535+
}
536+
537+
if serviceCtx.mode == "replicated" && service.Spec.TaskTemplate.Placement != nil && service.Spec.TaskTemplate.Placement.MaxReplicas > 0 {
538+
serviceCtx.replicas += fmt.Sprintf(
539+
" (max %d per node)",
540+
service.Spec.TaskTemplate.Placement.MaxReplicas,
541+
)
542+
}
543+
534544
if err := format(serviceCtx); err != nil {
535545
return err
536546
}

cli/command/service/formatter_test.go

Lines changed: 55 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ bar
8484
ID: "id_baz",
8585
Spec: swarm.ServiceSpec{
8686
Annotations: swarm.Annotations{Name: "baz"},
87+
Mode: swarm.ServiceMode{
88+
Global: &swarm.GlobalService{},
89+
},
8790
},
8891
Endpoint: swarm.Endpoint{
8992
Ports: []swarm.PortConfig{
@@ -95,11 +98,18 @@ bar
9598
},
9699
},
97100
},
101+
ServiceStatus: &swarm.ServiceStatus{
102+
RunningTasks: 2,
103+
DesiredTasks: 4,
104+
},
98105
},
99106
{
100107
ID: "id_bar",
101108
Spec: swarm.ServiceSpec{
102109
Annotations: swarm.Annotations{Name: "bar"},
110+
Mode: swarm.ServiceMode{
111+
Replicated: &swarm.ReplicatedService{},
112+
},
103113
},
104114
Endpoint: swarm.Endpoint{
105115
Ports: []swarm.PortConfig{
@@ -111,21 +121,15 @@ bar
111121
},
112122
},
113123
},
114-
},
115-
}
116-
info := map[string]ListInfo{
117-
"id_baz": {
118-
Mode: "global",
119-
Replicas: "2/4",
120-
},
121-
"id_bar": {
122-
Mode: "replicated",
123-
Replicas: "2/4",
124+
ServiceStatus: &swarm.ServiceStatus{
125+
RunningTasks: 2,
126+
DesiredTasks: 4,
127+
},
124128
},
125129
}
126130
out := bytes.NewBufferString("")
127131
testcase.context.Output = out
128-
err := ListFormatWrite(testcase.context, services, info)
132+
err := ListFormatWrite(testcase.context, services)
129133
if err != nil {
130134
assert.Error(t, err, testcase.expected)
131135
} else {
@@ -140,6 +144,9 @@ func TestServiceContextWriteJSON(t *testing.T) {
140144
ID: "id_baz",
141145
Spec: swarm.ServiceSpec{
142146
Annotations: swarm.Annotations{Name: "baz"},
147+
Mode: swarm.ServiceMode{
148+
Global: &swarm.GlobalService{},
149+
},
143150
},
144151
Endpoint: swarm.Endpoint{
145152
Ports: []swarm.PortConfig{
@@ -151,11 +158,18 @@ func TestServiceContextWriteJSON(t *testing.T) {
151158
},
152159
},
153160
},
161+
ServiceStatus: &swarm.ServiceStatus{
162+
RunningTasks: 2,
163+
DesiredTasks: 4,
164+
},
154165
},
155166
{
156167
ID: "id_bar",
157168
Spec: swarm.ServiceSpec{
158169
Annotations: swarm.Annotations{Name: "bar"},
170+
Mode: swarm.ServiceMode{
171+
Replicated: &swarm.ReplicatedService{},
172+
},
159173
},
160174
Endpoint: swarm.Endpoint{
161175
Ports: []swarm.PortConfig{
@@ -167,16 +181,10 @@ func TestServiceContextWriteJSON(t *testing.T) {
167181
},
168182
},
169183
},
170-
},
171-
}
172-
info := map[string]ListInfo{
173-
"id_baz": {
174-
Mode: "global",
175-
Replicas: "2/4",
176-
},
177-
"id_bar": {
178-
Mode: "replicated",
179-
Replicas: "2/4",
184+
ServiceStatus: &swarm.ServiceStatus{
185+
RunningTasks: 2,
186+
DesiredTasks: 4,
187+
},
180188
},
181189
}
182190
expectedJSONs := []map[string]interface{}{
@@ -185,7 +193,7 @@ func TestServiceContextWriteJSON(t *testing.T) {
185193
}
186194

187195
out := bytes.NewBufferString("")
188-
err := ListFormatWrite(formatter.Context{Format: "{{json .}}", Output: out}, services, info)
196+
err := ListFormatWrite(formatter.Context{Format: "{{json .}}", Output: out}, services)
189197
if err != nil {
190198
t.Fatal(err)
191199
}
@@ -199,21 +207,35 @@ func TestServiceContextWriteJSON(t *testing.T) {
199207
}
200208
func TestServiceContextWriteJSONField(t *testing.T) {
201209
services := []swarm.Service{
202-
{ID: "id_baz", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "baz"}}},
203-
{ID: "id_bar", Spec: swarm.ServiceSpec{Annotations: swarm.Annotations{Name: "bar"}}},
204-
}
205-
info := map[string]ListInfo{
206-
"id_baz": {
207-
Mode: "global",
208-
Replicas: "2/4",
210+
{
211+
ID: "id_baz",
212+
Spec: swarm.ServiceSpec{
213+
Annotations: swarm.Annotations{Name: "baz"},
214+
Mode: swarm.ServiceMode{
215+
Global: &swarm.GlobalService{},
216+
},
217+
},
218+
ServiceStatus: &swarm.ServiceStatus{
219+
RunningTasks: 2,
220+
DesiredTasks: 4,
221+
},
209222
},
210-
"id_bar": {
211-
Mode: "replicated",
212-
Replicas: "2/4",
223+
{
224+
ID: "id_bar",
225+
Spec: swarm.ServiceSpec{
226+
Annotations: swarm.Annotations{Name: "bar"},
227+
Mode: swarm.ServiceMode{
228+
Replicated: &swarm.ReplicatedService{},
229+
},
230+
},
231+
ServiceStatus: &swarm.ServiceStatus{
232+
RunningTasks: 2,
233+
DesiredTasks: 4,
234+
},
213235
},
214236
}
215237
out := bytes.NewBufferString("")
216-
err := ListFormatWrite(formatter.Context{Format: "{{json .Name}}", Output: out}, services, info)
238+
err := ListFormatWrite(formatter.Context{Format: "{{json .Name}}", Output: out}, services)
217239
if err != nil {
218240
t.Fatal(err)
219241
}

cli/command/service/list.go

Lines changed: 66 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ package service
22

33
import (
44
"context"
5-
"fmt"
65
"sort"
76

7+
"github.com/docker/docker/client"
88
"vbom.ml/util/sortorder"
99

10+
"github.com/docker/docker/api/types/versions"
11+
1012
"github.com/docker/cli/cli"
1113
"github.com/docker/cli/cli/command"
1214
"github.com/docker/cli/cli/command/formatter"
@@ -49,34 +51,34 @@ func runList(dockerCli command.Cli, options listOptions) error {
4951
client := dockerCli.Client()
5052

5153
serviceFilters := options.filter.Value()
52-
services, err := client.ServiceList(ctx, types.ServiceListOptions{Filters: serviceFilters})
54+
services, err := client.ServiceList(ctx, types.ServiceListOptions{
55+
Filters: serviceFilters,
56+
// When not running "quiet", also get service status (number of running
57+
// and desired tasks). Note that this is only supported on API v1.41 and
58+
// up; older API versions ignore this option, and we will have to collect
59+
// the information manually below.
60+
Status: !options.quiet,
61+
})
5362
if err != nil {
5463
return err
5564
}
5665

57-
sort.Slice(services, func(i, j int) bool {
58-
return sortorder.NaturalLess(services[i].Spec.Name, services[j].Spec.Name)
59-
})
60-
info := map[string]ListInfo{}
6166
if len(services) > 0 && !options.quiet {
62-
// only non-empty services and not quiet, should we call TaskList and NodeList api
63-
taskFilter := filters.NewArgs()
64-
for _, service := range services {
65-
taskFilter.Add("service", service.ID)
66-
}
67-
68-
tasks, err := client.TaskList(ctx, types.TaskListOptions{Filters: taskFilter})
69-
if err != nil {
70-
return err
71-
}
72-
73-
nodes, err := client.NodeList(ctx, types.NodeListOptions{})
74-
if err != nil {
75-
return err
67+
// Now that a request was made, we know what API version was used (either
68+
// through configuration, or after client and daemon negotiated a version).
69+
// If API version v1.41 or up was used; the daemon already did the legwork
70+
// for us, and we don't have to calculate the number of desired and running
71+
// tasks. On older API versions, we need to do some extra requests to get
72+
// that information
73+
if versions.LessThan(client.ClientVersion(), "v1.41") {
74+
if err := SetServiceStatus(ctx, client, services); err != nil {
75+
return err
76+
}
7677
}
77-
78-
info = GetServicesStatus(services, nodes, tasks)
7978
}
79+
sort.Slice(services, func(i, j int) bool {
80+
return sortorder.NaturalLess(services[i].Spec.Name, services[j].Spec.Name)
81+
})
8082

8183
format := options.format
8284
if len(format) == 0 {
@@ -91,52 +93,62 @@ func runList(dockerCli command.Cli, options listOptions) error {
9193
Output: dockerCli.Out(),
9294
Format: NewListFormat(format, options.quiet),
9395
}
94-
return ListFormatWrite(servicesCtx, services, info)
96+
return ListFormatWrite(servicesCtx, services)
9597
}
9698

97-
// GetServicesStatus returns a map of mode and replicas
98-
func GetServicesStatus(services []swarm.Service, nodes []swarm.Node, tasks []swarm.Task) map[string]ListInfo {
99-
running := map[string]int{}
100-
tasksNoShutdown := map[string]int{}
99+
// SetServiceStatus propagates the ServiceStatus field for "services".
100+
//
101+
// If API version v1.41 or up is used, this information is already set by the
102+
// daemon. On older API versions, we need to do some extra requests to get
103+
// that information
104+
func SetServiceStatus(ctx context.Context, c client.APIClient, services []swarm.Service) error {
105+
serviceIndex := make(map[string]*swarm.Service)
106+
107+
// only non-empty services and not quiet, should we call TaskList and NodeList api
108+
taskFilter := filters.NewArgs()
109+
for _, s := range services {
110+
s.ServiceStatus = &swarm.ServiceStatus{}
111+
serviceIndex[s.ID] = &s
112+
taskFilter.Add("service", s.ID)
113+
}
114+
115+
tasks, err := c.TaskList(ctx, types.TaskListOptions{Filters: taskFilter})
116+
if err != nil {
117+
return err
118+
}
101119

102120
activeNodes := make(map[string]struct{})
103-
for _, n := range nodes {
104-
if n.Status.State != swarm.NodeStateDown {
105-
activeNodes[n.ID] = struct{}{}
121+
if len(tasks) > 0 {
122+
nodes, err := c.NodeList(ctx, types.NodeListOptions{})
123+
if err != nil {
124+
return err
125+
}
126+
for _, n := range nodes {
127+
if n.Status.State != swarm.NodeStateDown {
128+
activeNodes[n.ID] = struct{}{}
129+
}
106130
}
107131
}
108132

109133
for _, task := range tasks {
110134
if task.DesiredState != swarm.TaskStateShutdown {
111-
tasksNoShutdown[task.ServiceID]++
135+
serviceIndex[task.ServiceID].ServiceStatus.DesiredTasks++
112136
}
113137

114138
if _, nodeActive := activeNodes[task.NodeID]; nodeActive && task.Status.State == swarm.TaskStateRunning {
115-
running[task.ServiceID]++
139+
serviceIndex[task.ServiceID].ServiceStatus.RunningTasks++
116140
}
117141
}
142+
return nil
143+
}
118144

119-
info := map[string]ListInfo{}
120-
for _, service := range services {
121-
info[service.ID] = ListInfo{}
122-
if service.Spec.Mode.Replicated != nil && service.Spec.Mode.Replicated.Replicas != nil {
123-
if service.Spec.TaskTemplate.Placement != nil && service.Spec.TaskTemplate.Placement.MaxReplicas > 0 {
124-
info[service.ID] = ListInfo{
125-
Mode: "replicated",
126-
Replicas: fmt.Sprintf("%d/%d (max %d per node)", running[service.ID], *service.Spec.Mode.Replicated.Replicas, service.Spec.TaskTemplate.Placement.MaxReplicas),
127-
}
128-
} else {
129-
info[service.ID] = ListInfo{
130-
Mode: "replicated",
131-
Replicas: fmt.Sprintf("%d/%d", running[service.ID], *service.Spec.Mode.Replicated.Replicas),
132-
}
133-
}
134-
} else if service.Spec.Mode.Global != nil {
135-
info[service.ID] = ListInfo{
136-
Mode: "global",
137-
Replicas: fmt.Sprintf("%d/%d", running[service.ID], tasksNoShutdown[service.ID]),
138-
}
139-
}
145+
func serviceMode(service swarm.Service) string {
146+
switch {
147+
case service.Spec.Mode.Global != nil:
148+
return "global"
149+
case service.Spec.Mode.Replicated != nil:
150+
return "replicated"
151+
default:
152+
return ""
140153
}
141-
return info
142154
}

0 commit comments

Comments
 (0)