Skip to content

Commit 7405ac5

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 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 <[email protected]>
1 parent 228e0f5 commit 7405ac5

File tree

9 files changed

+439
-231
lines changed

9 files changed

+439
-231
lines changed

‎cli/command/service/formatter.go‎

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/docker/docker/pkg/stringid"
1717
units "github.com/docker/go-units"
1818
"github.com/pkg/errors"
19+
"vbom.ml/util/sortorder"
1920
)
2021

2122
const serviceInspectPrettyTemplate formatter.Format = `
@@ -520,17 +521,14 @@ func NewListFormat(source string, quiet bool) formatter.Format {
520521
return formatter.Format(source)
521522
}
522523

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-
529524
// ListFormatWrite writes the context
530-
func ListFormatWrite(ctx formatter.Context, services []swarm.Service, info map[string]ListInfo) error {
525+
func ListFormatWrite(ctx formatter.Context, services []swarm.Service) error {
531526
render := func(format func(subContext formatter.SubContext) error) error {
527+
sort.Slice(services, func(i, j int) bool {
528+
return sortorder.NaturalLess(services[i].Spec.Name, services[j].Spec.Name)
529+
})
532530
for _, service := range services {
533-
serviceCtx := &serviceContext{service: service, mode: info[service.ID].Mode, replicas: info[service.ID].Replicas}
531+
serviceCtx := &serviceContext{service: service}
534532
if err := format(serviceCtx); err != nil {
535533
return err
536534
}
@@ -551,9 +549,7 @@ func ListFormatWrite(ctx formatter.Context, services []swarm.Service, info map[s
551549

552550
type serviceContext struct {
553551
formatter.HeaderContext
554-
service swarm.Service
555-
mode string
556-
replicas string
552+
service swarm.Service
557553
}
558554

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

571567
func (c *serviceContext) Mode() string {
572-
return c.mode
568+
switch {
569+
case c.service.Spec.Mode.Global != nil:
570+
return "global"
571+
case c.service.Spec.Mode.Replicated != nil:
572+
return "replicated"
573+
default:
574+
return ""
575+
}
573576
}
574577

575578
func (c *serviceContext) Replicas() string {
576-
return c.replicas
579+
s := &c.service
580+
581+
var running, desired uint64
582+
if s.ServiceStatus != nil {
583+
running = c.service.ServiceStatus.RunningTasks
584+
desired = c.service.ServiceStatus.DesiredTasks
585+
}
586+
if r := c.maxReplicas(); r > 0 {
587+
return fmt.Sprintf("%d/%d (max %d per node)", running, desired, r)
588+
}
589+
return fmt.Sprintf("%d/%d", running, desired)
590+
}
591+
592+
func (c *serviceContext) maxReplicas() uint64 {
593+
if c.Mode() != "replicated" || c.service.Spec.TaskTemplate.Placement == nil {
594+
return 0
595+
}
596+
return c.service.Spec.TaskTemplate.Placement.MaxReplicas
577597
}
578598

579599
func (c *serviceContext) Image() string {

‎cli/command/service/formatter_test.go‎

Lines changed: 119 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -33,29 +33,37 @@ func TestServiceContextWrite(t *testing.T) {
3333
// Table format
3434
{
3535
formatter.Context{Format: NewListFormat("table", false)},
36-
`ID NAME MODE REPLICAS IMAGE PORTS
37-
id_baz baz global 2/4 *:80->8080/tcp
38-
id_bar bar replicated 2/4 *:80->8080/tcp
36+
`ID NAME MODE REPLICAS IMAGE PORTS
37+
02_bar bar replicated 2/4 *:80->8090/udp
38+
01_baz baz global 1/3 *:80->8080/tcp
39+
04_qux2 qux2 replicated 3/3 (max 2 per node)
40+
03_qux10 qux10 replicated 2/3 (max 1 per node)
3941
`,
4042
},
4143
{
4244
formatter.Context{Format: NewListFormat("table", true)},
43-
`id_baz
44-
id_bar
45+
`02_bar
46+
01_baz
47+
04_qux2
48+
03_qux10
4549
`,
4650
},
4751
{
48-
formatter.Context{Format: NewListFormat("table {{.Name}}", false)},
49-
`NAME
50-
baz
51-
bar
52+
formatter.Context{Format: NewListFormat("table {{.Name}}\t{{.Mode}}", false)},
53+
`NAME MODE
54+
bar replicated
55+
baz global
56+
qux2 replicated
57+
qux10 replicated
5258
`,
5359
},
5460
{
5561
formatter.Context{Format: NewListFormat("table {{.Name}}", true)},
5662
`NAME
57-
baz
5863
bar
64+
baz
65+
qux2
66+
qux10
5967
`,
6068
},
6169
// Raw Format
@@ -65,25 +73,32 @@ bar
6573
},
6674
{
6775
formatter.Context{Format: NewListFormat("raw", true)},
68-
`id: id_baz
69-
id: id_bar
76+
`id: 02_bar
77+
id: 01_baz
78+
id: 04_qux2
79+
id: 03_qux10
7080
`,
7181
},
7282
// Custom Format
7383
{
7484
formatter.Context{Format: NewListFormat("{{.Name}}", false)},
75-
`baz
76-
bar
85+
`bar
86+
baz
87+
qux2
88+
qux10
7789
`,
7890
},
7991
}
8092

8193
for _, testcase := range cases {
8294
services := []swarm.Service{
8395
{
84-
ID: "id_baz",
96+
ID: "01_baz",
8597
Spec: swarm.ServiceSpec{
8698
Annotations: swarm.Annotations{Name: "baz"},
99+
Mode: swarm.ServiceMode{
100+
Global: &swarm.GlobalService{},
101+
},
87102
},
88103
Endpoint: swarm.Endpoint{
89104
Ports: []swarm.PortConfig{
@@ -95,37 +110,70 @@ bar
95110
},
96111
},
97112
},
113+
ServiceStatus: &swarm.ServiceStatus{
114+
RunningTasks: 1,
115+
DesiredTasks: 3,
116+
},
98117
},
99118
{
100-
ID: "id_bar",
119+
ID: "02_bar",
101120
Spec: swarm.ServiceSpec{
102121
Annotations: swarm.Annotations{Name: "bar"},
122+
Mode: swarm.ServiceMode{
123+
Replicated: &swarm.ReplicatedService{},
124+
},
103125
},
104126
Endpoint: swarm.Endpoint{
105127
Ports: []swarm.PortConfig{
106128
{
107129
PublishMode: "ingress",
108130
PublishedPort: 80,
109-
TargetPort: 8080,
110-
Protocol: "tcp",
131+
TargetPort: 8090,
132+
Protocol: "udp",
111133
},
112134
},
113135
},
136+
ServiceStatus: &swarm.ServiceStatus{
137+
RunningTasks: 2,
138+
DesiredTasks: 4,
139+
},
114140
},
115-
}
116-
info := map[string]ListInfo{
117-
"id_baz": {
118-
Mode: "global",
119-
Replicas: "2/4",
141+
{
142+
ID: "03_qux10",
143+
Spec: swarm.ServiceSpec{
144+
Annotations: swarm.Annotations{Name: "qux10"},
145+
Mode: swarm.ServiceMode{
146+
Replicated: &swarm.ReplicatedService{},
147+
},
148+
TaskTemplate: swarm.TaskSpec{
149+
Placement: &swarm.Placement{MaxReplicas: 1},
150+
},
151+
},
152+
ServiceStatus: &swarm.ServiceStatus{
153+
RunningTasks: 2,
154+
DesiredTasks: 3,
155+
},
120156
},
121-
"id_bar": {
122-
Mode: "replicated",
123-
Replicas: "2/4",
157+
{
158+
ID: "04_qux2",
159+
Spec: swarm.ServiceSpec{
160+
Annotations: swarm.Annotations{Name: "qux2"},
161+
Mode: swarm.ServiceMode{
162+
Replicated: &swarm.ReplicatedService{},
163+
},
164+
TaskTemplate: swarm.TaskSpec{
165+
Placement: &swarm.Placement{MaxReplicas: 2},
166+
},
167+
},
168+
ServiceStatus: &swarm.ServiceStatus{
169+
RunningTasks: 3,
170+
DesiredTasks: 3,
171+
},
124172
},
125173
}
126174
out := bytes.NewBufferString("")
127175
testcase.context.Output = out
128-
err := ListFormatWrite(testcase.context, services, info)
176+
err := ListFormatWrite(testcase.context, services)
129177
if err != nil {
130178
assert.Error(t, err, testcase.expected)
131179
} else {
@@ -137,9 +185,12 @@ bar
137185
func TestServiceContextWriteJSON(t *testing.T) {
138186
services := []swarm.Service{
139187
{
140-
ID: "id_baz",
188+
ID: "01_baz",
141189
Spec: swarm.ServiceSpec{
142190
Annotations: swarm.Annotations{Name: "baz"},
191+
Mode: swarm.ServiceMode{
192+
Global: &swarm.GlobalService{},
193+
},
143194
},
144195
Endpoint: swarm.Endpoint{
145196
Ports: []swarm.PortConfig{
@@ -151,11 +202,18 @@ func TestServiceContextWriteJSON(t *testing.T) {
151202
},
152203
},
153204
},
205+
ServiceStatus: &swarm.ServiceStatus{
206+
RunningTasks: 1,
207+
DesiredTasks: 3,
208+
},
154209
},
155210
{
156-
ID: "id_bar",
211+
ID: "02_bar",
157212
Spec: swarm.ServiceSpec{
158213
Annotations: swarm.Annotations{Name: "bar"},
214+
Mode: swarm.ServiceMode{
215+
Replicated: &swarm.ReplicatedService{},
216+
},
159217
},
160218
Endpoint: swarm.Endpoint{
161219
Ports: []swarm.PortConfig{
@@ -167,25 +225,19 @@ func TestServiceContextWriteJSON(t *testing.T) {
167225
},
168226
},
169227
},
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",
228+
ServiceStatus: &swarm.ServiceStatus{
229+
RunningTasks: 2,
230+
DesiredTasks: 4,
231+
},
180232
},
181233
}
182234
expectedJSONs := []map[string]interface{}{
183-
{"ID": "id_baz", "Name": "baz", "Mode": "global", "Replicas": "2/4", "Image": "", "Ports": "*:80->8080/tcp"},
184-
{"ID": "id_bar", "Name": "bar", "Mode": "replicated", "Replicas": "2/4", "Image": "", "Ports": "*:80->8080/tcp"},
235+
{"ID": "02_bar", "Name": "bar", "Mode": "replicated", "Replicas": "2/4", "Image": "", "Ports": "*:80->8080/tcp"},
236+
{"ID": "01_baz", "Name": "baz", "Mode": "global", "Replicas": "1/3", "Image": "", "Ports": "*:80->8080/tcp"},
185237
}
186238

187239
out := bytes.NewBufferString("")
188-
err := ListFormatWrite(formatter.Context{Format: "{{json .}}", Output: out}, services, info)
240+
err := ListFormatWrite(formatter.Context{Format: "{{json .}}", Output: out}, services)
189241
if err != nil {
190242
t.Fatal(err)
191243
}
@@ -199,21 +251,35 @@ func TestServiceContextWriteJSON(t *testing.T) {
199251
}
200252
func TestServiceContextWriteJSONField(t *testing.T) {
201253
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",
254+
{
255+
ID: "01_baz",
256+
Spec: swarm.ServiceSpec{
257+
Annotations: swarm.Annotations{Name: "baz"},
258+
Mode: swarm.ServiceMode{
259+
Global: &swarm.GlobalService{},
260+
},
261+
},
262+
ServiceStatus: &swarm.ServiceStatus{
263+
RunningTasks: 2,
264+
DesiredTasks: 4,
265+
},
209266
},
210-
"id_bar": {
211-
Mode: "replicated",
212-
Replicas: "2/4",
267+
{
268+
ID: "24_bar",
269+
Spec: swarm.ServiceSpec{
270+
Annotations: swarm.Annotations{Name: "bar"},
271+
Mode: swarm.ServiceMode{
272+
Replicated: &swarm.ReplicatedService{},
273+
},
274+
},
275+
ServiceStatus: &swarm.ServiceStatus{
276+
RunningTasks: 2,
277+
DesiredTasks: 4,
278+
},
213279
},
214280
}
215281
out := bytes.NewBufferString("")
216-
err := ListFormatWrite(formatter.Context{Format: "{{json .Name}}", Output: out}, services, info)
282+
err := ListFormatWrite(formatter.Context{Format: "{{json .Name}}", Output: out}, services)
217283
if err != nil {
218284
t.Fatal(err)
219285
}

0 commit comments

Comments
 (0)