Skip to content

Commit 8426186

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 92475ec commit 8426186

7 files changed

Lines changed: 293 additions & 186 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: 79 additions & 58 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,36 @@ 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(), "1.41") {
74+
var err error
75+
services, err = SetServiceStatus(ctx, client, services)
76+
if err != nil {
77+
return err
78+
}
7679
}
77-
78-
info = GetServicesStatus(services, nodes, tasks)
7980
}
81+
sort.Slice(services, func(i, j int) bool {
82+
return sortorder.NaturalLess(services[i].Spec.Name, services[j].Spec.Name)
83+
})
8084

8185
format := options.format
8286
if len(format) == 0 {
@@ -91,52 +95,69 @@ func runList(dockerCli command.Cli, options listOptions) error {
9195
Output: dockerCli.Out(),
9296
Format: NewListFormat(format, options.quiet),
9397
}
94-
return ListFormatWrite(servicesCtx, services, info)
98+
return ListFormatWrite(servicesCtx, services)
9599
}
96100

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{}
101+
// SetServiceStatus propagates the ServiceStatus field for "services".
102+
//
103+
// If API version v1.41 or up is used, this information is already set by the
104+
// daemon. On older API versions, we need to do some extra requests to get
105+
// that information
106+
func SetServiceStatus(ctx context.Context, c client.APIClient, services []swarm.Service) ([]swarm.Service, error) {
107+
// status := make(map[string]swarm.ServiceStatus)
108+
status := map[string]*swarm.ServiceStatus{}
109+
110+
// only non-empty services and not quiet, should we call TaskList and NodeList api
111+
taskFilter := filters.NewArgs()
112+
for _, s := range services {
113+
status[s.ID] = &swarm.ServiceStatus{}
114+
taskFilter.Add("service", s.ID)
115+
}
101116

102-
activeNodes := make(map[string]struct{})
103-
for _, n := range nodes {
104-
if n.Status.State != swarm.NodeStateDown {
105-
activeNodes[n.ID] = struct{}{}
106-
}
117+
tasks, err := c.TaskList(ctx, types.TaskListOptions{Filters: taskFilter})
118+
if err != nil {
119+
return nil, err
107120
}
108121

109-
for _, task := range tasks {
110-
if task.DesiredState != swarm.TaskStateShutdown {
111-
tasksNoShutdown[task.ServiceID]++
122+
if len(tasks) > 0 {
123+
activeNodes := make(map[string]struct{})
124+
nodes, err := c.NodeList(ctx, types.NodeListOptions{})
125+
if err != nil {
126+
return nil, err
112127
}
113-
114-
if _, nodeActive := activeNodes[task.NodeID]; nodeActive && task.Status.State == swarm.TaskStateRunning {
115-
running[task.ServiceID]++
128+
for _, n := range nodes {
129+
if n.Status.State != swarm.NodeStateDown {
130+
activeNodes[n.ID] = struct{}{}
131+
}
116132
}
117-
}
118133

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-
}
134+
for _, task := range tasks {
135+
if task.DesiredState != swarm.TaskStateShutdown {
136+
status[task.ServiceID].DesiredTasks++
133137
}
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+
if _, nodeActive := activeNodes[task.NodeID]; nodeActive && task.Status.State == swarm.TaskStateRunning {
140+
status[task.ServiceID].RunningTasks++
138141
}
139142
}
140143
}
141-
return info
144+
out := make([]swarm.Service, len(services))
145+
for i, s := range services {
146+
services[i].ServiceStatus = &swarm.ServiceStatus{
147+
DesiredTasks: status[s.ID].DesiredTasks,
148+
RunningTasks: status[s.ID].RunningTasks,
149+
}
150+
}
151+
return out, nil
152+
}
153+
154+
func serviceMode(service swarm.Service) string {
155+
switch {
156+
case service.Spec.Mode.Global != nil:
157+
return "global"
158+
case service.Spec.Mode.Replicated != nil:
159+
return "replicated"
160+
default:
161+
return ""
162+
}
142163
}

0 commit comments

Comments
 (0)