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 <drew.erny@docker.com>
This commit is contained in:
Drew Erny 2019-05-16 17:43:48 -05:00
parent e582a10b59
commit f36042d259
8 changed files with 237 additions and 2 deletions

View file

@ -167,7 +167,19 @@ func (sr *swarmRouter) getServices(ctx context.Context, w http.ResponseWriter, r
return errdefs.InvalidParameter(err) return errdefs.InvalidParameter(err)
} }
services, err := sr.backend.GetServices(basictypes.ServiceListOptions{Filters: filter}) // the status query parameter is only support in API versions >= 1.41. If
// the client is using a lesser version, ignore the parameter.
cliVersion := httputils.VersionFromContext(ctx)
var status bool
if value := r.URL.Query().Get("status"); value != "" && !versions.LessThan(cliVersion, "1.41") {
var err error
status, err = strconv.ParseBool(value)
if err != nil {
return errors.Wrapf(errdefs.InvalidParameter(err), "invalid value for status: %s", value)
}
}
services, err := sr.backend.GetServices(basictypes.ServiceListOptions{Filters: filter, Status: status})
if err != nil { if err != nil {
logrus.Errorf("Error getting services: %v", err) logrus.Errorf("Error getting services: %v", err)
return err return err
@ -178,15 +190,21 @@ func (sr *swarmRouter) getServices(ctx context.Context, w http.ResponseWriter, r
func (sr *swarmRouter) getService(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { func (sr *swarmRouter) getService(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
var insertDefaults bool var insertDefaults bool
if value := r.URL.Query().Get("insertDefaults"); value != "" { if value := r.URL.Query().Get("insertDefaults"); value != "" {
var err error var err error
insertDefaults, err = strconv.ParseBool(value) insertDefaults, err = strconv.ParseBool(value)
if err != nil { if err != nil {
err := fmt.Errorf("invalid value for insertDefaults: %s", value)
return errors.Wrapf(errdefs.InvalidParameter(err), "invalid value for insertDefaults: %s", value) return errors.Wrapf(errdefs.InvalidParameter(err), "invalid value for insertDefaults: %s", value)
} }
} }
// you may note that there is no code here to handle the "status" query
// parameter, as in getServices. the Status field is not supported when
// retrieving an individual service because the Backend API changes
// required to accommodate it would be too disruptive, and because that
// field is so rarely needed as part of an individual service inspection.
service, err := sr.backend.GetService(vars["id"], insertDefaults) service, err := sr.backend.GetService(vars["id"], insertDefaults)
if err != nil { if err != nil {
logrus.Errorf("Error getting service %s: %v", vars["id"], err) logrus.Errorf("Error getting service %s: %v", vars["id"], err)

View file

@ -3369,6 +3369,27 @@ definitions:
format: "dateTime" format: "dateTime"
Message: Message:
type: "string" type: "string"
ServiceStatus:
description: |
The status of the service's tasks. Provided only when requested as
part of a ServiceList operation.
type: "object"
properties:
RunningTasks:
description: "The number of tasks for the service currently in the Running state"
type: "integer"
format: "uint64"
example: 7
DesiredTasks:
description: |
The number of tasks for the service desired to be running.
For replicated services, this is the replica count from the
service spec. For global services, this is computed by taking
count of all tasks for the service with a Desired State other
than Shutdown.
type: "integer"
format: "uint64"
example: 10
example: example:
ID: "9mnpnzenvg8p8tdbtq4wvbkcz" ID: "9mnpnzenvg8p8tdbtq4wvbkcz"
Version: Version:
@ -9316,6 +9337,10 @@ paths:
- `label=<service label>` - `label=<service label>`
- `mode=["replicated"|"global"]` - `mode=["replicated"|"global"]`
- `name=<service name>` - `name=<service name>`
- name: "status"
in: "query"
type: "boolean"
description: "Include service status, with count of running and desired tasks"
tags: ["Service"] tags: ["Service"]
/services/create: /services/create:
post: post:

View file

@ -363,6 +363,10 @@ type ServiceUpdateOptions struct {
// ServiceListOptions holds parameters to list services with. // ServiceListOptions holds parameters to list services with.
type ServiceListOptions struct { type ServiceListOptions struct {
Filters filters.Args Filters filters.Args
// Status indicates whether the server should include the service task
// count of running and desired tasks.
Status bool
} }
// ServiceInspectOptions holds parameters related to the "service inspect" // ServiceInspectOptions holds parameters related to the "service inspect"

View file

@ -10,6 +10,13 @@ type Service struct {
PreviousSpec *ServiceSpec `json:",omitempty"` PreviousSpec *ServiceSpec `json:",omitempty"`
Endpoint Endpoint `json:",omitempty"` Endpoint Endpoint `json:",omitempty"`
UpdateStatus *UpdateStatus `json:",omitempty"` UpdateStatus *UpdateStatus `json:",omitempty"`
// ServiceStatus is an optional, extra field indicating the number of
// desired and running tasks. It is provided primarily as a shortcut to
// calculating these values client-side, which otherwise would require
// listing all tasks for a service, an operation that could be
// computation and network expensive.
ServiceStatus *ServiceStatus `json:",omitempty"`
} }
// ServiceSpec represents the spec of a service. // ServiceSpec represents the spec of a service.
@ -122,3 +129,17 @@ type UpdateConfig struct {
// started, or the new task is started before the old task is shut down. // started, or the new task is started before the old task is shut down.
Order string Order string
} }
// ServiceStatus represents the number of running tasks in a service and the
// number of tasks desired to be running.
type ServiceStatus struct {
// RunningTasks is the number of tasks for the service actually in the
// Running state
RunningTasks uint64
// DesiredTasks is the number of tasks desired to be running by the
// service. For replicated services, this is the replica count. For global
// services, this is computed by taking the number of tasks with desired
// state of not-Shutdown.
DesiredTasks uint64
}

View file

@ -23,6 +23,10 @@ func (cli *Client) ServiceList(ctx context.Context, options types.ServiceListOpt
query.Set("filters", filterJSON) query.Set("filters", filterJSON)
} }
if options.Status {
query.Set("status", "true")
}
resp, err := cli.get(ctx, "/services", query, nil) resp, err := cli.get(ctx, "/services", query, nil)
defer ensureReaderClosed(resp) defer ensureReaderClosed(resp)
if err != nil { if err != nil {

View file

@ -77,6 +77,12 @@ func (c *Cluster) GetServices(options apitypes.ServiceListOptions) ([]types.Serv
services := make([]types.Service, 0, len(r.Services)) services := make([]types.Service, 0, len(r.Services))
// if the user requests the service statuses, we'll store the IDs needed
// in this slice
var serviceIDs []string
if options.Status {
serviceIDs = make([]string, 0, len(r.Services))
}
for _, service := range r.Services { for _, service := range r.Services {
if options.Filters.Contains("mode") { if options.Filters.Contains("mode") {
var mode string var mode string
@ -91,6 +97,9 @@ func (c *Cluster) GetServices(options apitypes.ServiceListOptions) ([]types.Serv
continue continue
} }
} }
if options.Status {
serviceIDs = append(serviceIDs, service.ID)
}
svcs, err := convert.ServiceFromGRPC(*service) svcs, err := convert.ServiceFromGRPC(*service)
if err != nil { if err != nil {
return nil, err return nil, err
@ -98,6 +107,49 @@ func (c *Cluster) GetServices(options apitypes.ServiceListOptions) ([]types.Serv
services = append(services, svcs) services = append(services, svcs)
} }
if options.Status {
// Listing service statuses is a separate call because, while it is the
// most common UI operation, it is still just a UI operation, and it
// would be improper to include this data in swarm's Service object.
// We pay the cost with some complexity here, but this is still way
// more efficient than marshalling and unmarshalling all the JSON
// needed to list tasks and get this data otherwise client-side
resp, err := state.controlClient.ListServiceStatuses(
ctx,
&swarmapi.ListServiceStatusesRequest{Services: serviceIDs},
grpc.MaxCallRecvMsgSize(defaultRecvSizeForListResponse),
)
if err != nil {
return nil, err
}
// we'll need to match up statuses in the response with the services in
// the list operation. if we did this by operating on two lists, the
// result would be quadratic. instead, make a mapping of service IDs to
// service statuses so that this is roughly linear. additionally,
// convert the status response to an engine api service status here.
serviceMap := map[string]*types.ServiceStatus{}
for _, status := range resp.Statuses {
serviceMap[status.ServiceID] = &types.ServiceStatus{
RunningTasks: status.RunningTasks,
DesiredTasks: status.DesiredTasks,
}
}
// because this is a list of values and not pointers, make sure we
// actually alter the value when iterating.
for i, service := range services {
// the return value of the ListServiceStatuses operation is
// guaranteed to contain a value in the response for every argument
// in the request, so we can safely do this assignment. and even if
// it wasn't, and the service ID was for some reason absent from
// this map, the resulting value of service.Status would just be
// nil -- the same thing it was before
service.ServiceStatus = serviceMap[service.ID]
services[i] = service
}
}
return services, nil return services, nil
} }

View file

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

View file

@ -0,0 +1,108 @@
package service // import "github.com/docker/docker/integration/service"
import (
"context"
"fmt"
"testing"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
swarmtypes "github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/versions"
"github.com/docker/docker/integration/internal/swarm"
"gotest.tools/assert"
is "gotest.tools/assert/cmp"
"gotest.tools/poll"
"gotest.tools/skip"
)
// TestServiceListWithStatuses tests that performing a ServiceList operation
// correctly uses the Status parameter, and that the resulting response
// contains correct service statuses.
//
// NOTE(dperny): because it's a pain to elicit the behavior of an unconverged
// service reliably, I'm not testing that an unconverged service returns X
// running and Y desired tasks. Instead, I'm just going to trust that I can
// successfully assign a value to another value without screwing it up. The
// logic for computing service statuses is in swarmkit anyway, not in the
// engine, and is well-tested there, so this test just needs to make sure that
// statuses get correctly associated with the right services.
func TestServiceListWithStatuses(t *testing.T) {
skip.If(t, testEnv.IsRemoteDaemon)
skip.If(t, testEnv.DaemonInfo.OSType == "windows")
// statuses were added in API version 1.41
skip.If(t, versions.LessThan(testEnv.DaemonInfo.ServerVersion, "1.41"))
defer setupTest(t)()
d := swarm.NewSwarm(t, testEnv)
defer d.Stop(t)
client := d.NewClientT(t)
defer client.Close()
ctx := context.Background()
serviceCount := 3
// create some services.
for i := 0; i < serviceCount; i++ {
spec := fullSwarmServiceSpec(fmt.Sprintf("test-list-%d", i), uint64(i+1))
// for whatever reason, the args "-u root", when included, cause these
// tasks to fail and exit. instead, we'll just pass no args, which
// works.
spec.TaskTemplate.ContainerSpec.Args = []string{}
resp, err := client.ServiceCreate(ctx, spec, types.ServiceCreateOptions{
QueryRegistry: false,
})
assert.NilError(t, err)
id := resp.ID
// we need to wait specifically for the tasks to be running, which the
// serviceContainerCount function does not do. instead, we'll use a
// bespoke closure right here.
poll.WaitOn(t, func(log poll.LogT) poll.Result {
filter := filters.NewArgs()
filter.Add("service", id)
tasks, err := client.TaskList(context.Background(), types.TaskListOptions{
Filters: filter,
})
running := 0
for _, task := range tasks {
if task.Status.State == swarmtypes.TaskStateRunning {
running++
}
}
switch {
case err != nil:
return poll.Error(err)
case running == i+1:
return poll.Success()
default:
return poll.Continue(
"running task count %d (%d total), waiting for %d",
running, len(tasks), i+1,
)
}
})
}
// now, let's do the list operation with no status arg set.
resp, err := client.ServiceList(ctx, types.ServiceListOptions{})
assert.NilError(t, err)
assert.Check(t, is.Len(resp, serviceCount))
for _, service := range resp {
assert.Check(t, is.Nil(service.ServiceStatus))
}
// now try again, but with Status: true. This time, we should have statuses
resp, err = client.ServiceList(ctx, types.ServiceListOptions{Status: true})
assert.NilError(t, err)
assert.Check(t, is.Len(resp, serviceCount))
for _, service := range resp {
replicas := *service.Spec.Mode.Replicated.Replicas
assert.Assert(t, service.ServiceStatus != nil)
// Use assert.Check to not fail out of the test if this fails
assert.Check(t, is.Equal(service.ServiceStatus.DesiredTasks, replicas))
assert.Check(t, is.Equal(service.ServiceStatus.RunningTasks, replicas))
}
}