From 0276bc1ba94ebd96e505465d8a287f1dddeb1fbc Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 1 Sep 2026 23:52:21 +0200 Subject: [PATCH 01/13] vendor: github.com/fvbommel/sortorder v1.2.0 full diff: https://github.com/fvbommel/sortorder/compare/v1.1.0...v1.2.0 Signed-off-by: Sebastiaan van Stijn --- vendor.mod | 2 +- vendor.sum | 4 +-- .../github.com/fvbommel/sortorder/natsort.go | 34 +++++++++++++++---- vendor/modules.txt | 4 +-- 4 files changed, 32 insertions(+), 12 deletions(-) diff --git a/vendor.mod b/vendor.mod index 8b1affe7932d..46be786ffc38 100644 --- a/vendor.mod +++ b/vendor.mod @@ -23,7 +23,7 @@ require ( github.com/docker/docker-credential-helpers v0.9.9 github.com/docker/go-connections v0.8.1 github.com/docker/go-units v0.5.0 - github.com/fvbommel/sortorder v1.1.0 + github.com/fvbommel/sortorder v1.2.0 github.com/go-jose/go-jose/v4 v4.1.4 github.com/go-viper/mapstructure/v2 v2.5.0 github.com/gogo/protobuf v1.3.2 diff --git a/vendor.sum b/vendor.sum index c1e8e5b293d2..cab284654f66 100644 --- a/vendor.sum +++ b/vendor.sum @@ -49,8 +49,8 @@ github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= -github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw= -github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= +github.com/fvbommel/sortorder v1.2.0 h1:TRIiRiGX+djh3Yf4FVxmWmAcYfIr5dH0NbzJWOSAWZk= +github.com/fvbommel/sortorder v1.2.0/go.mod h1:LbhO04ijZIeUuvz9B9BkI/qYrpZZEn1gWhxv4QjUKVs= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= diff --git a/vendor/github.com/fvbommel/sortorder/natsort.go b/vendor/github.com/fvbommel/sortorder/natsort.go index e4f15110b8eb..5eba2bc6f5df 100644 --- a/vendor/github.com/fvbommel/sortorder/natsort.go +++ b/vendor/github.com/fvbommel/sortorder/natsort.go @@ -1,5 +1,7 @@ package sortorder +import "cmp" + // Natural implements sort.Interface to sort strings in natural order. This // means that e.g. "abc2" < "abc12". // @@ -25,18 +27,36 @@ func isDigit(b byte) bool { return '0' <= b && b <= '9' } // // Limitation: only ASCII digits (0-9) are considered. func NaturalLess(str1, str2 string) bool { + return NaturalCompare(str1, str2) < 0 +} + +// NaturalCompare compares str1 and str2 using the same natural ordering as +// [NaturalLess]. It returns a negative value if str1 sorts before str2, a +// positive value if str1 sorts after str2, and zero if str1 and str2 are equal. +// +// NaturalCompare is suitable for APIs that accept a three-way comparison +// function, such as [slices.SortFunc] and [slices.SortStableFunc]. +func NaturalCompare(str1, str2 string) int { idx1, idx2 := 0, 0 for idx1 < len(str1) && idx2 < len(str2) { c1, c2 := str1[idx1], str2[idx2] dig1, dig2 := isDigit(c1), isDigit(c2) switch { - case dig1 != dig2: // Digits before other characters. - return dig1 // True if LHS is a digit, false if the RHS is one. + case dig1 != dig2: + // The first difference is that one is a digit and the other is not. + // That means that one (possibly empty) non-digit string has ended, and the other has not. + // The one that has ended is ordered before the one that continues, + // for example: "ab1" < "abc1" after skipping the matching "ab" prefix. + // This means the side with the digit is ordered before the other one. + if dig1 { + return -1 + } + return 1 case !dig1: // && !dig2, because dig1 == dig2 // UTF-8 compares bytewise-lexicographically, no need to decode // codepoints. if c1 != c2 { - return c1 < c2 + return cmp.Compare(c1, c2) } idx1++ idx2++ @@ -55,22 +75,22 @@ func NaturalLess(str1, str2 string) bool { // If lengths of numbers with non-zero prefix differ, the shorter // one is less. if len1, len2 := idx1-nonZero1, idx2-nonZero2; len1 != len2 { - return len1 < len2 + return cmp.Compare(len1, len2) } // If they're equally long, string comparison is correct. if nr1, nr2 := str1[nonZero1:idx1], str2[nonZero2:idx2]; nr1 != nr2 { - return nr1 < nr2 + return cmp.Compare(nr1, nr2) } // Otherwise, the one with less zeros is less. // Because everything up to the number is equal, comparing the index // after the zeros is sufficient. if nonZero1 != nonZero2 { - return nonZero1 < nonZero2 + return cmp.Compare(nonZero1, nonZero2) } } // They're identical so far, so continue comparing. } // So far they are identical. At least one is ended. If the other continues, // it sorts last. - return len(str1) < len(str2) + return cmp.Compare(len(str1), len(str2)) } diff --git a/vendor/modules.txt b/vendor/modules.txt index 1a2ff3894f69..39b5a29ce677 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -91,8 +91,8 @@ github.com/docker/go-units # github.com/felixge/httpsnoop v1.1.0 ## explicit; go 1.25 github.com/felixge/httpsnoop -# github.com/fvbommel/sortorder v1.1.0 -## explicit; go 1.13 +# github.com/fvbommel/sortorder v1.2.0 +## explicit; go 1.21 github.com/fvbommel/sortorder # github.com/go-jose/go-jose/v4 v4.1.4 ## explicit; go 1.24.0 From d37ac1bd99e7b15281edd9bd393f10ac929bce0e Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Wed, 2 Sep 2026 02:54:58 +0200 Subject: [PATCH 02/13] cmd/docker-trust: update github.com/fvbommel/sortorder v1.2.0 Signed-off-by: Sebastiaan van Stijn --- cmd/docker-trust/go.mod | 2 +- cmd/docker-trust/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/docker-trust/go.mod b/cmd/docker-trust/go.mod index e29e424805e8..10d14f7f84dd 100644 --- a/cmd/docker-trust/go.mod +++ b/cmd/docker-trust/go.mod @@ -9,7 +9,7 @@ require ( github.com/docker/cli-docs-tool v0.11.0 github.com/docker/distribution v2.8.3+incompatible github.com/docker/go-connections v0.7.0 - github.com/fvbommel/sortorder v1.1.0 + github.com/fvbommel/sortorder v1.2.0 github.com/moby/moby/api v1.54.2 github.com/moby/moby/client v0.4.1 github.com/opencontainers/go-digest v1.0.0 diff --git a/cmd/docker-trust/go.sum b/cmd/docker-trust/go.sum index cbf1eca9b8f8..e4ab52452ed1 100644 --- a/cmd/docker-trust/go.sum +++ b/cmd/docker-trust/go.sum @@ -73,8 +73,8 @@ github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw= -github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= +github.com/fvbommel/sortorder v1.2.0 h1:TRIiRiGX+djh3Yf4FVxmWmAcYfIr5dH0NbzJWOSAWZk= +github.com/fvbommel/sortorder v1.2.0/go.mod h1:LbhO04ijZIeUuvz9B9BkI/qYrpZZEn1gWhxv4QjUKVs= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= From 1580a46aed9cc58e33a467d4a4c59e4f46ffa4c6 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 1 Sep 2026 13:27:45 +0200 Subject: [PATCH 03/13] cmd/docker-trust: modernize with slices and maps packages Signed-off-by: Sebastiaan van Stijn --- cmd/docker-trust/internal/trust/trust_push.go | 5 +-- cmd/docker-trust/trust/common.go | 5 +-- cmd/docker-trust/trust/formatter.go | 15 +++---- cmd/docker-trust/trust/inspect.go | 41 ++++++++++--------- cmd/docker-trust/trust/inspect_pretty.go | 7 +++- cmd/docker-trust/trust/sign.go | 9 ++-- 6 files changed, 42 insertions(+), 40 deletions(-) diff --git a/cmd/docker-trust/internal/trust/trust_push.go b/cmd/docker-trust/internal/trust/trust_push.go index 64f255964cf0..c65085f2c81c 100644 --- a/cmd/docker-trust/internal/trust/trust_push.go +++ b/cmd/docker-trust/internal/trust/trust_push.go @@ -7,7 +7,7 @@ import ( "errors" "fmt" "io" - "sort" + "slices" "github.com/distribution/reference" "github.com/docker/cli/cli/streams" @@ -113,8 +113,7 @@ func PushTrustedReference(ctx context.Context, ioStreams Streams, repoInfo *Repo var rootKeyID string // always select the first root key if len(keys) > 0 { - sort.Strings(keys) - rootKeyID = keys[0] + rootKeyID = slices.Min(keys) } else { rootPublicKey, err := repo.GetCryptoService().Create(data.CanonicalRootRole, "", data.ECDSAKey) if err != nil { diff --git a/cmd/docker-trust/trust/common.go b/cmd/docker-trust/trust/common.go index f05e5772732d..f839bd26bd8c 100644 --- a/cmd/docker-trust/trust/common.go +++ b/cmd/docker-trust/trust/common.go @@ -4,6 +4,7 @@ import ( "context" "encoding/hex" "fmt" + "slices" "sort" "strings" @@ -110,9 +111,6 @@ func lookupTrustInfo(ctx context.Context, cli command.Cli, remote string) ([]tru } func formatAdminRole(roleWithSigs client.RoleWithSignatures) string { - adminKeyList := roleWithSigs.KeyIDs - sort.Strings(adminKeyList) - var role string switch roleWithSigs.Name { case data.CanonicalTargetsRole: @@ -122,6 +120,7 @@ func formatAdminRole(roleWithSigs client.RoleWithSignatures) string { default: return "" } + adminKeyList := slices.Sorted(slices.Values(roleWithSigs.KeyIDs)) return fmt.Sprintf("%s:\t%s\n", role, strings.Join(adminKeyList, ", ")) } diff --git a/cmd/docker-trust/trust/formatter.go b/cmd/docker-trust/trust/formatter.go index 13e5712a2764..2bde36115f25 100644 --- a/cmd/docker-trust/trust/formatter.go +++ b/cmd/docker-trust/trust/formatter.go @@ -1,7 +1,7 @@ package trust import ( - "sort" + "slices" "strings" "github.com/docker/cli/cli/command/formatter" @@ -73,8 +73,7 @@ func (c *trustTagContext) Digest() string { // Signers returns the sorted list of entities who signed this tag func (c *trustTagContext) Signers() string { - sort.Strings(c.s.Signers) - return strings.Join(c.s.Signers, ", ") + return strings.Join(slices.Sorted(slices.Values(c.s.Signers)), ", ") } // signerInfoWrite writes the context. @@ -108,15 +107,13 @@ type signerInfoContext struct { // Keys returns the sorted list of keys associated with the signer func (c *signerInfoContext) Keys() string { - sort.Strings(c.s.Keys) - truncatedKeys := []string{} + keys := slices.Sorted(slices.Values(c.s.Keys)) if c.trunc { - for _, keyID := range c.s.Keys { - truncatedKeys = append(truncatedKeys, formatter.TruncateID(keyID)) + for i, keyID := range keys { + keys[i] = formatter.TruncateID(keyID) } - return strings.Join(truncatedKeys, ", ") } - return strings.Join(c.s.Keys, ", ") + return strings.Join(keys, ", ") } // Signer returns the name of the signer diff --git a/cmd/docker-trust/trust/inspect.go b/cmd/docker-trust/trust/inspect.go index fabb1d7a131e..74240b61ad52 100644 --- a/cmd/docker-trust/trust/inspect.go +++ b/cmd/docker-trust/trust/inspect.go @@ -4,10 +4,11 @@ package trust import ( + "cmp" "context" "encoding/json" "fmt" - "sort" + "slices" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" @@ -80,36 +81,38 @@ func getRepoTrustInfo(ctx context.Context, dockerCLI command.Cli, remote string) } } - signerList, adminList := []trustSigner{}, []trustSigner{} - - signerRoleToKeyIDs := getDelegationRoleToKeyMap(delegationRoles) - - for signerName, signerKeys := range signerRoleToKeyIDs { - signerKeyList := []trustKey{} + var signerList []trustSigner + for signerName, signerKeys := range getDelegationRoleToKeyMap(delegationRoles) { + signerKeyList := make([]trustKey, 0, len(signerKeys)) for _, keyID := range signerKeys { signerKeyList = append(signerKeyList, trustKey{ID: keyID}) } signerList = append(signerList, trustSigner{signerName, signerKeyList}) } - sort.Slice(signerList, func(i, j int) bool { return signerList[i].Name > signerList[j].Name }) + // Sort by name in descending order. + slices.SortFunc(signerList, func(a, b trustSigner) int { return cmp.Compare(b.Name, a.Name) }) + var adminList []trustSigner for _, adminRole := range adminRolesWithSigs { + var name string switch adminRole.Name { case data.CanonicalRootRole: - rootKeys := []trustKey{} - for _, keyID := range adminRole.KeyIDs { - rootKeys = append(rootKeys, trustKey{ID: keyID}) - } - adminList = append(adminList, trustSigner{"Root", rootKeys}) + name = "Root" case data.CanonicalTargetsRole: - targetKeys := []trustKey{} - for _, keyID := range adminRole.KeyIDs { - targetKeys = append(targetKeys, trustKey{ID: keyID}) - } - adminList = append(adminList, trustSigner{"Repository", targetKeys}) + name = "Repository" + default: + continue } + + keys := make([]trustKey, 0, len(adminRole.KeyIDs)) + for _, keyID := range adminRole.KeyIDs { + keys = append(keys, trustKey{ID: keyID}) + } + adminList = append(adminList, trustSigner{name, keys}) } - sort.Slice(adminList, func(i, j int) bool { return adminList[i].Name > adminList[j].Name }) + + // Sort by name in descending order. + slices.SortFunc(adminList, func(a, b trustSigner) int { return cmp.Compare(b.Name, a.Name) }) return json.Marshal(trustRepo{ Name: remote, diff --git a/cmd/docker-trust/trust/inspect_pretty.go b/cmd/docker-trust/trust/inspect_pretty.go index 3eac2cfbe827..68e717be71e9 100644 --- a/cmd/docker-trust/trust/inspect_pretty.go +++ b/cmd/docker-trust/trust/inspect_pretty.go @@ -1,9 +1,11 @@ package trust import ( + "cmp" "context" "fmt" "io" + "slices" "sort" "github.com/docker/cli/cli/command" @@ -44,7 +46,10 @@ func prettyPrintTrustInfo(ctx context.Context, dockerCLI command.Cli, remote str } func printSortedAdminKeys(out io.Writer, adminRoles []client.RoleWithSignatures) { - sort.Slice(adminRoles, func(i, j int) bool { return adminRoles[i].Name > adminRoles[j].Name }) + // Sort by name in descending order. + slices.SortFunc(adminRoles, func(a, b client.RoleWithSignatures) int { + return cmp.Compare(b.Name, a.Name) + }) for _, adminRole := range adminRoles { if formattedAdminRole := formatAdminRole(adminRole); formattedAdminRole != "" { _, _ = fmt.Fprintf(out, " %s", formattedAdminRole) diff --git a/cmd/docker-trust/trust/sign.go b/cmd/docker-trust/trust/sign.go index 6b4b933f7242..f965ffb86a37 100644 --- a/cmd/docker-trust/trust/sign.go +++ b/cmd/docker-trust/trust/sign.go @@ -6,7 +6,7 @@ import ( "fmt" "io" "path" - "sort" + "slices" "strings" "github.com/distribution/reference" @@ -189,8 +189,8 @@ func getExistingSignatureInfoForReleasedTag(notaryRepo notaryclient.Repository, } func prettyPrintExistingSignatureInfo(out io.Writer, existingSigInfo trustTagRow) { - sort.Strings(existingSigInfo.Signers) - joinedSigners := strings.Join(existingSigInfo.Signers, ", ") + signers := slices.Sorted(slices.Values(existingSigInfo.Signers)) + joinedSigners := strings.Join(signers, ", ") _, _ = fmt.Fprintf(out, "Existing signatures for tag %s digest %s from:\n%s\n", existingSigInfo.SignedTag, existingSigInfo.Digest, joinedSigners) } @@ -228,8 +228,7 @@ func getOrGenerateNotaryKey(notaryRepo notaryclient.Repository, role data.RoleNa var key data.PublicKey // always select the first key by ID if len(keys) > 0 { - sort.Strings(keys) - keyID := keys[0] + keyID := slices.Min(keys) privKey, _, err := notaryRepo.GetCryptoService().GetPrivateKey(keyID) if err != nil { return nil, err From d930e71e4b1ae93dddc036518de821512976e0ba Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 1 Sep 2026 13:28:34 +0200 Subject: [PATCH 04/13] cli/command: modernize with slices and maps packages Signed-off-by: Sebastiaan van Stijn --- cli/command/completion/functions_test.go | 7 ++- cli/command/container/create_test.go | 8 +-- cli/command/container/restart_test.go | 7 ++- cli/command/container/rm_test.go | 8 ++- cli/command/container/stop_test.go | 7 ++- cli/command/formatter/buildcache.go | 29 +++++----- cli/command/formatter/container.go | 32 +++++----- .../image/build/internal/git/gitutils.go | 6 +- cli/command/image/build_test.go | 4 +- cli/command/image/push_test.go | 8 ++- cli/command/service/formatter.go | 22 ++++--- cli/command/service/logs.go | 7 ++- cli/command/service/opts.go | 10 ++-- cli/command/service/update.go | 58 +++++++------------ cli/command/service/update_test.go | 6 +- cli/command/stack/loader.go | 6 +- cli/command/stack/remove.go | 16 ++--- cli/command/system/events.go | 17 +++--- cli/command/system/info.go | 6 +- cli/command/system/version.go | 13 ++--- cli/command/volume/create_test.go | 9 ++- 21 files changed, 140 insertions(+), 146 deletions(-) diff --git a/cli/command/completion/functions_test.go b/cli/command/completion/functions_test.go index bb2fa1b62ddb..87706005c970 100644 --- a/cli/command/completion/functions_test.go +++ b/cli/command/completion/functions_test.go @@ -3,9 +3,9 @@ package completion import ( "context" "errors" - "sort" "testing" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/moby/moby/api/types/container" "github.com/moby/moby/api/types/image" "github.com/moby/moby/api/types/network" @@ -177,9 +177,10 @@ func TestCompleteEnvVarNames(t *testing.T) { values, directives := EnvVarNames()(nil, nil, "") assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion") - sort.Strings(values) expected := []string{"ENV_A", "ENV_B"} - assert.Check(t, is.DeepEqual(values, expected)) + assert.Check(t, is.DeepEqual(values, expected, cmpopts.SortSlices(func(a, b string) bool { + return a < b + }))) } func TestCompleteFileNames(t *testing.T) { diff --git a/cli/command/container/create_test.go b/cli/command/container/create_test.go index 1c6ec9264975..9c0099904e3b 100644 --- a/cli/command/container/create_test.go +++ b/cli/command/container/create_test.go @@ -7,7 +7,6 @@ import ( "os" "path/filepath" "runtime" - "sort" "strings" "testing" @@ -15,6 +14,7 @@ import ( "github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/internal/test" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/moby/moby/api/types/container" "github.com/moby/moby/api/types/system" "github.com/moby/moby/client" @@ -284,12 +284,12 @@ func TestCreateContainerWithProxyConfig(t *testing.T) { "ALL_PROXY=allProxy", "all_proxy=allProxy", } - sort.Strings(expected) fakeCLI := test.NewFakeCli(&fakeClient{ createContainerFunc: func(options client.ContainerCreateOptions) (client.ContainerCreateResult, error) { - sort.Strings(options.Config.Env) - assert.DeepEqual(t, options.Config.Env, expected) + assert.DeepEqual(t, options.Config.Env, expected, cmpopts.SortSlices(func(a, b string) bool { + return a < b + })) return client.ContainerCreateResult{}, nil }, }) diff --git a/cli/command/container/restart_test.go b/cli/command/container/restart_test.go index 569571b7175a..475444686638 100644 --- a/cli/command/container/restart_test.go +++ b/cli/command/container/restart_test.go @@ -4,11 +4,11 @@ import ( "context" "errors" "io" - "sort" "sync" "testing" "github.com/docker/cli/internal/test" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" @@ -87,8 +87,9 @@ func TestRestart(t *testing.T) { } else { assert.Check(t, is.Nil(err)) } - sort.Strings(restarted) - assert.Check(t, is.DeepEqual(restarted, tc.restarted)) + assert.Check(t, is.DeepEqual(restarted, tc.restarted, cmpopts.SortSlices(func(a, b string) bool { + return a < b + }))) }) } } diff --git a/cli/command/container/rm_test.go b/cli/command/container/rm_test.go index 4ab14407bb35..5e31e9abbcd5 100644 --- a/cli/command/container/rm_test.go +++ b/cli/command/container/rm_test.go @@ -4,11 +4,11 @@ import ( "context" "errors" "io" - "sort" "sync" "testing" "github.com/docker/cli/internal/test" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/moby/moby/client" "gotest.tools/v3/assert" ) @@ -53,8 +53,10 @@ func TestRemoveForce(t *testing.T) { assert.NilError(t, err) } assert.Equal(t, cli.ErrBuffer().String(), "") - sort.Strings(removed) - assert.DeepEqual(t, removed, []string{"mycontainer", "nosuchcontainer"}) + expected := []string{"mycontainer", "nosuchcontainer"} + assert.DeepEqual(t, removed, expected, cmpopts.SortSlices(func(a, b string) bool { + return a < b + })) }) } } diff --git a/cli/command/container/stop_test.go b/cli/command/container/stop_test.go index ed7fc335e267..484e7b4f39f5 100644 --- a/cli/command/container/stop_test.go +++ b/cli/command/container/stop_test.go @@ -4,11 +4,11 @@ import ( "context" "errors" "io" - "sort" "sync" "testing" "github.com/docker/cli/internal/test" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/moby/moby/client" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" @@ -88,8 +88,9 @@ func TestStop(t *testing.T) { } else { assert.Check(t, is.Nil(err)) } - sort.Strings(stopped) - assert.Check(t, is.DeepEqual(stopped, tc.stopped)) + assert.Check(t, is.DeepEqual(stopped, tc.stopped, cmpopts.SortSlices(func(a, b string) bool { + return a < b + }))) }) } } diff --git a/cli/command/formatter/buildcache.go b/cli/command/formatter/buildcache.go index 3a3c349988ef..d63cf05d63a3 100644 --- a/cli/command/formatter/buildcache.go +++ b/cli/command/formatter/buildcache.go @@ -1,7 +1,10 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.26 + package formatter import ( - "sort" + "slices" "strconv" "strings" "time" @@ -52,20 +55,20 @@ shared: {{.Shared}} } func buildCacheSort(buildCache []build.CacheRecord) { - sort.Slice(buildCache, func(i, j int) bool { - lui, luj := buildCache[i].LastUsedAt, buildCache[j].LastUsedAt + slices.SortFunc(buildCache, func(a, b build.CacheRecord) int { switch { - case lui == nil && luj == nil: - return strings.Compare(buildCache[i].ID, buildCache[j].ID) < 0 - case lui == nil: - return true - case luj == nil: - return false - case lui.Equal(*luj): - return strings.Compare(buildCache[i].ID, buildCache[j].ID) < 0 - default: - return lui.Before(*luj) + case a.LastUsedAt == nil && b.LastUsedAt == nil: + return strings.Compare(a.ID, b.ID) + case a.LastUsedAt == nil: + return -1 + case b.LastUsedAt == nil: + return 1 + } + + if c := a.LastUsedAt.Compare(*b.LastUsedAt); c != 0 { + return c } + return strings.Compare(a.ID, b.ID) }) } diff --git a/cli/command/formatter/container.go b/cli/command/formatter/container.go index b65209c26c89..7b77a8a75375 100644 --- a/cli/command/formatter/container.go +++ b/cli/command/formatter/container.go @@ -4,9 +4,10 @@ package formatter import ( + "cmp" "fmt" "net" - "sort" + "slices" "strconv" "strings" "time" @@ -295,7 +296,7 @@ func (c *ContainerContext) Labels() string { for k, v := range c.c.Labels { joinLabels = append(joinLabels, k+"="+v) } - sort.Strings(joinLabels) + slices.Sort(joinLabels) return strings.Join(joinLabels, ",") } @@ -395,9 +396,7 @@ func DisplayablePorts(ports []container.PortSummary) string { var result []string var hostMappings []string var groupMapKeys []string - sort.Slice(ports, func(i, j int) bool { - return comparePorts(ports[i], ports[j]) - }) + slices.SortFunc(ports, comparePorts) for _, port := range ports { current := port.PrivatePort @@ -452,18 +451,13 @@ func formGroup(key string, start, last uint16) string { return group + "/" + groupType } -func comparePorts(i, j container.PortSummary) bool { - if i.PrivatePort != j.PrivatePort { - return i.PrivatePort < j.PrivatePort - } - - if i.IP != j.IP { - return i.IP.Less(j.IP) - } - - if i.PublicPort != j.PublicPort { - return i.PublicPort < j.PublicPort - } - - return i.Type < j.Type +// comparePorts compares ports by private port, IP address, public port, +// and protocol, in that order. +func comparePorts(a, b container.PortSummary) int { + return cmp.Or( + cmp.Compare(a.PrivatePort, b.PrivatePort), + a.IP.Compare(b.IP), + cmp.Compare(a.PublicPort, b.PublicPort), + cmp.Compare(a.Type, b.Type), + ) } diff --git a/cli/command/image/build/internal/git/gitutils.go b/cli/command/image/build/internal/git/gitutils.go index e9245cb098a6..82b7188324c2 100644 --- a/cli/command/image/build/internal/git/gitutils.go +++ b/cli/command/image/build/internal/git/gitutils.go @@ -1,3 +1,6 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.26 + package git import ( @@ -7,6 +10,7 @@ import ( "os" "os/exec" "path/filepath" + "slices" "strings" "github.com/moby/sys/symlink" @@ -202,7 +206,7 @@ func (repo gitRepo) checkout(root string) (string, error) { } func (repo gitRepo) gitWithinDir(dir string, args ...string) ([]byte, error) { - args = append([]string{"-c", "protocol.file.allow=never"}, args...) // Block sneaky repositories from using repos from the filesystem as submodules. + args = slices.Concat([]string{"-c", "protocol.file.allow=never"}, args) // Block sneaky repositories from using repos from the filesystem as submodules. cmd := exec.Command("git", args...) cmd.Dir = dir // Disable unsafe remote protocols. diff --git a/cli/command/image/build_test.go b/cli/command/image/build_test.go index 88d94d6cdec4..1d61d05bbe70 100644 --- a/cli/command/image/build_test.go +++ b/cli/command/image/build_test.go @@ -8,7 +8,7 @@ import ( "io" "os" "path/filepath" - "sort" + "slices" "testing" "github.com/docker/cli/cli/streams" @@ -211,6 +211,6 @@ func (f *fakeBuild) filenames(t *testing.T) []string { for _, header := range h { names = append(names, header.Name) } - sort.Strings(names) + slices.Sort(names) return names } diff --git a/cli/command/image/push_test.go b/cli/command/image/push_test.go index e0e0195e6a28..3dbca45ca9df 100644 --- a/cli/command/image/push_test.go +++ b/cli/command/image/push_test.go @@ -102,9 +102,11 @@ func TestRunPushRespectsNoColorForAuxNotes(t *testing.T) { SelectedManifest: ocispec.Descriptor{Digest: "sha256:2222222222222222222222222222222222222222222222222222222222222222"}, }) assert.NilError(t, err) - line := append([]byte(`{"aux":`), aux...) - line = append(line, '}', '\n') - return fakeStreamResult{ReadCloser: io.NopCloser(bytes.NewReader(line))}, nil + var buf bytes.Buffer + buf.WriteString(`{"aux":`) + buf.Write(aux) + buf.WriteString("}\n") + return fakeStreamResult{ReadCloser: io.NopCloser(&buf)}, nil }, }) cli.Out().SetIsTerminal(true) diff --git a/cli/command/service/formatter.go b/cli/command/service/formatter.go index 1ec5263debe8..5b77bbb1a5ee 100644 --- a/cli/command/service/formatter.go +++ b/cli/command/service/formatter.go @@ -1,8 +1,13 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.26 + package service import ( + "cmp" "errors" "fmt" + "slices" "sort" "strconv" "strings" @@ -775,17 +780,16 @@ func (c *serviceContext) Ports() string { return "" } - pr := portRange{} - ports := []string{} - - servicePorts := c.service.Endpoint.Ports - sort.Slice(servicePorts, func(i, j int) bool { - if servicePorts[i].Protocol == servicePorts[j].Protocol { - return servicePorts[i].PublishedPort < servicePorts[j].PublishedPort - } - return servicePorts[i].Protocol < servicePorts[j].Protocol + // Sort by protocol first, then by published port. + slices.SortFunc(c.service.Endpoint.Ports, func(a, b swarm.PortConfig) int { + return cmp.Or( + cmp.Compare(a.Protocol, b.Protocol), + cmp.Compare(a.PublishedPort, b.PublishedPort), + ) }) + var pr portRange + var ports []string for _, p := range c.service.Endpoint.Ports { if p.PublishMode == swarm.PortConfigPublishModeIngress { prIsRange := pr.tEnd != pr.tStart diff --git a/cli/command/service/logs.go b/cli/command/service/logs.go index 347442332546..79908b865758 100644 --- a/cli/command/service/logs.go +++ b/cli/command/service/logs.go @@ -1,3 +1,6 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.26 + package service import ( @@ -6,7 +9,7 @@ import ( "errors" "fmt" "io" - "sort" + "slices" "strconv" "strings" @@ -314,7 +317,7 @@ func (lw *logWriter) Write(buf []byte) (int, error) { d = append(d, k+"="+details[k]) } // then sort em - sort.Strings(d) + slices.Sort(d) // then join and append output = append(output, []byte(strings.Join(d, ","))...) output = append(output, ' ') diff --git a/cli/command/service/opts.go b/cli/command/service/opts.go index e04f392e46b2..e2758e64cd7d 100644 --- a/cli/command/service/opts.go +++ b/cli/command/service/opts.go @@ -4,12 +4,12 @@ package service import ( + "cmp" "context" "errors" "fmt" "net/netip" "slices" - "sort" "strconv" "strings" "time" @@ -737,15 +737,15 @@ func (options *serviceOptions) ToService(ctx context.Context, apiClient client.N } networks := convertNetworks(options.networks) - for i, net := range networks { - nwID, err := resolveNetworkID(ctx, apiClient, net.Target) + for i := range networks { + nwID, err := resolveNetworkID(ctx, apiClient, networks[i].Target) if err != nil { return service, err } networks[i].Target = nwID } - sort.Slice(networks, func(i, j int) bool { - return networks[i].Target < networks[j].Target + slices.SortFunc(networks, func(a, b swarm.NetworkAttachmentConfig) int { + return cmp.Compare(a.Target, b.Target) }) resources, err := options.resources.ToResourceRequirements(flags) diff --git a/cli/command/service/update.go b/cli/command/service/update.go index 28869c9a7477..2e7a0ee25fce 100644 --- a/cli/command/service/update.go +++ b/cli/command/service/update.go @@ -4,13 +4,13 @@ package service import ( + "cmp" "context" "errors" "fmt" "maps" "net/netip" "slices" - "sort" "strings" "time" @@ -637,7 +637,7 @@ func updatePlacementConstraints(flags *pflag.FlagSet, placement *swarm.Placement } } // Sort so that result is predictable. - sort.Strings(newConstraints) + slices.Sort(newConstraints) placement.Constraints = newConstraints } @@ -722,7 +722,7 @@ func updateSysCtls(flags *pflag.FlagSet, field *map[string]string) { } func updateUlimits(flags *pflag.FlagSet, ulimits []*container.Ulimit) []*container.Ulimit { - newUlimits := make(map[string]*container.Ulimit) + newUlimits := make(map[string]*container.Ulimit, len(ulimits)) for _, ulimit := range ulimits { newUlimits[ulimit.Name] = ulimit @@ -739,17 +739,9 @@ func updateUlimits(flags *pflag.FlagSet, ulimits []*container.Ulimit) []*contain newUlimits[ulimit.Name] = ulimit } } - if len(newUlimits) == 0 { - return nil - } - limits := make([]*container.Ulimit, 0, len(newUlimits)) - for _, ulimit := range newUlimits { - limits = append(limits, ulimit) - } - sort.SliceStable(limits, func(i, j int) bool { - return limits[i].Name < limits[j].Name + return slices.SortedFunc(maps.Values(newUlimits), func(a, b *container.Ulimit) int { + return cmp.Compare(a.Name, b.Name) }) - return limits } func updateEnvironment(flags *pflag.FlagSet, field *[]string) { @@ -955,14 +947,12 @@ func updateMounts(flags *pflag.FlagSet, mounts *[]mount.Mount) error { newMounts = append(newMounts, mnt) } } - sort.Slice(newMounts, func(i, j int) bool { - a, b := newMounts[i], newMounts[j] - - if a.Source == b.Source { - return a.Target < b.Target - } - - return a.Source < b.Source + // Sort mounts by source, then by target. + slices.SortFunc(newMounts, func(a, b mount.Mount) int { + return cmp.Or( + cmp.Compare(a.Source, b.Source), + cmp.Compare(a.Target, b.Target), + ) }) *mounts = newMounts return nil @@ -982,7 +972,7 @@ func updateGroups(flags *pflag.FlagSet, groups *[]string) error { } } // Sort so that result is predictable. - sort.Strings(newGroups) + slices.Sort(newGroups) *groups = newGroups return nil @@ -1041,7 +1031,7 @@ func updateDNSConfig(flags *pflag.FlagSet, config **swarm.DNSConfig) error { } } // Sort so that result is predictable. - sort.Strings(newConfig.Search) + slices.Sort(newConfig.Search) options := (*config).Options if flags.Changed(flagDNSOptionAdd) { @@ -1056,7 +1046,7 @@ func updateDNSConfig(flags *pflag.FlagSet, config **swarm.DNSConfig) error { } } // Sort so that result is predictable. - sort.Strings(newConfig.Options) + slices.Sort(newConfig.Options) *config = newConfig return nil @@ -1109,12 +1099,8 @@ portLoop: } } - // Sort the PortConfig to avoid unnecessary updates - sort.Slice(newPorts, func(i, j int) bool { - // We convert PortConfig into `port/protocol`, e.g., `80/tcp` - // In updatePorts we already filter out with map so there is duplicate entries - return portConfigToString(&newPorts[i]) < portConfigToString(&newPorts[j]) - }) + // Sort the PortConfig to avoid unnecessary updates. + slices.SortFunc(newPorts, swarm.PortConfig.Compare) *portConfig = newPorts return nil } @@ -1350,8 +1336,8 @@ func updateNetworks(ctx context.Context, apiClient client.NetworkAPIClient, flag } } - sort.Slice(newNetworks, func(i, j int) bool { - return newNetworks[i].Target < newNetworks[j].Target + slices.SortFunc(newNetworks, func(a, b swarm.NetworkAttachmentConfig) int { + return cmp.Compare(a.Target, b.Target) }) spec.TaskTemplate.Networks = newNetworks @@ -1525,10 +1511,6 @@ func capsList(caps map[string]bool) []string { if caps[opts.AllCapabilities] { return []string{opts.AllCapabilities} } - out := make([]string, 0, len(caps)) - for c := range caps { - out = append(out, c) - } - sort.Strings(out) - return out + + return slices.Sorted(maps.Keys(caps)) } diff --git a/cli/command/service/update_test.go b/cli/command/service/update_test.go index 38dd29cf9aa2..51ba0eaec24f 100644 --- a/cli/command/service/update_test.go +++ b/cli/command/service/update_test.go @@ -4,7 +4,7 @@ import ( "context" "fmt" "net/netip" - "sort" + "slices" "strconv" "testing" "time" @@ -141,7 +141,7 @@ func TestUpdateEnvironment(t *testing.T) { updateEnvironment(flags, &envs) assert.Assert(t, is.Len(envs, 2)) // Order has been removed in updateEnvironment (map) - sort.Strings(envs) + slices.Sort(envs) assert.Check(t, is.Equal("toadd=newenv", envs[0])) assert.Check(t, is.Equal("tokeep=value", envs[1])) } @@ -308,7 +308,7 @@ func TestUpdatePorts(t *testing.T) { assert.Assert(t, is.Len(portConfigs, 2)) // Do a sort to have the order (might have changed by map) targetPorts := []int{int(portConfigs[0].TargetPort), int(portConfigs[1].TargetPort)} - sort.Ints(targetPorts) + slices.Sort(targetPorts) assert.Check(t, is.Equal(555, targetPorts[0])) assert.Check(t, is.Equal(1000, targetPorts[1])) } diff --git a/cli/command/stack/loader.go b/cli/command/stack/loader.go index 49366e4d50cc..10579a08d89d 100644 --- a/cli/command/stack/loader.go +++ b/cli/command/stack/loader.go @@ -10,7 +10,7 @@ import ( "os" "path/filepath" "runtime" - "sort" + "slices" "strings" "github.com/distribution/reference" @@ -75,9 +75,9 @@ func getDictsFrom(configFiles []composetypes.ConfigFile) []map[string]any { func propertyWarnings(properties map[string]string) string { msgs := make([]string, 0, len(properties)) for name, description := range properties { - msgs = append(msgs, fmt.Sprintf("%s: %s", name, description)) + msgs = append(msgs, name+": "+description) } - sort.Strings(msgs) + slices.Sort(msgs) return strings.Join(msgs, "\n\n") } diff --git a/cli/command/stack/remove.go b/cli/command/stack/remove.go index 90ab2df29bbb..2f6d061b35b6 100644 --- a/cli/command/stack/remove.go +++ b/cli/command/stack/remove.go @@ -1,10 +1,14 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.26 + package stack import ( + "cmp" "context" "errors" "fmt" - "sort" + "slices" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" @@ -96,15 +100,11 @@ func runRemove(ctx context.Context, dockerCli command.Cli, opts removeOptions) e return errors.Join(errs...) } -func sortServiceByName(services []swarm.Service) func(i, j int) bool { - return func(i, j int) bool { - return services[i].Spec.Name < services[j].Spec.Name - } -} - func removeServices(ctx context.Context, dockerCLI command.Cli, services []swarm.Service) bool { + slices.SortFunc(services, func(a, b swarm.Service) int { + return cmp.Compare(a.Spec.Name, b.Spec.Name) + }) var hasError bool - sort.Slice(services, sortServiceByName(services)) for _, service := range services { _, _ = fmt.Fprintln(dockerCLI.Out(), "Removing service", service.Spec.Name) if _, err := dockerCLI.Client().ServiceRemove(ctx, service.ID, client.ServiceRemoveOptions{}); err != nil { diff --git a/cli/command/system/events.go b/cli/command/system/events.go index eb77c6c6b2f9..36264ef010d9 100644 --- a/cli/command/system/events.go +++ b/cli/command/system/events.go @@ -1,10 +1,14 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.26 + package system import ( "context" "fmt" "io" - "sort" + "maps" + "slices" "strings" "text/template" "time" @@ -133,18 +137,13 @@ func prettyPrintEvent(out io.Writer, event events.Message) error { _, _ = fmt.Fprintf(out, "%s %s %s", event.Type, event.Action, event.Actor.ID) if len(event.Actor.Attributes) > 0 { - keys := make([]string, 0, len(event.Actor.Attributes)) - for k := range event.Actor.Attributes { - keys = append(keys, k) - } - sort.Strings(keys) + keys := slices.Sorted(maps.Keys(event.Actor.Attributes)) attrs := make([]string, 0, len(keys)) for _, k := range keys { - v := event.Actor.Attributes[k] - attrs = append(attrs, k+"="+v) + attrs = append(attrs, k+"="+event.Actor.Attributes[k]) } _, _ = fmt.Fprintf(out, " (%s)", strings.Join(attrs, ", ")) } - _, _ = fmt.Fprint(out, "\n") + _, _ = fmt.Fprintln(out) return nil } diff --git a/cli/command/system/info.go b/cli/command/system/info.go index 80a21629ec6e..fcc533c4265d 100644 --- a/cli/command/system/info.go +++ b/cli/command/system/info.go @@ -8,7 +8,7 @@ import ( "errors" "fmt" "io" - "sort" + "slices" "strings" "github.com/docker/cli/cli" @@ -468,11 +468,11 @@ func printSwarmInfo(output io.Writer, info system.Info) { } fprintln(output, " Node Address:", info.Swarm.NodeAddr) if len(info.Swarm.RemoteManagers) > 0 { - managers := []string{} + managers := make([]string, 0, len(info.Swarm.RemoteManagers)) for _, entry := range info.Swarm.RemoteManagers { managers = append(managers, entry.Addr) } - sort.Strings(managers) + slices.Sort(managers) fprintln(output, " Manager Addresses:") for _, entry := range managers { fprintf(output, " %s\n", entry) diff --git a/cli/command/system/version.go b/cli/command/system/version.go index b385ef7181d6..41b7de87c9e7 100644 --- a/cli/command/system/version.go +++ b/cli/command/system/version.go @@ -1,11 +1,15 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.26 + package system import ( "context" "fmt" "io" + "maps" "runtime" - "sort" + "slices" "strconv" "text/template" "time" @@ -252,10 +256,5 @@ func newVersionTemplate(templateFormat string) (*template.Template, error) { } func getDetailsOrder(v system.ComponentVersion) []string { - out := make([]string, 0, len(v.Details)) - for k := range v.Details { - out = append(out, k) - } - sort.Strings(out) - return out + return slices.Sorted(maps.Keys(v.Details)) } diff --git a/cli/command/volume/create_test.go b/cli/command/volume/create_test.go index 7f8a2c387c6a..3f9e43cdc016 100644 --- a/cli/command/volume/create_test.go +++ b/cli/command/volume/create_test.go @@ -8,11 +8,11 @@ import ( "fmt" "io" "maps" - "sort" "strings" "testing" "github.com/docker/cli/internal/test" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/moby/moby/api/types/volume" "github.com/moby/moby/client" "gotest.tools/v3/assert" @@ -230,10 +230,9 @@ func TestVolumeCreateClusterOpts(t *testing.T) { cli := test.NewFakeCli(&fakeClient{ volumeCreateFunc: func(options client.VolumeCreateOptions) (client.VolumeCreateResult, error) { - sort.SliceStable(options.ClusterVolumeSpec.Secrets, func(i, j int) bool { - return options.ClusterVolumeSpec.Secrets[i].Key < options.ClusterVolumeSpec.Secrets[j].Key - }) - assert.DeepEqual(t, options, expectedOptions) + assert.Check(t, is.DeepEqual(options, expectedOptions, cmpopts.SortSlices(func(a, b volume.Secret) bool { + return a.Key < b.Key + }))) return client.VolumeCreateResult{}, nil }, }) From b5e8866d24fe9a3b89880b275768377171449a94 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 1 Sep 2026 13:28:52 +0200 Subject: [PATCH 05/13] cli/compose: modernize with slices and maps packages Signed-off-by: Sebastiaan van Stijn --- cli/compose/convert/service.go | 22 ++++++++++--------- cli/compose/loader/interpolate.go | 23 +++++++------------- cli/compose/loader/loader.go | 36 +++++++++---------------------- cli/compose/loader/loader_test.go | 16 ++++++-------- cli/compose/loader/merge.go | 17 ++++++++++----- 5 files changed, 48 insertions(+), 66 deletions(-) diff --git a/cli/compose/convert/service.go b/cli/compose/convert/service.go index 762bf144ffdf..d30597451a7d 100644 --- a/cli/compose/convert/service.go +++ b/cli/compose/convert/service.go @@ -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 } diff --git a/cli/compose/loader/interpolate.go b/cli/compose/loader/interpolate.go index 11c44113d451..ef84ea10a4bd 100644 --- a/cli/compose/loader/interpolate.go +++ b/cli/compose/loader/interpolate.go @@ -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) -} diff --git a/cli/compose/loader/loader.go b/cli/compose/loader/loader.go index 91d85cfd0a43..0a9cb03ae0a5 100644 --- a/cli/compose/loader/loader.go +++ b/cli/compose/loader/loader.go @@ -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 } diff --git a/cli/compose/loader/loader_test.go b/cli/compose/loader/loader_test.go index 1b97ffaf2d2b..41f70c7cb23c 100644 --- a/cli/compose/loader/loader_test.go +++ b/cli/compose/loader/loader_test.go @@ -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" diff --git a/cli/compose/loader/merge.go b/cli/compose/loader/merge.go index 4b397a2bd62e..cf666ab0b2cc 100644 --- a/cli/compose/loader/merge.go +++ b/cli/compose/loader/merge.go @@ -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 } From 2bbd2a67e33fcccfef55f7df6de157e3a5ac3c4a Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 1 Sep 2026 13:29:16 +0200 Subject: [PATCH 06/13] templates: modernize with slices and maps packages Signed-off-by: Sebastiaan van Stijn --- templates/templates.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/templates.go b/templates/templates.go index 24f29c179fc6..5443d70d7c77 100644 --- a/templates/templates.go +++ b/templates/templates.go @@ -8,7 +8,7 @@ import ( "encoding/json" "fmt" "reflect" - "sort" + "slices" "strings" "text/template" ) @@ -125,12 +125,12 @@ func joinElements(elems any, sep string) (string, error) { return b.String(), nil case reflect.Map: - var out []string + out := make([]string, 0, rv.Len()) for _, k := range rv.MapKeys() { out = append(out, fmt.Sprint(rv.MapIndex(k).Interface())) } // Not ideal, but trying to keep a consistent order - sort.Strings(out) + slices.Sort(out) return strings.Join(out, sep), nil default: From 9f82fc0cda898aec74f671ecccac210036b2a50e Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 1 Sep 2026 13:29:35 +0200 Subject: [PATCH 07/13] opts: modernize with slices and maps packages Signed-off-by: Sebastiaan van Stijn --- opts/capabilities.go | 9 ++++++--- opts/ulimit.go | 20 ++++++++++---------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/opts/capabilities.go b/opts/capabilities.go index 82d071853b67..96a0e7d0f4f9 100644 --- a/opts/capabilities.go +++ b/opts/capabilities.go @@ -1,7 +1,10 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.26 + package opts import ( - "sort" + "slices" "strings" ) @@ -82,8 +85,8 @@ func EffectiveCapAddCapDrop(add, drop []string) (capAdd, capDrop []string) { } } - sort.Strings(capAdd) - sort.Strings(capDrop) + slices.Sort(capAdd) + slices.Sort(capDrop) return capAdd, capDrop } diff --git a/opts/ulimit.go b/opts/ulimit.go index aa88bce71a24..ba4a82c9d865 100644 --- a/opts/ulimit.go +++ b/opts/ulimit.go @@ -1,8 +1,13 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.26 + package opts import ( "fmt" - "sort" + "maps" + "slices" + "strings" "github.com/docker/go-units" "github.com/moby/moby/api/types/container" @@ -41,20 +46,15 @@ func (o *UlimitOpt) String() string { for _, v := range *o.values { out = append(out, v.String()) } - sort.Strings(out) - return fmt.Sprintf("%v", out) + slices.Sort(out) + return fmt.Sprint(out) } // GetList returns a slice of pointers to Ulimits. Values are sorted by name. func (o *UlimitOpt) GetList() []*container.Ulimit { - ulimits := make([]*container.Ulimit, 0, len(*o.values)) - for _, v := range *o.values { - ulimits = append(ulimits, v) - } - sort.SliceStable(ulimits, func(i, j int) bool { - return ulimits[i].Name < ulimits[j].Name + return slices.SortedFunc(maps.Values(*o.values), func(a, b *container.Ulimit) int { + return strings.Compare(a.Name, b.Name) }) - return ulimits } // Type returns the option type From dd166f0fad06d51d403c0bebce876b1f9dcc7ceb Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 1 Sep 2026 13:30:07 +0200 Subject: [PATCH 08/13] e2e: modernize Signed-off-by: Sebastiaan van Stijn --- e2e/stack/deploy_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/e2e/stack/deploy_test.go b/e2e/stack/deploy_test.go index daf7ccc220a3..166236936e7a 100644 --- a/e2e/stack/deploy_test.go +++ b/e2e/stack/deploy_test.go @@ -1,10 +1,10 @@ package stack import ( - "sort" "strings" "testing" + "github.com/google/go-cmp/cmp/cmpopts" "gotest.tools/v3/assert" "gotest.tools/v3/golden" "gotest.tools/v3/icmd" @@ -21,7 +21,7 @@ func TestDeployWithNamedResources(t *testing.T) { result.Assert(t, icmd.Success) stdout := strings.Split(result.Stdout(), "\n") expected := strings.Split(string(golden.Get(t, "stack-deploy-with-names.golden")), "\n") - sort.Strings(stdout) - sort.Strings(expected) - assert.DeepEqual(t, stdout, expected) + assert.DeepEqual(t, stdout, expected, cmpopts.SortSlices(func(a, b string) bool { + return a < b + })) } From 40e0151bd0fdea60f0fe84e60c96e55dd6d1fc20 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 1 Sep 2026 21:37:43 +0200 Subject: [PATCH 09/13] cli-plugins: modernize with slices and maps packages Signed-off-by: Sebastiaan van Stijn --- cli-plugins/manager/cobra_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cli-plugins/manager/cobra_test.go b/cli-plugins/manager/cobra_test.go index 7089bdccb9cc..0de070fefddc 100644 --- a/cli-plugins/manager/cobra_test.go +++ b/cli-plugins/manager/cobra_test.go @@ -3,6 +3,7 @@ package manager import ( "os" "path/filepath" + "slices" "sync" "testing" @@ -45,7 +46,7 @@ func TestPluginStubCompletionRestoresOSArgs(t *testing.T) { t.Cleanup(func() { os.Args = savedArgs }) originalArgs := []string{"docker", "image", "ls"} - os.Args = append([]string(nil), originalArgs...) + os.Args = slices.Clone(originalArgs) _, directive := cmd.ValidArgsFunction(cmd, []string{"--all"}, "alp") assert.Equal(t, directive, cobra.ShellCompDirectiveError) From 98aa9e4de9e7617edc8dc14affc4945f041e7345 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 1 Sep 2026 21:39:01 +0200 Subject: [PATCH 10/13] cli/config: use filepath.IsLocal to validate config paths Use filepath.Rel and filepath.IsLocal instead of comparing path prefixes as strings when checking that a path stays within the config directory. Also avoid calling Dir multiple times when constructing and validating the path. Signed-off-by: Sebastiaan van Stijn --- cli/config/config.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cli/config/config.go b/cli/config/config.go index 5a637805091c..a7ea0acdd7e6 100644 --- a/cli/config/config.go +++ b/cli/config/config.go @@ -7,7 +7,6 @@ import ( "os/user" "path/filepath" "runtime" - "strings" "sync" "github.com/docker/cli/cli/config/configfile" @@ -98,9 +97,11 @@ func SetDir(dir string) { // Path returns the path to a file relative to the config dir func Path(p ...string) (string, error) { - path := filepath.Join(append([]string{Dir()}, p...)...) - if !strings.HasPrefix(path, Dir()+string(filepath.Separator)) { - return "", fmt.Errorf("path %q is outside of root config directory %q", path, Dir()) + root := Dir() + path := filepath.Join(append([]string{root}, p...)...) + + if rel, err := filepath.Rel(root, path); err != nil || !filepath.IsLocal(rel) { + return "", fmt.Errorf("path %q is outside of root config directory %q", path, root) } return path, nil } From 69eed38aea6f76882b5734bd9d8229ead8b302e1 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Wed, 2 Sep 2026 01:00:33 +0200 Subject: [PATCH 11/13] volume create: simplify cluster volume options Extract construction of the cluster volume spec from runCreate, and use struct literals for the individual cluster volume options. Also simplify topology parsing by reusing ConvertKVStringsToMap, use slices.SortFunc for deterministic secret ordering, and use max to clamp negative capacity values to zero. Signed-off-by: Sebastiaan van Stijn --- cli/command/volume/create.go | 140 +++++++++++++++-------------------- 1 file changed, 58 insertions(+), 82 deletions(-) diff --git a/cli/command/volume/create.go b/cli/command/volume/create.go index 5c2098e03519..80b09ed2097b 100644 --- a/cli/command/volume/create.go +++ b/cli/command/volume/create.go @@ -4,10 +4,11 @@ package volume import ( + "cmp" "context" "errors" "fmt" - "sort" + "slices" "strings" "github.com/docker/cli/cli" @@ -117,93 +118,68 @@ func hasClusterVolumeOptionSet(flags *pflag.FlagSet) bool { } func runCreate(ctx context.Context, dockerCli command.Cli, options createOptions) error { - volOpts := client.VolumeCreateOptions{ - Driver: options.driver, - DriverOpts: options.driverOpts.GetAll(), - Name: options.name, - Labels: opts.ConvertKVStringsToMap(options.labels.GetSlice()), + res, err := dockerCli.Client().VolumeCreate(ctx, client.VolumeCreateOptions{ + Driver: options.driver, + DriverOpts: options.driverOpts.GetAll(), + Name: options.name, + Labels: opts.ConvertKVStringsToMap(options.labels.GetSlice()), + ClusterVolumeSpec: clusterVolumeSpec(options), + }) + if err != nil { + return err } - if options.cluster { - volOpts.ClusterVolumeSpec = &volume.ClusterVolumeSpec{ - Group: options.group, - AccessMode: &volume.AccessMode{ - Scope: volume.Scope(options.scope), - Sharing: volume.SharingMode(options.sharing), - }, - Availability: volume.Availability(options.availability), - } - - switch options.accessType { - case "mount": - volOpts.ClusterVolumeSpec.AccessMode.MountVolume = &volume.TypeMount{} - case "block": - volOpts.ClusterVolumeSpec.AccessMode.BlockVolume = &volume.TypeBlock{} - } - - vcr := &volume.CapacityRange{} - if r := options.requiredBytes.Value(); r >= 0 { - vcr.RequiredBytes = r - } - - if l := options.limitBytes.Value(); l >= 0 { - vcr.LimitBytes = l - } - volOpts.ClusterVolumeSpec.CapacityRange = vcr - - for key, secret := range options.secrets.GetAll() { - volOpts.ClusterVolumeSpec.Secrets = append( - volOpts.ClusterVolumeSpec.Secrets, - volume.Secret{ - Key: key, - Secret: secret, - }, - ) - } - sort.SliceStable(volOpts.ClusterVolumeSpec.Secrets, func(i, j int) bool { - return volOpts.ClusterVolumeSpec.Secrets[i].Key < volOpts.ClusterVolumeSpec.Secrets[j].Key - }) - // TODO(dperny): ignore if no topology specified - topology := &volume.TopologyRequirement{} - for _, top := range options.requisiteTopology.GetSlice() { - // each topology takes the form segment=value,segment=value - // comma-separated list of equal separated maps - segments := map[string]string{} - for segment := range strings.SplitSeq(top, ",") { - // TODO(dperny): validate topology syntax - k, v, _ := strings.Cut(segment, "=") - segments[k] = v - } - topology.Requisite = append( - topology.Requisite, - volume.Topology{Segments: segments}, - ) - } - - for _, top := range options.preferredTopology.GetSlice() { - // each topology takes the form segment=value,segment=value - // comma-separated list of equal separated maps - segments := map[string]string{} - for segment := range strings.SplitSeq(top, ",") { - // TODO(dperny): validate topology syntax - k, v, _ := strings.Cut(segment, "=") - segments[k] = v - } + _, _ = fmt.Fprintln(dockerCli.Out(), res.Volume.Name) + return nil +} - topology.Preferred = append( - topology.Preferred, - volume.Topology{Segments: segments}, - ) - } +func clusterVolumeSpec(options createOptions) *volume.ClusterVolumeSpec { + if !options.cluster { + return nil + } - volOpts.ClusterVolumeSpec.AccessibilityRequirements = topology + var secrets []volume.Secret + for key, secret := range options.secrets.GetAll() { + secrets = append(secrets, volume.Secret{Key: key, Secret: secret}) } + slices.SortFunc(secrets, func(a, b volume.Secret) int { + return cmp.Compare(a.Key, b.Key) + }) - res, err := dockerCli.Client().VolumeCreate(ctx, volOpts) - if err != nil { - return err + accessMode := &volume.AccessMode{ + Scope: volume.Scope(options.scope), + Sharing: volume.SharingMode(options.sharing), + } + switch options.accessType { + case "mount": + accessMode.MountVolume = &volume.TypeMount{} + case "block": + accessMode.BlockVolume = &volume.TypeBlock{} } - _, _ = fmt.Fprintln(dockerCli.Out(), res.Volume.Name) - return nil + return &volume.ClusterVolumeSpec{ + Group: options.group, + AccessMode: accessMode, + AccessibilityRequirements: &volume.TopologyRequirement{ + Requisite: parseTopologies(options.requisiteTopology.GetSlice()), + Preferred: parseTopologies(options.preferredTopology.GetSlice()), + }, + CapacityRange: &volume.CapacityRange{ + RequiredBytes: max(options.requiredBytes.Value(), 0), + LimitBytes: max(options.limitBytes.Value(), 0), + }, + Secrets: secrets, + Availability: volume.Availability(options.availability), + } +} + +func parseTopologies(values []string) []volume.Topology { + topologies := make([]volume.Topology, 0, len(values)) + for _, top := range values { + // TODO(dperny): validate topology syntax + topologies = append(topologies, volume.Topology{ + Segments: opts.ConvertKVStringsToMap(strings.Split(top, ",")), + }) + } + return topologies } From 59b31d37b51711512383b304516123d771d80ddd Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Wed, 2 Sep 2026 02:24:46 +0200 Subject: [PATCH 12/13] update naturalsort sorting with slices Signed-off-by: Sebastiaan van Stijn --- cli-plugins/manager/manager.go | 9 +++++--- cli/cobra.go | 9 +++++--- cli/command/config/ls.go | 10 ++++++--- cli/command/container/port.go | 9 ++++---- cli/command/context/list.go | 6 +++--- cli/command/network/list.go | 10 ++++++--- cli/command/node/list.go | 10 ++++++--- cli/command/plugin/list.go | 10 ++++++--- cli/command/secret/ls.go | 10 ++++++--- cli/command/service/formatter.go | 5 ++--- cli/command/stack/list.go | 9 +++++--- cli/command/stack/services.go | 10 ++++++--- cli/command/system/prune.go | 6 ++---- cli/command/task/print.go | 33 +++++++++++------------------- cli/command/volume/list.go | 10 ++++++--- cli/context/store/metadatastore.go | 6 +++--- 16 files changed, 94 insertions(+), 68 deletions(-) diff --git a/cli-plugins/manager/manager.go b/cli-plugins/manager/manager.go index bdbed4e023d2..bf398533aa22 100644 --- a/cli-plugins/manager/manager.go +++ b/cli-plugins/manager/manager.go @@ -1,3 +1,6 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.26 + package manager import ( @@ -6,7 +9,7 @@ import ( "os" "os/exec" "path/filepath" - "sort" + slices "slices" "strings" "sync" @@ -164,8 +167,8 @@ func ListPlugins(dockerCli config.Provider, rootcmd *cobra.Command) ([]Plugin, e return nil, err } - sort.Slice(plugins, func(i, j int) bool { - return sortorder.NaturalLess(plugins[i].Name, plugins[j].Name) + slices.SortFunc(plugins, func(a, b Plugin) int { + return sortorder.NaturalCompare(a.Name, b.Name) }) return plugins, nil diff --git a/cli/cobra.go b/cli/cobra.go index 4ec721f88772..d4ad6652a95a 100644 --- a/cli/cobra.go +++ b/cli/cobra.go @@ -1,9 +1,12 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.26 + package cli import ( "fmt" "os" - "sort" + "slices" "strings" "github.com/docker/cli/cli-plugins/metadata" @@ -273,8 +276,8 @@ func topCommands(cmd *cobra.Command) []*cobra.Command { cmds = append(cmds, sub) } } - sort.SliceStable(cmds, func(i, j int) bool { - return sortorder.NaturalLess(cmds[i].Annotations["category-top"], cmds[j].Annotations["category-top"]) + slices.SortFunc(cmds, func(a, b *cobra.Command) int { + return sortorder.NaturalCompare(a.Annotations["category-top"], b.Annotations["category-top"]) }) return cmds } diff --git a/cli/command/config/ls.go b/cli/command/config/ls.go index cbed96200ee9..94541fcb575d 100644 --- a/cli/command/config/ls.go +++ b/cli/command/config/ls.go @@ -1,8 +1,11 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.26 + package config import ( "context" - "sort" + "slices" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" @@ -10,6 +13,7 @@ import ( flagsHelper "github.com/docker/cli/cli/flags" "github.com/docker/cli/opts" "github.com/fvbommel/sortorder" + "github.com/moby/moby/api/types/swarm" "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -62,8 +66,8 @@ func runList(ctx context.Context, dockerCLI command.Cli, options listOptions) er } } - sort.Slice(res.Items, func(i, j int) bool { - return sortorder.NaturalLess(res.Items[i].Spec.Name, res.Items[j].Spec.Name) + slices.SortFunc(res.Items, func(a, b swarm.Config) int { + return sortorder.NaturalCompare(a.Spec.Name, b.Spec.Name) }) configCtx := formatter.Context{ diff --git a/cli/command/container/port.go b/cli/command/container/port.go index 534ddd1d34c0..839e40ca2f96 100644 --- a/cli/command/container/port.go +++ b/cli/command/container/port.go @@ -1,10 +1,13 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.26 + package container import ( "context" "fmt" "net" - "sort" + "slices" "strings" "github.com/docker/cli/cli" @@ -80,9 +83,7 @@ func runPort(ctx context.Context, dockerCli command.Cli, opts *portOptions) erro } if len(out) > 0 { - sort.Slice(out, func(i, j int) bool { - return sortorder.NaturalLess(out[i], out[j]) - }) + slices.SortFunc(out, sortorder.NaturalCompare) _, _ = fmt.Fprintln(dockerCli.Out(), strings.Join(out, "\n")) } diff --git a/cli/command/context/list.go b/cli/command/context/list.go index 67e9b9c66011..3507ae9c2205 100644 --- a/cli/command/context/list.go +++ b/cli/command/context/list.go @@ -6,7 +6,7 @@ package context import ( "fmt" "os" - "sort" + "slices" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" @@ -101,8 +101,8 @@ func runList(dockerCli command.Cli, opts *listOptions) error { Error: errMsg, }) } - sort.Slice(contexts, func(i, j int) bool { - return sortorder.NaturalLess(contexts[i].Name, contexts[j].Name) + slices.SortFunc(contexts, func(a, b *formatter.ClientContext) int { + return sortorder.NaturalCompare(a.Name, b.Name) }) if err := format(dockerCli, opts, contexts); err != nil { return err diff --git a/cli/command/network/list.go b/cli/command/network/list.go index 70d94ec78655..9f2c90f4598a 100644 --- a/cli/command/network/list.go +++ b/cli/command/network/list.go @@ -1,8 +1,11 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.26 + package network import ( "context" - "sort" + "slices" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" @@ -10,6 +13,7 @@ import ( flagsHelper "github.com/docker/cli/cli/flags" "github.com/docker/cli/opts" "github.com/fvbommel/sortorder" + "github.com/moby/moby/api/types/network" "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -61,8 +65,8 @@ func runList(ctx context.Context, dockerCLI command.Cli, options listOptions) er } } - sort.Slice(res.Items, func(i, j int) bool { - return sortorder.NaturalLess(res.Items[i].Name, res.Items[j].Name) + slices.SortFunc(res.Items, func(a, b network.Summary) int { + return sortorder.NaturalCompare(a.Name, b.Name) }) networksCtx := formatter.Context{ diff --git a/cli/command/node/list.go b/cli/command/node/list.go index 526ec655878f..48e5641f2999 100644 --- a/cli/command/node/list.go +++ b/cli/command/node/list.go @@ -1,8 +1,11 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.26 + package node import ( "context" - "sort" + "slices" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" @@ -10,6 +13,7 @@ import ( flagsHelper "github.com/docker/cli/cli/flags" "github.com/docker/cli/opts" "github.com/fvbommel/sortorder" + "github.com/moby/moby/api/types/swarm" "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -73,8 +77,8 @@ func runList(ctx context.Context, dockerCLI command.Cli, options listOptions) er Output: dockerCLI.Out(), Format: newFormat(format, options.quiet), } - sort.Slice(res.Items, func(i, j int) bool { - return sortorder.NaturalLess(res.Items[i].Description.Hostname, res.Items[j].Description.Hostname) + slices.SortFunc(res.Items, func(a, b swarm.Node) int { + return sortorder.NaturalCompare(a.Description.Hostname, b.Description.Hostname) }) return formatWrite(nodesCtx, res, info) } diff --git a/cli/command/plugin/list.go b/cli/command/plugin/list.go index fb651ab25abe..3606ac5bbfb4 100644 --- a/cli/command/plugin/list.go +++ b/cli/command/plugin/list.go @@ -1,8 +1,11 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.26 + package plugin import ( "context" - "sort" + "slices" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" @@ -10,6 +13,7 @@ import ( flagsHelper "github.com/docker/cli/cli/flags" "github.com/docker/cli/opts" "github.com/fvbommel/sortorder" + "github.com/moby/moby/api/types/plugin" "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -54,8 +58,8 @@ func runList(ctx context.Context, dockerCli command.Cli, options listOptions) er return err } - sort.Slice(resp.Items, func(i, j int) bool { - return sortorder.NaturalLess(resp.Items[i].Name, resp.Items[j].Name) + slices.SortFunc(resp.Items, func(a, b plugin.Plugin) int { + return sortorder.NaturalCompare(a.Name, b.Name) }) format := options.format diff --git a/cli/command/secret/ls.go b/cli/command/secret/ls.go index d68b52165c25..999ea53e38b3 100644 --- a/cli/command/secret/ls.go +++ b/cli/command/secret/ls.go @@ -1,8 +1,11 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.26 + package secret import ( "context" - "sort" + "slices" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" @@ -10,6 +13,7 @@ import ( flagsHelper "github.com/docker/cli/cli/flags" "github.com/docker/cli/opts" "github.com/fvbommel/sortorder" + "github.com/moby/moby/api/types/swarm" "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -59,8 +63,8 @@ func runSecretList(ctx context.Context, dockerCLI command.Cli, options listOptio } } - sort.Slice(res.Items, func(i, j int) bool { - return sortorder.NaturalLess(res.Items[i].Spec.Name, res.Items[j].Spec.Name) + slices.SortFunc(res.Items, func(a, b swarm.Secret) int { + return sortorder.NaturalCompare(a.Spec.Name, b.Spec.Name) }) secretCtx := formatter.Context{ diff --git a/cli/command/service/formatter.go b/cli/command/service/formatter.go index 5b77bbb1a5ee..4440f6165dce 100644 --- a/cli/command/service/formatter.go +++ b/cli/command/service/formatter.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "slices" - "sort" "strconv" "strings" "time" @@ -620,8 +619,8 @@ func NewListFormat(source string, quiet bool) formatter.Format { // ListFormatWrite writes the context func ListFormatWrite(ctx formatter.Context, services client.ServiceListResult) error { render := func(format func(subContext formatter.SubContext) error) error { - sort.Slice(services.Items, func(i, j int) bool { - return sortorder.NaturalLess(services.Items[i].Spec.Name, services.Items[j].Spec.Name) + slices.SortFunc(services.Items, func(a, b swarm.Service) int { + return sortorder.NaturalCompare(a.Spec.Name, b.Spec.Name) }) for _, service := range services.Items { serviceCtx := &serviceContext{service: service} diff --git a/cli/command/stack/list.go b/cli/command/stack/list.go index 6461b0902076..8463bd551571 100644 --- a/cli/command/stack/list.go +++ b/cli/command/stack/list.go @@ -1,8 +1,11 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.26 + package stack import ( "context" - "sort" + "slices" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" @@ -52,8 +55,8 @@ func runList(ctx context.Context, dockerCLI command.Cli, opts listOptions) error Output: dockerCLI.Out(), Format: format, } - sort.Slice(stacks, func(i, j int) bool { - return sortorder.NaturalLess(stacks[i].Name, stacks[j].Name) + slices.SortFunc(stacks, func(a, b stackSummary) int { + return sortorder.NaturalCompare(a.Name, b.Name) }) return stackWrite(stackCtx, stacks) } diff --git a/cli/command/stack/services.go b/cli/command/stack/services.go index 26d252d98e93..49bf573da7c1 100644 --- a/cli/command/stack/services.go +++ b/cli/command/stack/services.go @@ -1,9 +1,12 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.26 + package stack import ( "context" "fmt" - "sort" + "slices" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" @@ -12,6 +15,7 @@ import ( flagsHelper "github.com/docker/cli/cli/flags" cliopts "github.com/docker/cli/opts" "github.com/fvbommel/sortorder" + "github.com/moby/moby/api/types/swarm" "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -68,8 +72,8 @@ func formatWrite(dockerCLI command.Cli, services client.ServiceListResult, opts _, _ = fmt.Fprintln(dockerCLI.Err(), "Nothing found in stack:", opts.namespace) return nil } - sort.Slice(services.Items, func(i, j int) bool { - return sortorder.NaturalLess(services.Items[i].Spec.Name, services.Items[j].Spec.Name) + slices.SortFunc(services.Items, func(a, b swarm.Service) int { + return sortorder.NaturalCompare(a.Spec.Name, b.Spec.Name) }) f := opts.format diff --git a/cli/command/system/prune.go b/cli/command/system/prune.go index 698b6d1bb365..33544bf5b52c 100644 --- a/cli/command/system/prune.go +++ b/cli/command/system/prune.go @@ -8,7 +8,7 @@ import ( "context" "errors" "fmt" - "sort" + "slices" "text/template" "github.com/containerd/errdefs" @@ -172,9 +172,7 @@ func dryRun(ctx context.Context, dockerCli command.Cli, options pruneOptions) (s filters = append(filters, name+"="+v) } } - sort.Slice(filters, func(i, j int) bool { - return sortorder.NaturalLess(filters[i], filters[j]) - }) + slices.SortFunc(filters, sortorder.NaturalCompare) } var buffer bytes.Buffer diff --git a/cli/command/task/print.go b/cli/command/task/print.go index ae27d42d0133..dbb0435ea1ef 100644 --- a/cli/command/task/print.go +++ b/cli/command/task/print.go @@ -1,9 +1,13 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.26 + package task import ( + "cmp" "context" "fmt" - "sort" + "slices" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/formatter" @@ -14,24 +18,6 @@ import ( "github.com/moby/moby/client" ) -type tasksSortable []swarm.Task - -func (t tasksSortable) Len() int { - return len(t) -} - -func (t tasksSortable) Swap(i, j int) { - t[i], t[j] = t[j], t[i] -} - -func (t tasksSortable) Less(i, j int) bool { - if t[i].Name != t[j].Name { - return sortorder.NaturalLess(t[i].Name, t[j].Name) - } - // Sort tasks for the same service and slot by most recent. - return t[j].Meta.CreatedAt.Before(t[i].CreatedAt) -} - // Print task information in a format. // Besides this, command `docker node ps ` // and `docker stack ps` will call this, too. @@ -43,8 +29,13 @@ func Print(ctx context.Context, dockerCli command.Cli, tasks client.TaskListResu // First sort tasks, so that all tasks (including previous ones) of the same // service and slot are together. This must be done first, to print "previous" - // tasks indented - sort.Stable(tasksSortable(tasks.Items)) + // tasks indented. + slices.SortStableFunc(tasks.Items, func(a, b swarm.Task) int { + return cmp.Or( + sortorder.NaturalCompare(a.Name, b.Name), + b.Meta.CreatedAt.Compare(a.Meta.CreatedAt), + ) + }) names := map[string]string{} nodes := map[string]string{} diff --git a/cli/command/volume/list.go b/cli/command/volume/list.go index 1e0df1f84b59..bab03f9f44f5 100644 --- a/cli/command/volume/list.go +++ b/cli/command/volume/list.go @@ -1,8 +1,11 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.26 + package volume import ( "context" - "sort" + "slices" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" @@ -10,6 +13,7 @@ import ( flagsHelper "github.com/docker/cli/cli/flags" "github.com/docker/cli/opts" "github.com/fvbommel/sortorder" + "github.com/moby/moby/api/types/volume" "github.com/moby/moby/client" "github.com/spf13/cobra" ) @@ -85,8 +89,8 @@ func runList(ctx context.Context, dockerCLI command.Cli, options listOptions) er } } - sort.Slice(res.Items, func(i, j int) bool { - return sortorder.NaturalLess(res.Items[i].Name, res.Items[j].Name) + slices.SortFunc(res.Items, func(a, b volume.Volume) int { + return sortorder.NaturalCompare(a.Name, b.Name) }) volumeCtx := formatter.Context{ diff --git a/cli/context/store/metadatastore.go b/cli/context/store/metadatastore.go index 8bada79fe51d..1751e820690b 100644 --- a/cli/context/store/metadatastore.go +++ b/cli/context/store/metadatastore.go @@ -10,7 +10,7 @@ import ( "os" "path/filepath" "reflect" - "sort" + "slices" "github.com/fvbommel/sortorder" "github.com/moby/sys/atomicwriter" @@ -122,8 +122,8 @@ func (s *metadataStore) list() ([]Metadata, error) { } res = append(res, c) } - sort.Slice(res, func(i, j int) bool { - return sortorder.NaturalLess(res[i].Name, res[j].Name) + slices.SortFunc(res, func(a, b Metadata) int { + return sortorder.NaturalCompare(a.Name, b.Name) }) return res, nil } From 904816af0ecca13a496067c6982f6afeb6f95e93 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Wed, 2 Sep 2026 02:54:58 +0200 Subject: [PATCH 13/13] cmd/docker-trust: update naturalsort sorting with slices Signed-off-by: Sebastiaan van Stijn --- cmd/docker-trust/trust/common.go | 5 ++--- cmd/docker-trust/trust/inspect_pretty.go | 7 +++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/cmd/docker-trust/trust/common.go b/cmd/docker-trust/trust/common.go index f839bd26bd8c..c743d49722ce 100644 --- a/cmd/docker-trust/trust/common.go +++ b/cmd/docker-trust/trust/common.go @@ -5,7 +5,6 @@ import ( "encoding/hex" "fmt" "slices" - "sort" "strings" "github.com/docker/cli/cli/command" @@ -163,8 +162,8 @@ func matchReleasedSignatures(allTargets []client.TargetSignedStruct) []trustTagR for targetKey, signers := range releasedTargetRows { signatureRows = append(signatureRows, trustTagRow{targetKey, signers}) } - sort.Slice(signatureRows, func(i, j int) bool { - return sortorder.NaturalLess(signatureRows[i].SignedTag, signatureRows[j].SignedTag) + slices.SortFunc(signatureRows, func(a, b trustTagRow) int { + return sortorder.NaturalCompare(a.SignedTag, b.SignedTag) }) return signatureRows } diff --git a/cmd/docker-trust/trust/inspect_pretty.go b/cmd/docker-trust/trust/inspect_pretty.go index 68e717be71e9..4336f0872a24 100644 --- a/cmd/docker-trust/trust/inspect_pretty.go +++ b/cmd/docker-trust/trust/inspect_pretty.go @@ -6,7 +6,6 @@ import ( "fmt" "io" "slices" - "sort" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/formatter" @@ -85,15 +84,15 @@ func printSignerInfo(out io.Writer, roleToKeyIDs map[string][]string) error { Format: defaultSignerInfoTableFormat, Trunc: true, } - formattedSignerInfo := []signerInfo{} + formattedSignerInfo := make([]signerInfo, 0, len(roleToKeyIDs)) for name, keyIDs := range roleToKeyIDs { formattedSignerInfo = append(formattedSignerInfo, signerInfo{ Name: name, Keys: keyIDs, }) } - sort.Slice(formattedSignerInfo, func(i, j int) bool { - return sortorder.NaturalLess(formattedSignerInfo[i].Name, formattedSignerInfo[j].Name) + slices.SortFunc(formattedSignerInfo, func(a, b signerInfo) int { + return sortorder.NaturalCompare(a.Name, b.Name) }) return signerInfoWrite(signerInfoCtx, formattedSignerInfo) }