From db28780976b977996c5a8efd604bad24cbc05ee4 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 09cf89e82e519b15a98d224b2cb8a055e239a9e0 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 78458e11e12c3937fc8e295e4ee4327c926a19e4 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 6e1393089b1bb95a94f0a4eedf1ea389e6ac02a2 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 services per file. Service-names should be unique per-file, so using an intermediate map would not benefit us (we'd still have to loop over all of them to produce the map, and again to iterate over the map) 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 42a211162c8029a5c3d240ab2fc8d2709f460c6a 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 b35a2d0837aa1b78a37b08973272227adb1b143d 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