cli/compose: modernize with slices and maps packages

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn
2026-09-02 02:50:27 +02:00
parent ed406be52e
commit 33d586d81f
5 changed files with 48 additions and 66 deletions
+12 -10
View File
@@ -11,7 +11,6 @@ import (
"net/netip"
"os"
"slices"
"sort"
"strings"
"time"
@@ -235,8 +234,8 @@ func convertServiceNetworks(
nets = append(nets, netAttachConfig)
}
sort.Slice(nets, func(i, j int) bool {
return nets[i].Target < nets[j].Target
slices.SortFunc(nets, func(a, b swarm.NetworkAttachmentConfig) int {
return cmp.Compare(a.Target, b.Target)
})
return nets, nil
}
@@ -276,8 +275,10 @@ func convertServiceSecrets(
return nil, err
}
// sort to ensure idempotence (don't restart services just because the entries are in different order)
sort.SliceStable(secrs, func(i, j int) bool { return secrs[i].SecretName < secrs[j].SecretName })
return secrs, err
slices.SortStableFunc(secrs, func(a, b *swarm.SecretReference) int {
return cmp.Compare(a.SecretName, b.SecretName)
})
return secrs, nil
}
// convertServiceConfigObjs takes an API client, a namespace, a ServiceConfig,
@@ -352,8 +353,10 @@ func convertServiceConfigObjs(
return nil, err
}
// sort to ensure idempotence (don't restart services just because the entries are in different order)
sort.SliceStable(confs, func(i, j int) bool { return confs[i].ConfigName < confs[j].ConfigName })
return confs, err
slices.SortStableFunc(confs, func(a, b *swarm.ConfigReference) int {
return cmp.Compare(a.ConfigName, b.ConfigName)
})
return confs, nil
}
type swarmReferenceTarget struct {
@@ -609,8 +612,7 @@ func convertEndpointSpec(endpointMode string, source []composetypes.ServicePortC
// convertEnvironment converts key/value mappings to a slice, and sorts
// the results.
func convertEnvironment(source map[string]*string) []string {
var output []string
output := make([]string, 0, len(source))
for name, value := range source {
switch value {
case nil:
@@ -619,7 +621,7 @@ func convertEnvironment(source map[string]*string) []string {
output = append(output, name+"="+*value)
}
}
sort.Strings(output)
slices.Sort(output)
return output
}
+8 -15
View File
@@ -5,6 +5,7 @@ package loader
import (
"fmt"
"slices"
"strconv"
"strings"
@@ -35,20 +36,16 @@ var interpolateTypeCastMapping = map[interp.Path]interp.Cast{
servicePath("tty"): toBoolean,
servicePath("volumes", interp.PathMatchList, "read_only"): toBoolean,
servicePath("volumes", interp.PathMatchList, "volume", "nocopy"): toBoolean,
iPath("networks", interp.PathMatchAll, "external"): toBoolean,
iPath("networks", interp.PathMatchAll, "internal"): toBoolean,
iPath("networks", interp.PathMatchAll, "attachable"): toBoolean,
iPath("volumes", interp.PathMatchAll, "external"): toBoolean,
iPath("secrets", interp.PathMatchAll, "external"): toBoolean,
iPath("configs", interp.PathMatchAll, "external"): toBoolean,
}
func iPath(parts ...string) interp.Path {
return interp.NewPath(parts...)
interp.NewPath("networks", interp.PathMatchAll, "external"): toBoolean,
interp.NewPath("networks", interp.PathMatchAll, "internal"): toBoolean,
interp.NewPath("networks", interp.PathMatchAll, "attachable"): toBoolean,
interp.NewPath("volumes", interp.PathMatchAll, "external"): toBoolean,
interp.NewPath("secrets", interp.PathMatchAll, "external"): toBoolean,
interp.NewPath("configs", interp.PathMatchAll, "external"): toBoolean,
}
func servicePath(parts ...string) interp.Path {
return iPath(append([]string{"services", interp.PathMatchAll}, parts...)...)
return interp.NewPath(slices.Concat([]string{"services", interp.PathMatchAll}, parts)...)
}
func toInt(value string) (any, error) {
@@ -70,7 +67,3 @@ func toBoolean(value string) (any, error) {
return nil, fmt.Errorf("invalid boolean: %s", value)
}
}
func interpolateConfig(configDict map[string]any, opts interp.Options) (map[string]any, error) {
return interp.Interpolate(configDict, opts)
}
+10 -26
View File
@@ -10,7 +10,7 @@ import (
"path"
"path/filepath"
"reflect"
"sort"
"slices"
"strconv"
"strings"
"time"
@@ -111,7 +111,7 @@ func Load(configDetails types.ConfigDetails, opt ...func(*Options)) (*types.Conf
}
if !options.SkipInterpolation {
configDict, err = interpolateConfig(configDict, *options.Interpolate)
configDict, err = interp.Interpolate(configDict, *options.Interpolate)
if err != nil {
return nil, err
}
@@ -231,16 +231,7 @@ func GetUnsupportedProperties(configDicts ...map[string]any) []string {
}
}
return sortedKeys(unsupported)
}
func sortedKeys(set map[string]bool) []string {
keys := make([]string, 0, len(set))
for key := range set {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
return slices.Sorted(maps.Keys(unsupported))
}
// GetDeprecatedProperties returns the list of any deprecated properties that
@@ -827,8 +818,7 @@ func transformListOrMapping(listOrMapping any, sep string, allowNil bool, allowS
result := make([]string, 0, len(value))
for _, entry := range value {
for i, allowSep := range allowSeps {
entry := fmt.Sprint(entry)
k, v, ok := strings.Cut(entry, allowSep)
k, v, ok := strings.Cut(fmt.Sprint(entry), allowSep)
if ok {
// Entry uses this allowed separator. Add it to the result, using
// sep as a separator.
@@ -885,7 +875,7 @@ var transformShellCommand TransformerFunc = func(value any) (any, error) {
var transformHealthCheckTest TransformerFunc = func(data any) (any, error) {
switch value := data.(type) {
case string:
return append([]string{"CMD-SHELL"}, value), nil
return []string{"CMD-SHELL", value}, nil
case []any:
return value, nil
default:
@@ -925,16 +915,10 @@ func toServicePortConfigs(value string) ([]any, error) {
return nil, err
}
// We need to sort the key of the ports to make sure it is consistent
keys := make([]string, 0, len(ports))
for port := range ports {
keys = append(keys, string(port))
}
sort.Strings(keys)
var portConfigs []any
for _, key := range keys {
for _, key := range slices.Sorted(maps.Keys(ports)) {
// Reuse ConvertPortToPortConfig so that it is consistent
port, err := network.ParsePort(key)
port, err := network.ParsePort(string(key))
if err != nil {
return nil, err
}
@@ -975,13 +959,13 @@ func toString(value any, allowNil bool) any {
}
func toStringList(value map[string]any, separator string, allowNil bool) []string {
output := []string{}
output := make([]string, 0, len(value))
for key, value := range value {
if value == nil && !allowNil {
continue
}
output = append(output, fmt.Sprintf("%s%s%s", key, separator, value))
output = append(output, fmt.Sprintf("%s%s%v", key, separator, value))
}
sort.Strings(output)
slices.Sort(output)
return output
}
+6 -10
View File
@@ -7,7 +7,6 @@ import (
"bytes"
"os"
"runtime"
"sort"
"testing"
"github.com/docker/cli/cli/compose/types"
@@ -236,7 +235,9 @@ func TestLoad(t *testing.T) {
actual, err := Load(buildConfigDetails(sampleDict, nil))
assert.NilError(t, err)
assert.Check(t, is.Equal(sampleConfig.Version, actual.Version))
assert.Check(t, is.DeepEqual(serviceSort(sampleConfig.Services), serviceSort(actual.Services)))
assert.Check(t, is.DeepEqual(sampleConfig.Services, actual.Services, cmpopts.SortSlices(func(a, b types.ServiceConfig) bool {
return a.Name < b.Name
})))
assert.Check(t, is.DeepEqual(sampleConfig.Networks, actual.Networks))
assert.Check(t, is.DeepEqual(sampleConfig.Volumes, actual.Volumes))
}
@@ -310,7 +311,9 @@ services:
func TestParseAndLoad(t *testing.T) {
actual, err := loadYAML(sampleYAML)
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(serviceSort(sampleConfig.Services), serviceSort(actual.Services)))
assert.Check(t, is.DeepEqual(sampleConfig.Services, actual.Services, cmpopts.SortSlices(func(a, b types.ServiceConfig) bool {
return a.Name < b.Name
})))
assert.Check(t, is.DeepEqual(sampleConfig.Networks, actual.Networks))
assert.Check(t, is.DeepEqual(sampleConfig.Volumes, actual.Volumes))
}
@@ -1191,13 +1194,6 @@ services:
assert.ErrorContains(t, err, "services.tmpfs.volumes.0.tmpfs.size: must be an integer")
}
func serviceSort(services []types.ServiceConfig) []types.ServiceConfig {
sort.Slice(services, func(i, j int) bool {
return services[i].Name < services[j].Name
})
return services
}
func TestLoadAttachableNetwork(t *testing.T) {
config, err := loadYAML(`
version: "3.2"
+12 -5
View File
@@ -9,7 +9,6 @@ import (
"fmt"
"reflect"
"slices"
"sort"
"dario.cat/mergo"
"github.com/docker/cli/cli/compose/types"
@@ -152,7 +151,9 @@ func toServiceSecretConfigsSlice(dst reflect.Value, m map[any]any) error {
for _, v := range m {
s = append(s, v.(types.ServiceSecretConfig))
}
sort.Slice(s, func(i, j int) bool { return s[i].Source < s[j].Source })
slices.SortFunc(s, func(a, b types.ServiceSecretConfig) int {
return cmp.Compare(a.Source, b.Source)
})
dst.Set(reflect.ValueOf(s))
return nil
}
@@ -162,7 +163,9 @@ func toSServiceConfigObjConfigsSlice(dst reflect.Value, m map[any]any) error {
for _, v := range m {
s = append(s, v.(types.ServiceConfigObjConfig))
}
sort.Slice(s, func(i, j int) bool { return s[i].Source < s[j].Source })
slices.SortFunc(s, func(a, b types.ServiceConfigObjConfig) int {
return cmp.Compare(a.Source, b.Source)
})
dst.Set(reflect.ValueOf(s))
return nil
}
@@ -172,7 +175,9 @@ func toServicePortConfigsSlice(dst reflect.Value, m map[any]any) error {
for _, v := range m {
s = append(s, v.(types.ServicePortConfig))
}
sort.Slice(s, func(i, j int) bool { return s[i].Published < s[j].Published })
slices.SortFunc(s, func(a, b types.ServicePortConfig) int {
return cmp.Compare(a.Published, b.Published)
})
dst.Set(reflect.ValueOf(s))
return nil
}
@@ -182,7 +187,9 @@ func toServiceVolumeConfigsSlice(dst reflect.Value, m map[any]any) error {
for _, v := range m {
s = append(s, v.(types.ServiceVolumeConfig))
}
sort.Slice(s, func(i, j int) bool { return s[i].Target < s[j].Target })
slices.SortFunc(s, func(a, b types.ServiceVolumeConfig) int {
return cmp.Compare(a.Target, b.Target)
})
dst.Set(reflect.ValueOf(s))
return nil
}