api/types/swarm: remove deprecated ServiceSpec.Networks field

This field was deprecated in [engine-api@5c4b684], which got vendored into
Moby in [moby@8f7a8c7] (API v1.25), and wired up in [moby@99a98cc].

[engine-api@5c4b684]: 5c4b684b2f
[moby@8f7a8c7]: 8f7a8c75ae
[moby@99a98cc]: 99a98ccc14

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn
2025-10-14 12:46:16 +02:00
parent ec83dd46ed
commit 5061d0a74d
7 changed files with 101 additions and 39 deletions

View File

@@ -35,12 +35,7 @@ type ServiceSpec struct {
Mode ServiceMode `json:",omitempty"`
UpdateConfig *UpdateConfig `json:",omitempty"`
RollbackConfig *UpdateConfig `json:",omitempty"`
// Networks specifies which networks the service should attach to.
//
// Deprecated: This field is deprecated since v1.44. The Networks field in TaskSpec should be used instead.
Networks []NetworkAttachmentConfig `json:",omitempty"`
EndpointSpec *EndpointSpec `json:",omitempty"`
EndpointSpec *EndpointSpec `json:",omitempty"`
}
// ServiceMode represents the mode of a service.

View File

@@ -94,17 +94,30 @@ func serviceSpecFromGRPC(spec *swarmapi.ServiceSpec) (*types.ServiceSpec, error)
return nil, nil
}
serviceNetworks := make([]types.NetworkAttachmentConfig, 0, len(spec.Networks))
for _, n := range spec.Networks {
netConfig := types.NetworkAttachmentConfig{Target: n.Target, Aliases: n.Aliases, DriverOpts: n.DriverAttachmentOpts}
serviceNetworks = append(serviceNetworks, netConfig)
}
taskTemplate, err := taskSpecFromGRPC(spec.Task)
if err != nil {
return nil, err
}
if len(taskTemplate.Networks) == 0 && len(spec.Networks) > 0 {
// The ServiceSpec.Networks field was deprecated in API v1.25, with
// the deprecation notice updated in API v1.44. We only consider this
// field on API < v1.44 and if the replacement TaskSpec.Networks is
// not set.
//
// Account for any service that was created using the old spec.
//
// TODO(thaJeztah): would swarm still return this? Remove this when we drop API v1.25
taskTemplate.Networks = make([]types.NetworkAttachmentConfig, 0, len(spec.Networks))
for _, n := range spec.Networks {
taskTemplate.Networks = append(taskTemplate.Networks, types.NetworkAttachmentConfig{
Target: n.Target,
Aliases: n.Aliases,
DriverOpts: n.DriverAttachmentOpts,
})
}
}
switch t := spec.Task.GetRuntime().(type) {
case *swarmapi.TaskSpec_Container:
containerConfig := t.Container
@@ -125,7 +138,6 @@ func serviceSpecFromGRPC(spec *swarmapi.ServiceSpec) (*types.ServiceSpec, error)
convertedSpec := &types.ServiceSpec{
Annotations: annotationsFromGRPC(spec.Annotations),
TaskTemplate: taskTemplate,
Networks: serviceNetworks,
EndpointSpec: endpointSpecFromGRPC(spec.Endpoint),
}
@@ -160,12 +172,6 @@ func ServiceSpecToGRPC(s types.ServiceSpec) (swarmapi.ServiceSpec, error) {
name = namesgenerator.GetRandomName(0)
}
serviceNetworks := make([]*swarmapi.NetworkAttachmentConfig, 0, len(s.Networks)) //nolint:staticcheck // ignore SA1019: field is deprecated.
for _, n := range s.Networks { //nolint:staticcheck // ignore SA1019: field is deprecated.
netConfig := &swarmapi.NetworkAttachmentConfig{Target: n.Target, Aliases: n.Aliases, DriverAttachmentOpts: n.DriverOpts}
serviceNetworks = append(serviceNetworks, netConfig)
}
taskNetworks := make([]*swarmapi.NetworkAttachmentConfig, 0, len(s.TaskTemplate.Networks))
for _, n := range s.TaskTemplate.Networks {
netConfig := &swarmapi.NetworkAttachmentConfig{Target: n.Target, Aliases: n.Aliases, DriverAttachmentOpts: n.DriverOpts}
@@ -183,7 +189,6 @@ func ServiceSpecToGRPC(s types.ServiceSpec) (swarmapi.ServiceSpec, error) {
Networks: taskNetworks,
ForceUpdate: s.TaskTemplate.ForceUpdate,
},
Networks: serviceNetworks,
}
switch s.TaskTemplate.Runtime {
@@ -672,8 +677,11 @@ func networkAttachmentSpecFromGRPC(attachment swarmapi.NetworkAttachmentSpec) *t
func taskSpecFromGRPC(taskSpec swarmapi.TaskSpec) (types.TaskSpec, error) {
taskNetworks := make([]types.NetworkAttachmentConfig, 0, len(taskSpec.Networks))
for _, n := range taskSpec.Networks {
netConfig := types.NetworkAttachmentConfig{Target: n.Target, Aliases: n.Aliases, DriverOpts: n.DriverAttachmentOpts}
taskNetworks = append(taskNetworks, netConfig)
taskNetworks = append(taskNetworks, types.NetworkAttachmentConfig{
Target: n.Target,
Aliases: n.Aliases,
DriverOpts: n.DriverAttachmentOpts,
})
}
t := types.TaskSpec{

View File

@@ -328,12 +328,7 @@ func (c *Cluster) RemoveNetwork(input string) error {
}
func (c *Cluster) populateNetworkID(ctx context.Context, client swarmapi.ControlClient, s *types.ServiceSpec) error {
// Always prefer NetworkAttachmentConfigs from TaskTemplate
// but fallback to service spec for backward compatibility
networks := s.TaskTemplate.Networks
if len(networks) == 0 {
networks = s.Networks //nolint:staticcheck // ignore SA1019: field is deprecated.
}
for i, nw := range networks {
apiNetwork, err := getNetwork(ctx, client, nw.Target, nil)
if err != nil {

View File

@@ -10,6 +10,7 @@ import (
"github.com/moby/moby/api/types/registry"
types "github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/api/types/versions"
"github.com/moby/moby/v2/daemon/internal/compat"
"github.com/moby/moby/v2/daemon/internal/filters"
"github.com/moby/moby/v2/daemon/server/backend"
"github.com/moby/moby/v2/daemon/server/httputils"
@@ -171,6 +172,13 @@ func (sr *swarmRouter) getServices(ctx context.Context, w http.ResponseWriter, r
log.G(ctx).WithContext(ctx).WithError(err).Debug("Error getting services")
return err
}
if versions.LessThan(cliVersion, "1.25") {
legacyResponse := make([]*compat.Wrapper, 0, len(services))
for _, s := range services {
legacyResponse = append(legacyResponse, backFillLegacyNetwork(s))
}
return httputils.WriteJSON(w, http.StatusOK, legacyResponse)
}
return httputils.WriteJSON(w, http.StatusOK, services)
}
@@ -201,11 +209,29 @@ func (sr *swarmRouter) getService(ctx context.Context, w http.ResponseWriter, r
return err
}
cliVersion := httputils.VersionFromContext(ctx)
if versions.LessThan(cliVersion, "1.25") {
return httputils.WriteJSON(w, http.StatusOK, backFillLegacyNetwork(service))
}
return httputils.WriteJSON(w, http.StatusOK, service)
}
// serviceWithLegacy is used to unmarshal legacy requests that contain
// the ServiceSpec.Networks field.
type serviceWithLegacy struct {
types.ServiceSpec
// Networks specifies which networks the service should attach to.
//
// This field was deprecated in API v1.25, with the deprecation notice
// updated in API v1.44. We only consider this field on API < v1.44,
// and if the replacement TaskSpec.Networks is not set.
Networks []types.NetworkAttachmentConfig `json:",omitempty"`
}
func (sr *swarmRouter) createService(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
var service types.ServiceSpec
var service serviceWithLegacy
if err := httputils.ReadJSON(r, &service); err != nil {
return err
}
@@ -220,7 +246,8 @@ func (sr *swarmRouter) createService(ctx context.Context, w http.ResponseWriter,
adjustForAPIVersion(v, &service)
}
resp, err := sr.backend.CreateService(service, encodedAuth, queryRegistry)
serviceSpec := service.ServiceSpec
resp, err := sr.backend.CreateService(serviceSpec, encodedAuth, queryRegistry)
if err != nil {
log.G(ctx).WithFields(log.Fields{
"error": err,
@@ -233,7 +260,7 @@ func (sr *swarmRouter) createService(ctx context.Context, w http.ResponseWriter,
}
func (sr *swarmRouter) updateService(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
var service types.ServiceSpec
var service serviceWithLegacy
if err := httputils.ReadJSON(r, &service); err != nil {
return err
}
@@ -259,7 +286,8 @@ func (sr *swarmRouter) updateService(ctx context.Context, w http.ResponseWriter,
adjustForAPIVersion(v, &service)
}
resp, err := sr.backend.UpdateService(vars["id"], version, service, flags, queryRegistry)
serviceSpec := service.ServiceSpec
resp, err := sr.backend.UpdateService(vars["id"], version, serviceSpec, flags, queryRegistry)
if err != nil {
log.G(ctx).WithContext(ctx).WithFields(log.Fields{
"error": err,
@@ -549,3 +577,22 @@ func (sr *swarmRouter) updateConfig(ctx context.Context, w http.ResponseWriter,
id := vars["id"]
return sr.backend.UpdateConfig(id, version, config)
}
func backFillLegacyNetwork(s types.Service) *compat.Wrapper {
var opts []compat.Option
if len(s.Spec.TaskTemplate.Networks) > 0 {
opts = append(opts, compat.WithExtraFields(map[string]any{
"Spec": map[string]any{
"Networks": s.Spec.TaskTemplate.Networks,
},
}))
}
if s.PreviousSpec != nil && len(s.PreviousSpec.TaskTemplate.Networks) > 0 {
opts = append(opts, compat.WithExtraFields(map[string]any{
"PreviousSpec": map[string]any{
"Networks": s.PreviousSpec.TaskTemplate.Networks,
},
}))
}
return compat.Wrap(s, opts...)
}

View File

@@ -73,7 +73,7 @@ func (sr *swarmRouter) swarmLogs(ctx context.Context, w http.ResponseWriter, r *
// adjustForAPIVersion takes a version and service spec and removes fields to
// make the spec compatible with the specified version.
func adjustForAPIVersion(cliVersion string, service *swarm.ServiceSpec) {
func adjustForAPIVersion(cliVersion string, service *serviceWithLegacy) {
if cliVersion == "" {
return
}
@@ -142,6 +142,25 @@ func adjustForAPIVersion(cliVersion string, service *swarm.ServiceSpec) {
service.TaskTemplate.ContainerSpec.Healthcheck.StartInterval = 0
}
}
// Always prefer NetworkAttachmentConfigs from TaskTemplate
// but fallback to service spec for backward compatibility.
//
// The ServiceSpec.Networks field was deprecated in API v1.25, with
// the deprecation notice updated in API v1.44. We only consider this
// field on API < v1.44 and if the replacement TaskSpec.Networks is
// not set.
if len(service.TaskTemplate.Networks) == 0 && len(service.Networks) > 0 {
service.TaskTemplate.Networks = make([]swarm.NetworkAttachmentConfig, 0, len(service.Networks))
for _, n := range service.Networks {
service.TaskTemplate.Networks = append(service.TaskTemplate.Networks, swarm.NetworkAttachmentConfig{
Target: n.Target,
Aliases: n.Aliases,
DriverOpts: n.DriverOpts,
})
}
}
}
if versions.LessThan(cliVersion, "1.46") {

View File

@@ -14,7 +14,7 @@ func TestAdjustForAPIVersion(t *testing.T) {
// testing the negative -- does this leave everything else alone? -- is
// prohibitively time-consuming to write, because it would need an object
// with literally every field filled in.
spec := &swarm.ServiceSpec{
serviceSpec := swarm.ServiceSpec{
TaskTemplate: swarm.TaskSpec{
ContainerSpec: &swarm.ContainerSpec{
Sysctls: expectedSysctls,
@@ -70,6 +70,9 @@ func TestAdjustForAPIVersion(t *testing.T) {
},
}
spec := &serviceWithLegacy{
ServiceSpec: serviceSpec,
}
adjustForAPIVersion("1.46", spec)
if !reflect.DeepEqual(
spec.TaskTemplate.ContainerSpec.Mounts[0].TmpfsOptions.Options,

View File

@@ -35,12 +35,7 @@ type ServiceSpec struct {
Mode ServiceMode `json:",omitempty"`
UpdateConfig *UpdateConfig `json:",omitempty"`
RollbackConfig *UpdateConfig `json:",omitempty"`
// Networks specifies which networks the service should attach to.
//
// Deprecated: This field is deprecated since v1.44. The Networks field in TaskSpec should be used instead.
Networks []NetworkAttachmentConfig `json:",omitempty"`
EndpointSpec *EndpointSpec `json:",omitempty"`
EndpointSpec *EndpointSpec `json:",omitempty"`
}
// ServiceMode represents the mode of a service.