Skip to content
This repository was archived by the owner on Oct 13, 2023. It is now read-only.

Commit f36042d

Browse files
committed
Add support for sending down service Running and Desired task counts
Adds a new ServiceStatus field to the Service object, which includes the running and desired task counts. This new field is gated behind a "status" query parameter. Signed-off-by: Drew Erny <[email protected]>
1 parent e582a10 commit f36042d

File tree

8 files changed

+237
-2
lines changed

8 files changed

+237
-2
lines changed

api/server/router/swarm/cluster_routes.go

+20-2
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,19 @@ func (sr *swarmRouter) getServices(ctx context.Context, w http.ResponseWriter, r
167167
return errdefs.InvalidParameter(err)
168168
}
169169

170-
services, err := sr.backend.GetServices(basictypes.ServiceListOptions{Filters: filter})
170+
// the status query parameter is only support in API versions >= 1.41. If
171+
// the client is using a lesser version, ignore the parameter.
172+
cliVersion := httputils.VersionFromContext(ctx)
173+
var status bool
174+
if value := r.URL.Query().Get("status"); value != "" && !versions.LessThan(cliVersion, "1.41") {
175+
var err error
176+
status, err = strconv.ParseBool(value)
177+
if err != nil {
178+
return errors.Wrapf(errdefs.InvalidParameter(err), "invalid value for status: %s", value)
179+
}
180+
}
181+
182+
services, err := sr.backend.GetServices(basictypes.ServiceListOptions{Filters: filter, Status: status})
171183
if err != nil {
172184
logrus.Errorf("Error getting services: %v", err)
173185
return err
@@ -178,15 +190,21 @@ func (sr *swarmRouter) getServices(ctx context.Context, w http.ResponseWriter, r
178190

179191
func (sr *swarmRouter) getService(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
180192
var insertDefaults bool
193+
181194
if value := r.URL.Query().Get("insertDefaults"); value != "" {
182195
var err error
183196
insertDefaults, err = strconv.ParseBool(value)
184197
if err != nil {
185-
err := fmt.Errorf("invalid value for insertDefaults: %s", value)
186198
return errors.Wrapf(errdefs.InvalidParameter(err), "invalid value for insertDefaults: %s", value)
187199
}
188200
}
189201

202+
// you may note that there is no code here to handle the "status" query
203+
// parameter, as in getServices. the Status field is not supported when
204+
// retrieving an individual service because the Backend API changes
205+
// required to accommodate it would be too disruptive, and because that
206+
// field is so rarely needed as part of an individual service inspection.
207+
190208
service, err := sr.backend.GetService(vars["id"], insertDefaults)
191209
if err != nil {
192210
logrus.Errorf("Error getting service %s: %v", vars["id"], err)

api/swagger.yaml

+25
Original file line numberDiff line numberDiff line change
@@ -3369,6 +3369,27 @@ definitions:
33693369
format: "dateTime"
33703370
Message:
33713371
type: "string"
3372+
ServiceStatus:
3373+
description: |
3374+
The status of the service's tasks. Provided only when requested as
3375+
part of a ServiceList operation.
3376+
type: "object"
3377+
properties:
3378+
RunningTasks:
3379+
description: "The number of tasks for the service currently in the Running state"
3380+
type: "integer"
3381+
format: "uint64"
3382+
example: 7
3383+
DesiredTasks:
3384+
description: |
3385+
The number of tasks for the service desired to be running.
3386+
For replicated services, this is the replica count from the
3387+
service spec. For global services, this is computed by taking
3388+
count of all tasks for the service with a Desired State other
3389+
than Shutdown.
3390+
type: "integer"
3391+
format: "uint64"
3392+
example: 10
33723393
example:
33733394
ID: "9mnpnzenvg8p8tdbtq4wvbkcz"
33743395
Version:
@@ -9316,6 +9337,10 @@ paths:
93169337
- `label=<service label>`
93179338
- `mode=["replicated"|"global"]`
93189339
- `name=<service name>`
9340+
- name: "status"
9341+
in: "query"
9342+
type: "boolean"
9343+
description: "Include service status, with count of running and desired tasks"
93199344
tags: ["Service"]
93209345
/services/create:
93219346
post:

api/types/client.go

+4
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,10 @@ type ServiceUpdateOptions struct {
363363
// ServiceListOptions holds parameters to list services with.
364364
type ServiceListOptions struct {
365365
Filters filters.Args
366+
367+
// Status indicates whether the server should include the service task
368+
// count of running and desired tasks.
369+
Status bool
366370
}
367371

368372
// ServiceInspectOptions holds parameters related to the "service inspect"

api/types/swarm/service.go

+21
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ type Service struct {
1010
PreviousSpec *ServiceSpec `json:",omitempty"`
1111
Endpoint Endpoint `json:",omitempty"`
1212
UpdateStatus *UpdateStatus `json:",omitempty"`
13+
14+
// ServiceStatus is an optional, extra field indicating the number of
15+
// desired and running tasks. It is provided primarily as a shortcut to
16+
// calculating these values client-side, which otherwise would require
17+
// listing all tasks for a service, an operation that could be
18+
// computation and network expensive.
19+
ServiceStatus *ServiceStatus `json:",omitempty"`
1320
}
1421

1522
// ServiceSpec represents the spec of a service.
@@ -122,3 +129,17 @@ type UpdateConfig struct {
122129
// started, or the new task is started before the old task is shut down.
123130
Order string
124131
}
132+
133+
// ServiceStatus represents the number of running tasks in a service and the
134+
// number of tasks desired to be running.
135+
type ServiceStatus struct {
136+
// RunningTasks is the number of tasks for the service actually in the
137+
// Running state
138+
RunningTasks uint64
139+
140+
// DesiredTasks is the number of tasks desired to be running by the
141+
// service. For replicated services, this is the replica count. For global
142+
// services, this is computed by taking the number of tasks with desired
143+
// state of not-Shutdown.
144+
DesiredTasks uint64
145+
}

client/service_list.go

+4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ func (cli *Client) ServiceList(ctx context.Context, options types.ServiceListOpt
2323
query.Set("filters", filterJSON)
2424
}
2525

26+
if options.Status {
27+
query.Set("status", "true")
28+
}
29+
2630
resp, err := cli.get(ctx, "/services", query, nil)
2731
defer ensureReaderClosed(resp)
2832
if err != nil {

daemon/cluster/services.go

+52
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@ func (c *Cluster) GetServices(options apitypes.ServiceListOptions) ([]types.Serv
7777

7878
services := make([]types.Service, 0, len(r.Services))
7979

80+
// if the user requests the service statuses, we'll store the IDs needed
81+
// in this slice
82+
var serviceIDs []string
83+
if options.Status {
84+
serviceIDs = make([]string, 0, len(r.Services))
85+
}
8086
for _, service := range r.Services {
8187
if options.Filters.Contains("mode") {
8288
var mode string
@@ -91,13 +97,59 @@ func (c *Cluster) GetServices(options apitypes.ServiceListOptions) ([]types.Serv
9197
continue
9298
}
9399
}
100+
if options.Status {
101+
serviceIDs = append(serviceIDs, service.ID)
102+
}
94103
svcs, err := convert.ServiceFromGRPC(*service)
95104
if err != nil {
96105
return nil, err
97106
}
98107
services = append(services, svcs)
99108
}
100109

110+
if options.Status {
111+
// Listing service statuses is a separate call because, while it is the
112+
// most common UI operation, it is still just a UI operation, and it
113+
// would be improper to include this data in swarm's Service object.
114+
// We pay the cost with some complexity here, but this is still way
115+
// more efficient than marshalling and unmarshalling all the JSON
116+
// needed to list tasks and get this data otherwise client-side
117+
resp, err := state.controlClient.ListServiceStatuses(
118+
ctx,
119+
&swarmapi.ListServiceStatusesRequest{Services: serviceIDs},
120+
grpc.MaxCallRecvMsgSize(defaultRecvSizeForListResponse),
121+
)
122+
if err != nil {
123+
return nil, err
124+
}
125+
126+
// we'll need to match up statuses in the response with the services in
127+
// the list operation. if we did this by operating on two lists, the
128+
// result would be quadratic. instead, make a mapping of service IDs to
129+
// service statuses so that this is roughly linear. additionally,
130+
// convert the status response to an engine api service status here.
131+
serviceMap := map[string]*types.ServiceStatus{}
132+
for _, status := range resp.Statuses {
133+
serviceMap[status.ServiceID] = &types.ServiceStatus{
134+
RunningTasks: status.RunningTasks,
135+
DesiredTasks: status.DesiredTasks,
136+
}
137+
}
138+
139+
// because this is a list of values and not pointers, make sure we
140+
// actually alter the value when iterating.
141+
for i, service := range services {
142+
// the return value of the ListServiceStatuses operation is
143+
// guaranteed to contain a value in the response for every argument
144+
// in the request, so we can safely do this assignment. and even if
145+
// it wasn't, and the service ID was for some reason absent from
146+
// this map, the resulting value of service.Status would just be
147+
// nil -- the same thing it was before
148+
service.ServiceStatus = serviceMap[service.ID]
149+
services[i] = service
150+
}
151+
}
152+
101153
return services, nil
102154
}
103155

docs/api/version-history.md

+3
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ keywords: "API, Docker, rcli, REST, documentation"
3131
* `GET /info` now returns an `OSVersion` field, containing the operating system's
3232
version. This change is not versioned, and affects all API versions if the daemon
3333
has this patch.
34+
* `GET /services` now accepts query parameter `status`. When set `true`,
35+
services returned will include `ServiceStatus`, which provides Desired and
36+
Running task counts for the service.
3437

3538
## v1.40 API changes
3639

integration/service/list_test.go

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package service // import "github.com/docker/docker/integration/service"
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"testing"
7+
8+
"github.com/docker/docker/api/types"
9+
"github.com/docker/docker/api/types/filters"
10+
swarmtypes "github.com/docker/docker/api/types/swarm"
11+
"github.com/docker/docker/api/types/versions"
12+
"github.com/docker/docker/integration/internal/swarm"
13+
"gotest.tools/assert"
14+
is "gotest.tools/assert/cmp"
15+
"gotest.tools/poll"
16+
"gotest.tools/skip"
17+
)
18+
19+
// TestServiceListWithStatuses tests that performing a ServiceList operation
20+
// correctly uses the Status parameter, and that the resulting response
21+
// contains correct service statuses.
22+
//
23+
// NOTE(dperny): because it's a pain to elicit the behavior of an unconverged
24+
// service reliably, I'm not testing that an unconverged service returns X
25+
// running and Y desired tasks. Instead, I'm just going to trust that I can
26+
// successfully assign a value to another value without screwing it up. The
27+
// logic for computing service statuses is in swarmkit anyway, not in the
28+
// engine, and is well-tested there, so this test just needs to make sure that
29+
// statuses get correctly associated with the right services.
30+
func TestServiceListWithStatuses(t *testing.T) {
31+
skip.If(t, testEnv.IsRemoteDaemon)
32+
skip.If(t, testEnv.DaemonInfo.OSType == "windows")
33+
// statuses were added in API version 1.41
34+
skip.If(t, versions.LessThan(testEnv.DaemonInfo.ServerVersion, "1.41"))
35+
defer setupTest(t)()
36+
d := swarm.NewSwarm(t, testEnv)
37+
defer d.Stop(t)
38+
client := d.NewClientT(t)
39+
defer client.Close()
40+
41+
ctx := context.Background()
42+
43+
serviceCount := 3
44+
// create some services.
45+
for i := 0; i < serviceCount; i++ {
46+
spec := fullSwarmServiceSpec(fmt.Sprintf("test-list-%d", i), uint64(i+1))
47+
// for whatever reason, the args "-u root", when included, cause these
48+
// tasks to fail and exit. instead, we'll just pass no args, which
49+
// works.
50+
spec.TaskTemplate.ContainerSpec.Args = []string{}
51+
resp, err := client.ServiceCreate(ctx, spec, types.ServiceCreateOptions{
52+
QueryRegistry: false,
53+
})
54+
assert.NilError(t, err)
55+
id := resp.ID
56+
// we need to wait specifically for the tasks to be running, which the
57+
// serviceContainerCount function does not do. instead, we'll use a
58+
// bespoke closure right here.
59+
poll.WaitOn(t, func(log poll.LogT) poll.Result {
60+
filter := filters.NewArgs()
61+
filter.Add("service", id)
62+
tasks, err := client.TaskList(context.Background(), types.TaskListOptions{
63+
Filters: filter,
64+
})
65+
66+
running := 0
67+
for _, task := range tasks {
68+
if task.Status.State == swarmtypes.TaskStateRunning {
69+
running++
70+
}
71+
}
72+
73+
switch {
74+
case err != nil:
75+
return poll.Error(err)
76+
case running == i+1:
77+
return poll.Success()
78+
default:
79+
return poll.Continue(
80+
"running task count %d (%d total), waiting for %d",
81+
running, len(tasks), i+1,
82+
)
83+
}
84+
})
85+
}
86+
87+
// now, let's do the list operation with no status arg set.
88+
resp, err := client.ServiceList(ctx, types.ServiceListOptions{})
89+
assert.NilError(t, err)
90+
assert.Check(t, is.Len(resp, serviceCount))
91+
for _, service := range resp {
92+
assert.Check(t, is.Nil(service.ServiceStatus))
93+
}
94+
95+
// now try again, but with Status: true. This time, we should have statuses
96+
resp, err = client.ServiceList(ctx, types.ServiceListOptions{Status: true})
97+
assert.NilError(t, err)
98+
assert.Check(t, is.Len(resp, serviceCount))
99+
for _, service := range resp {
100+
replicas := *service.Spec.Mode.Replicated.Replicas
101+
102+
assert.Assert(t, service.ServiceStatus != nil)
103+
// Use assert.Check to not fail out of the test if this fails
104+
assert.Check(t, is.Equal(service.ServiceStatus.DesiredTasks, replicas))
105+
assert.Check(t, is.Equal(service.ServiceStatus.RunningTasks, replicas))
106+
}
107+
108+
}

0 commit comments

Comments
 (0)