Просмотр исходного кода

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>
Drew Erny 6 лет назад
Родитель
Сommit
f36042d259

+ 20 - 2
api/server/router/swarm/cluster_routes.go

@@ -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)

+ 25 - 0
api/swagger.yaml

@@ -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:

+ 4 - 0
api/types/client.go

@@ -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"

+ 21 - 0
api/types/swarm/service.go

@@ -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
+}

+ 4 - 0
client/service_list.go

@@ -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 {

+ 52 - 0
daemon/cluster/services.go

@@ -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
 }
 }
 
 

+ 3 - 0
docs/api/version-history.md

@@ -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
 
 

+ 108 - 0
integration/service/list_test.go

@@ -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))
+	}
+
+}