diff --git a/api/server/router/swarm/cluster_routes.go b/api/server/router/swarm/cluster_routes.go index 1e0e6100f7..509c7acad5 100644 --- a/api/server/router/swarm/cluster_routes.go +++ b/api/server/router/swarm/cluster_routes.go @@ -219,6 +219,12 @@ func (sr *swarmRouter) createService(ctx context.Context, w http.ResponseWriter, // API version 1.40 service.TaskTemplate.ContainerSpec.Sysctls = nil } + + if service.TaskTemplate.Placement != nil { + // MaxReplicas for docker swarm services weren't supported before + // API version 1.40 + service.TaskTemplate.Placement.MaxReplicas = 0 + } } } @@ -265,6 +271,12 @@ func (sr *swarmRouter) updateService(ctx context.Context, w http.ResponseWriter, // API version 1.40 service.TaskTemplate.ContainerSpec.Sysctls = nil } + + if service.TaskTemplate.Placement != nil { + // MaxReplicas for docker swarm services weren't supported before + // API version 1.40 + service.TaskTemplate.Placement.MaxReplicas = 0 + } } } diff --git a/api/swagger.yaml b/api/swagger.yaml index a052b8316b..06dff2eb7c 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -2878,6 +2878,11 @@ definitions: SpreadDescriptor: "node.labels.datacenter" - Spread: SpreadDescriptor: "node.labels.rack" + MaxReplicas: + description: "Maximum number of replicas for per node (default value is 0, which is unlimited)" + type: "integer" + format: "int64" + default: 0 Platforms: description: | Platforms stores all the platforms that the service's image can diff --git a/api/types/swarm/task.go b/api/types/swarm/task.go index b35605d12f..d5a57df5db 100644 --- a/api/types/swarm/task.go +++ b/api/types/swarm/task.go @@ -127,6 +127,7 @@ type ResourceRequirements struct { type Placement struct { Constraints []string `json:",omitempty"` Preferences []PlacementPreference `json:",omitempty"` + MaxReplicas uint64 `json:",omitempty"` // Platforms stores all the platforms that the image can run on. // This field is used in the platform filter for scheduling. If empty, diff --git a/daemon/cluster/convert/service.go b/daemon/cluster/convert/service.go index 09b023c9d7..2b72342590 100644 --- a/daemon/cluster/convert/service.go +++ b/daemon/cluster/convert/service.go @@ -246,6 +246,7 @@ func ServiceSpecToGRPC(s types.ServiceSpec) (swarmapi.ServiceSpec, error) { spec.Task.Placement = &swarmapi.Placement{ Constraints: s.TaskTemplate.Placement.Constraints, Preferences: preferences, + MaxReplicas: s.TaskTemplate.Placement.MaxReplicas, Platforms: platforms, } } @@ -472,6 +473,7 @@ func placementFromGRPC(p *swarmapi.Placement) *types.Placement { } r := &types.Placement{ Constraints: p.Constraints, + MaxReplicas: p.MaxReplicas, } for _, pref := range p.Preferences { diff --git a/docs/api/version-history.md b/docs/api/version-history.md index f0cdbdb189..3a838839e2 100644 --- a/docs/api/version-history.md +++ b/docs/api/version-history.md @@ -33,6 +33,10 @@ keywords: "API, Docker, rcli, REST, documentation" * `GET /info` now returns information about `DataPathPort` that is currently used in swarm * `GET /swarm` endpoint now returns DataPathPort info * `POST /containers/create` now takes `KernelMemoryTCP` field to set hard limit for kernel TCP buffer memory. +* `GET /service` now returns `MaxReplicas` as part of the `Placement`. +* `GET /service/{id}` now returns `MaxReplicas` as part of the `Placement`. +* `POST /service/create` and `POST /services/(id or name)/update` now take the field `MaxReplicas` + as part of the service `Placement`, allowing to specify maximum replicas per node for the service. ## V1.39 API changes diff --git a/integration/internal/swarm/service.go b/integration/internal/swarm/service.go index 83eb0a6ba2..33e644e935 100644 --- a/integration/internal/swarm/service.go +++ b/integration/internal/swarm/service.go @@ -141,6 +141,14 @@ func ServiceWithReplicas(n uint64) ServiceSpecOpt { } } +// ServiceWithMaxReplicas sets the max replicas for the service +func ServiceWithMaxReplicas(n uint64) ServiceSpecOpt { + return func(spec *swarmtypes.ServiceSpec) { + ensurePlacement(spec) + spec.TaskTemplate.Placement.MaxReplicas = n + } +} + // ServiceWithName sets the name of the service func ServiceWithName(name string) ServiceSpecOpt { return func(spec *swarmtypes.ServiceSpec) { @@ -210,3 +218,9 @@ func ensureContainerSpec(spec *swarmtypes.ServiceSpec) { spec.TaskTemplate.ContainerSpec = &swarmtypes.ContainerSpec{} } } + +func ensurePlacement(spec *swarmtypes.ServiceSpec) { + if spec.TaskTemplate.Placement == nil { + spec.TaskTemplate.Placement = &swarmtypes.Placement{} + } +} diff --git a/integration/service/create_test.go b/integration/service/create_test.go index 2a87af483d..8731ed4890 100644 --- a/integration/service/create_test.go +++ b/integration/service/create_test.go @@ -153,6 +153,26 @@ func TestCreateServiceConflict(t *testing.T) { assert.Check(t, is.Contains(string(buf), "service "+serviceName+" already exists")) } +func TestCreateServiceMaxReplicas(t *testing.T) { + defer setupTest(t)() + d := swarm.NewSwarm(t, testEnv) + defer d.Stop(t) + client := d.NewClientT(t) + defer client.Close() + + var maxReplicas uint64 = 2 + serviceSpec := []swarm.ServiceSpecOpt{ + swarm.ServiceWithReplicas(maxReplicas), + swarm.ServiceWithMaxReplicas(maxReplicas), + } + + serviceID := swarm.CreateService(t, d, serviceSpec...) + poll.WaitOn(t, serviceRunningTasksCount(client, serviceID, maxReplicas), swarm.ServicePoll) + + _, _, err := client.ServiceInspectWithRaw(context.Background(), serviceID, types.ServiceInspectOptions{}) + assert.NilError(t, err) +} + func TestCreateWithDuplicateNetworkNames(t *testing.T) { skip.If(t, testEnv.DaemonInfo.OSType == "windows") defer setupTest(t)()