diff --git a/api/swagger.yaml b/api/swagger.yaml index defd7334d3..be71e0ffa3 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -7399,6 +7399,7 @@ paths: - `id=` - `label=` + - `mode=["replicated"|"global"]` - `name=` tags: ["Service"] /services/create: diff --git a/daemon/cluster/filters.go b/daemon/cluster/filters.go index 88668edaac..554694da2c 100644 --- a/daemon/cluster/filters.go +++ b/daemon/cluster/filters.go @@ -45,22 +45,6 @@ func newListNodesFilters(filter filters.Args) (*swarmapi.ListNodesRequest_Filter return f, nil } -func newListServicesFilters(filter filters.Args) (*swarmapi.ListServicesRequest_Filters, error) { - accepted := map[string]bool{ - "name": true, - "id": true, - "label": true, - } - if err := filter.Validate(accepted); err != nil { - return nil, err - } - return &swarmapi.ListServicesRequest_Filters{ - NamePrefixes: filter.Get("name"), - IDPrefixes: filter.Get("id"), - Labels: runconfigopts.ConvertKVStringsToMap(filter.Get("label")), - }, nil -} - func newListTasksFilters(filter filters.Args, transformFunc func(filters.Args) error) (*swarmapi.ListTasksRequest_Filters, error) { accepted := map[string]bool{ "name": true, diff --git a/daemon/cluster/services.go b/daemon/cluster/services.go index c9d0b93dbb..bdb91ea25b 100644 --- a/daemon/cluster/services.go +++ b/daemon/cluster/services.go @@ -21,6 +21,7 @@ import ( "github.com/docker/docker/daemon/logger" "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/stdcopy" + runconfigopts "github.com/docker/docker/runconfig/opts" swarmapi "github.com/docker/swarmkit/api" gogotypes "github.com/gogo/protobuf/types" "github.com/pkg/errors" @@ -37,10 +38,25 @@ func (c *Cluster) GetServices(options apitypes.ServiceListOptions) ([]types.Serv return nil, c.errNoManager(state) } - filters, err := newListServicesFilters(options.Filters) - if err != nil { + // We move the accepted filter check here as "mode" filter + // is processed in the daemon, not in SwarmKit. So it might + // be good to have accepted file check in the same file as + // the filter processing (in the for loop below). + accepted := map[string]bool{ + "name": true, + "id": true, + "label": true, + "mode": true, + } + if err := options.Filters.Validate(accepted); err != nil { return nil, err } + filters := &swarmapi.ListServicesRequest_Filters{ + NamePrefixes: options.Filters.Get("name"), + IDPrefixes: options.Filters.Get("id"), + Labels: runconfigopts.ConvertKVStringsToMap(options.Filters.Get("label")), + } + ctx, cancel := c.getRequestContext() defer cancel() @@ -54,6 +70,19 @@ func (c *Cluster) GetServices(options apitypes.ServiceListOptions) ([]types.Serv services := []types.Service{} for _, service := range r.Services { + if options.Filters.Include("mode") { + var mode string + switch service.Spec.GetMode().(type) { + case *swarmapi.ServiceSpec_Global: + mode = "global" + case *swarmapi.ServiceSpec_Replicated: + mode = "replicated" + } + + if !options.Filters.ExactMatch("mode", mode) { + continue + } + } services = append(services, convert.ServiceFromGRPC(*service)) } diff --git a/docs/api/version-history.md b/docs/api/version-history.md index 3a92bdbb5d..199876ca91 100644 --- a/docs/api/version-history.md +++ b/docs/api/version-history.md @@ -27,6 +27,7 @@ keywords: "API, Docker, rcli, REST, documentation" * `POST /build` now accepts `extrahosts` parameter to specify a host to ip mapping to use during the build. * `POST /services/create` and `POST /services/(id or name)/update` now accept a `rollback` value for `FailureAction`. * `POST /services/create` and `POST /services/(id or name)/update` now accept an optional `RollbackConfig` object which specifies rollback options. +* `GET /services` now supports a `mode` filter to filter services based on the service mode (either `global` or `replicated`). ## v1.27 API changes diff --git a/docs/reference/commandline/service_ls.md b/docs/reference/commandline/service_ls.md index 86273ff1ff..6b07b84053 100644 --- a/docs/reference/commandline/service_ls.md +++ b/docs/reference/commandline/service_ls.md @@ -60,6 +60,7 @@ The currently supported filters are: * [id](service_ls.md#id) * [label](service_ls.md#label) +* [mode](service_ls.md#mode) * [name](service_ls.md#name) #### id @@ -98,6 +99,18 @@ ID NAME MODE REPLICAS IMAGE 74nzcxxjv6fq backend replicated 3/3 redis:3.0.6 ``` +#### mode + +The `mode` filter matches on the mode (either `replicated` or `global`) of a service. + +The following filter matches only `global` services. + +```bash +$ docker service ls --filter mode=global +ID NAME MODE REPLICAS IMAGE +w7y0v2yrn620 top global 1/1 busybox +``` + #### name The `name` filter matches on all or part of a service's name. diff --git a/integration-cli/docker_cli_swarm_test.go b/integration-cli/docker_cli_swarm_test.go index 026891e3ef..a3a287b4c9 100644 --- a/integration-cli/docker_cli_swarm_test.go +++ b/integration-cli/docker_cli_swarm_test.go @@ -1797,3 +1797,34 @@ func (s *DockerSwarmSuite) TestSwarmStopSignal(c *check.C) { c.Assert(err, checker.IsNil, check.Commentf(out)) c.Assert(strings.TrimSpace(out), checker.Equals, "SIGUSR1") } + +func (s *DockerSwarmSuite) TestSwarmServiceLsFilterMode(c *check.C) { + d := s.AddDaemon(c, true, true) + + out, err := d.Cmd("service", "create", "--name", "top1", "busybox", "top") + c.Assert(err, checker.IsNil, check.Commentf(out)) + c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "") + + out, err = d.Cmd("service", "create", "--name", "top2", "--mode=global", "busybox", "top") + c.Assert(err, checker.IsNil, check.Commentf(out)) + c.Assert(strings.TrimSpace(out), checker.Not(checker.Equals), "") + + // make sure task has been deployed. + waitAndAssert(c, defaultReconciliationTimeout, d.CheckActiveContainerCount, checker.Equals, 2) + + out, err = d.Cmd("service", "ls") + c.Assert(err, checker.IsNil, check.Commentf(out)) + c.Assert(out, checker.Contains, "top1") + c.Assert(out, checker.Contains, "top2") + c.Assert(out, checker.Not(checker.Contains), "localnet") + + out, err = d.Cmd("service", "ls", "--filter", "mode=global") + c.Assert(out, checker.Not(checker.Contains), "top1") + c.Assert(out, checker.Contains, "top2") + c.Assert(err, checker.IsNil, check.Commentf(out)) + + out, err = d.Cmd("service", "ls", "--filter", "mode=replicated") + c.Assert(err, checker.IsNil, check.Commentf(out)) + c.Assert(out, checker.Contains, "top1") + c.Assert(out, checker.Not(checker.Contains), "top2") +}