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) 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/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/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/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/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/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/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/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/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 1ec5263debe8..4440f6165dce 100644 --- a/cli/command/service/formatter.go +++ b/cli/command/service/formatter.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 service import ( + "cmp" "errors" "fmt" - "sort" + "slices" "strconv" "strings" "time" @@ -615,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} @@ -775,17 +779,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/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/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/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/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/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/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/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/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 } 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 }, }) 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/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 } 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 } 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 } 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= 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..c743d49722ce 100644 --- a/cmd/docker-trust/trust/common.go +++ b/cmd/docker-trust/trust/common.go @@ -4,7 +4,7 @@ import ( "context" "encoding/hex" "fmt" - "sort" + "slices" "strings" "github.com/docker/cli/cli/command" @@ -110,9 +110,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 +119,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, ", ")) } @@ -164,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/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..4336f0872a24 100644 --- a/cmd/docker-trust/trust/inspect_pretty.go +++ b/cmd/docker-trust/trust/inspect_pretty.go @@ -1,10 +1,11 @@ package trust import ( + "cmp" "context" "fmt" "io" - "sort" + "slices" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/formatter" @@ -44,7 +45,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) @@ -80,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) } 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 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 + })) } 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 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: 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