From ab6c37160d9ef82ee7c32bf5684f871286176042 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sun, 15 Feb 2026 17:48:44 +0100 Subject: [PATCH 1/6] cli/compose/convert: convertUlimits: modernize Signed-off-by: Sebastiaan van Stijn --- cli/compose/convert/service.go | 35 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/cli/compose/convert/service.go b/cli/compose/convert/service.go index 957ddb10a8d0..2a93d7e0fbb2 100644 --- a/cli/compose/convert/service.go +++ b/cli/compose/convert/service.go @@ -1,11 +1,16 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.24 + package convert import ( + "cmp" "context" "errors" "fmt" "net/netip" "os" + "slices" "sort" "strings" "time" @@ -702,28 +707,22 @@ func convertCredentialSpec(namespace Namespace, spec composetypes.CredentialSpec } func convertUlimits(origUlimits map[string]*composetypes.UlimitsConfig) []*container.Ulimit { - newUlimits := make(map[string]*container.Ulimit) + ulimits := make([]*container.Ulimit, 0, len(origUlimits)) for name, u := range origUlimits { + soft, hard := int64(u.Soft), int64(u.Hard) if u.Single != 0 { - newUlimits[name] = &container.Ulimit{ - Name: name, - Soft: int64(u.Single), - Hard: int64(u.Single), - } - } else { - newUlimits[name] = &container.Ulimit{ - Name: name, - Soft: int64(u.Soft), - Hard: int64(u.Hard), - } + soft, hard = int64(u.Single), int64(u.Single) } + + ulimits = append(ulimits, &container.Ulimit{ + Name: name, + Soft: soft, + Hard: hard, + }) } - ulimits := make([]*container.Ulimit, 0, len(newUlimits)) - for _, ulimit := range newUlimits { - ulimits = append(ulimits, ulimit) - } - sort.SliceStable(ulimits, func(i, j int) bool { - return ulimits[i].Name < ulimits[j].Name + + slices.SortFunc(ulimits, func(a, b *container.Ulimit) int { + return cmp.Compare(a.Name, b.Name) }) return ulimits } From 230f3a4e80a2f138d0ac3809844d6eb55ec328ec Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sun, 15 Feb 2026 17:49:43 +0100 Subject: [PATCH 2/6] cli/compose/convert: convertEndpointSpec: fix sorting of ports The existing code only sorted by PublishedPort (host port), and did not account for multiple ports mapped to the same host-port, but using a different protocol. Signed-off-by: Sebastiaan van Stijn --- cli/compose/convert/service.go | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/cli/compose/convert/service.go b/cli/compose/convert/service.go index 2a93d7e0fbb2..03f097223f6a 100644 --- a/cli/compose/convert/service.go +++ b/cli/compose/convert/service.go @@ -572,21 +572,42 @@ func convertResources(source composetypes.Resources) (*swarm.ResourceRequirement return resources, nil } +// compareSwarmPortConfig returns the lexical ordering of a and b, and can be used +// with [slices.SortFunc]. +// +// The comparison is performed in the following priority order: +// +// 1. PublishedPort (host port) +// 2. TargetPort (container port) +// 3. Protocol +// 4. PublishMode +// +// TODO(thaJeztah): define this on swarm.PortConfig itself to allow re-use. +func compareSwarmPortConfig(a, b swarm.PortConfig) int { + if n := cmp.Compare(a.PublishedPort, b.PublishedPort); n != 0 { + return n + } + if n := cmp.Compare(a.TargetPort, b.TargetPort); n != 0 { + return n + } + if n := cmp.Compare(a.Protocol, b.Protocol); n != 0 { + return n + } + return cmp.Compare(a.PublishMode, b.PublishMode) +} + func convertEndpointSpec(endpointMode string, source []composetypes.ServicePortConfig) *swarm.EndpointSpec { portConfigs := make([]swarm.PortConfig, 0, len(source)) for _, port := range source { - portConfig := swarm.PortConfig{ + portConfigs = append(portConfigs, swarm.PortConfig{ Protocol: network.IPProtocol(port.Protocol), TargetPort: port.Target, PublishedPort: port.Published, PublishMode: swarm.PortConfigPublishMode(port.Mode), - } - portConfigs = append(portConfigs, portConfig) + }) } - sort.Slice(portConfigs, func(i, j int) bool { - return portConfigs[i].PublishedPort < portConfigs[j].PublishedPort - }) + slices.SortFunc(portConfigs, compareSwarmPortConfig) return &swarm.EndpointSpec{ Mode: swarm.ResolutionMode(strings.ToLower(endpointMode)), From dbd5e20a198d29751c45f06e50af368ecac64f4f Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sun, 15 Feb 2026 18:06:16 +0100 Subject: [PATCH 3/6] cli/compose/loader: mergeServices: tidy up and modernize - construct merge-opts as a slice - remove intermediate var for overrideServices - use slices.SortFunc for sorting Signed-off-by: Sebastiaan van Stijn --- cli/compose/loader/merge.go | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/cli/compose/loader/merge.go b/cli/compose/loader/merge.go index 950f9f8c01c8..969ddf17daf5 100644 --- a/cli/compose/loader/merge.go +++ b/cli/compose/loader/merge.go @@ -4,8 +4,10 @@ package loader import ( + "cmp" "fmt" "reflect" + "slices" "sort" "dario.cat/mergo" @@ -52,10 +54,10 @@ func merge(configs []*types.Config) (*types.Config, error) { } func mergeServices(base, override []types.ServiceConfig) ([]types.ServiceConfig, error) { - baseServices := mapByName(base) - overrideServices := mapByName(override) - specials := &specials{ - m: map[reflect.Type]func(dst, src reflect.Value) error{ + mergeOpts := []func(*mergo.Config){ + mergo.WithAppendSlice, + mergo.WithOverride, + mergo.WithTransformers(&specials{m: map[reflect.Type]func(dst, src reflect.Value) error{ reflect.PointerTo(reflect.TypeFor[types.LoggingConfig]()): safelyMerge(mergeLoggingConfig), reflect.TypeFor[[]types.ServicePortConfig](): mergeSlice(toServicePortConfigsMap, toServicePortConfigsSlice), reflect.TypeFor[[]types.ServiceSecretConfig](): mergeSlice(toServiceSecretConfigsMap, toServiceSecretConfigsSlice), @@ -65,11 +67,13 @@ func mergeServices(base, override []types.ServiceConfig) ([]types.ServiceConfig, reflect.TypeFor[types.ShellCommand](): mergeShellCommand, reflect.PointerTo(reflect.TypeFor[types.ServiceNetworkConfig]()): mergeServiceNetworkConfig, reflect.PointerTo(reflect.TypeFor[uint64]()): mergeUint64, - }, + }}), } - for name, overrideService := range overrideServices { + + baseServices := mapByName(base) + for name, overrideService := range mapByName(override) { if baseService, ok := baseServices[name]; ok { - if err := mergo.Merge(&baseService, &overrideService, mergo.WithAppendSlice, mergo.WithOverride, mergo.WithTransformers(specials)); err != nil { + if err := mergo.Merge(&baseService, &overrideService, mergeOpts...); err != nil { return base, fmt.Errorf("cannot merge service %s: %w", name, err) } baseServices[name] = baseService @@ -77,11 +81,16 @@ func mergeServices(base, override []types.ServiceConfig) ([]types.ServiceConfig, } baseServices[name] = overrideService } + services := make([]types.ServiceConfig, 0, len(baseServices)) for _, baseService := range baseServices { services = append(services, baseService) } - sort.Slice(services, func(i, j int) bool { return services[i].Name < services[j].Name }) + + slices.SortFunc(services, func(a, b types.ServiceConfig) int { + return cmp.Compare(a.Name, b.Name) + }) + return services, nil } From 3b6c9f00969ae103b78b683863b72bf5b3b24231 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sun, 15 Feb 2026 18:14:47 +0100 Subject: [PATCH 4/6] cli/compose/loader: mergeServices: remove intermediate map for overrides The code was using an intermediate map, indexed by name, for both the "base" services _and_ for overrides. This meant that multiple files containing an override for a service would be ignored. Remove the intermediate map for overrides, and apply all overrides for a service instead. Signed-off-by: Sebastiaan van Stijn --- cli/compose/loader/merge.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cli/compose/loader/merge.go b/cli/compose/loader/merge.go index 969ddf17daf5..ea439755ec66 100644 --- a/cli/compose/loader/merge.go +++ b/cli/compose/loader/merge.go @@ -71,15 +71,15 @@ func mergeServices(base, override []types.ServiceConfig) ([]types.ServiceConfig, } baseServices := mapByName(base) - for name, overrideService := range mapByName(override) { - if baseService, ok := baseServices[name]; ok { + for _, overrideService := range override { + if baseService, ok := baseServices[overrideService.Name]; ok { if err := mergo.Merge(&baseService, &overrideService, mergeOpts...); err != nil { - return base, fmt.Errorf("cannot merge service %s: %w", name, err) + return base, fmt.Errorf("cannot merge service %s: %w", overrideService.Name, err) } - baseServices[name] = baseService + baseServices[overrideService.Name] = baseService continue } - baseServices[name] = overrideService + baseServices[overrideService.Name] = overrideService } services := make([]types.ServiceConfig, 0, len(baseServices)) From c74b4a3b393ae0829b24fa74230248db95eb57ed Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sun, 15 Feb 2026 18:26:34 +0100 Subject: [PATCH 5/6] cli/compose/loader: mergeServices: inline mapByName It's now only used once; let's inline it to remove some abstraction. Signed-off-by: Sebastiaan van Stijn --- cli/compose/loader/merge.go | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/cli/compose/loader/merge.go b/cli/compose/loader/merge.go index ea439755ec66..5bf711d0c244 100644 --- a/cli/compose/loader/merge.go +++ b/cli/compose/loader/merge.go @@ -70,7 +70,11 @@ func mergeServices(base, override []types.ServiceConfig) ([]types.ServiceConfig, }}), } - baseServices := mapByName(base) + baseServices := make(map[string]types.ServiceConfig, len(base)) + for _, s := range base { + baseServices[s.Name] = s + } + for _, overrideService := range override { if baseService, ok := baseServices[overrideService.Name]; ok { if err := mergo.Merge(&baseService, &overrideService, mergeOpts...); err != nil { @@ -283,14 +287,6 @@ func getLoggingDriver(v reflect.Value) string { return v.FieldByName("Driver").String() } -func mapByName(services []types.ServiceConfig) map[string]types.ServiceConfig { - m := map[string]types.ServiceConfig{} - for _, service := range services { - m[service.Name] = service - } - return m -} - func mergeVolumes(base, override map[string]types.VolumeConfig) (map[string]types.VolumeConfig, error) { err := mergo.Map(&base, &override, mergo.WithOverride) return base, err From cdc3038d683f8dc2c8d62bf36d10891ec4fdc154 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sun, 15 Feb 2026 18:27:49 +0100 Subject: [PATCH 6/6] cli/compose/loader: remove getLoggingDriver Inline it in mergeLoggingConfig and add some vars, which also makes it more readable. Signed-off-by: Sebastiaan van Stijn --- cli/compose/loader/merge.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/cli/compose/loader/merge.go b/cli/compose/loader/merge.go index 5bf711d0c244..28e42838b61b 100644 --- a/cli/compose/loader/merge.go +++ b/cli/compose/loader/merge.go @@ -230,11 +230,13 @@ func sliceToMap(tomap tomapFn, v reflect.Value) (map[any]any, error) { } func mergeLoggingConfig(dst, src reflect.Value) error { + dstDriver := dst.Elem().FieldByName("Driver").String() + srcDriver := src.Elem().FieldByName("Driver").String() + // Same driver, merging options - if getLoggingDriver(dst.Elem()) == getLoggingDriver(src.Elem()) || - getLoggingDriver(dst.Elem()) == "" || getLoggingDriver(src.Elem()) == "" { - if getLoggingDriver(dst.Elem()) == "" { - dst.Elem().FieldByName("Driver").SetString(getLoggingDriver(src.Elem())) + if dstDriver == srcDriver || dstDriver == "" || srcDriver == "" { + if dstDriver == "" { + dst.Elem().FieldByName("Driver").SetString(srcDriver) } dstOptions := dst.Elem().FieldByName("Options").Interface().(map[string]string) srcOptions := src.Elem().FieldByName("Options").Interface().(map[string]string) @@ -283,10 +285,6 @@ func mergeUint64(dst, src reflect.Value) error { return nil } -func getLoggingDriver(v reflect.Value) string { - return v.FieldByName("Driver").String() -} - func mergeVolumes(base, override map[string]types.VolumeConfig) (map[string]types.VolumeConfig, error) { err := mergo.Map(&base, &override, mergo.WithOverride) return base, err