From e0f4bc699cbef4065df2ba8fa4a52d8b372b20f2 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 14 Jul 2025 19:51:30 +0200 Subject: [PATCH] cli/command/formatter: add TrunateID utility We were depending on pkg/stringid to truncate IDs for presentation. While traditionally, we used a fixed length for "truncated" IDs, this is not a strict requirement (any ID-prefix should work, but conflicts may happen on shorter IDs). This patch adds a local `TruncateID()` utility in the formatter package; it's currently using the same implementation and length as the `stringid.TruncateID` function, but may diverge in future. Signed-off-by: Sebastiaan van Stijn --- cli/command/container/formatter_stats.go | 3 +- cli/command/formatter/buildcache.go | 5 +-- cli/command/formatter/container.go | 5 +-- cli/command/formatter/container_test.go | 2 +- cli/command/formatter/displayutils.go | 19 +++++++++ cli/command/formatter/displayutils_test.go | 43 +++++++++++++++++++++ cli/command/formatter/image.go | 3 +- cli/command/formatter/image_test.go | 2 +- cli/command/image/formatter_history.go | 3 +- cli/command/image/formatter_history_test.go | 2 +- cli/command/image/tree.go | 4 +- cli/command/network/formatter.go | 3 +- cli/command/network/formatter_test.go | 2 +- cli/command/plugin/formatter.go | 3 +- cli/command/plugin/formatter_test.go | 2 +- cli/command/service/formatter.go | 3 +- cli/command/service/logs.go | 4 +- cli/command/service/progress/progress.go | 6 +-- cli/command/task/formatter.go | 3 +- cli/command/trust/formatter.go | 3 +- 20 files changed, 86 insertions(+), 34 deletions(-) diff --git a/cli/command/container/formatter_stats.go b/cli/command/container/formatter_stats.go index b7fdf4733..9816ca834 100644 --- a/cli/command/container/formatter_stats.go +++ b/cli/command/container/formatter_stats.go @@ -5,7 +5,6 @@ import ( "sync" "github.com/docker/cli/cli/command/formatter" - "github.com/docker/docker/pkg/stringid" units "github.com/docker/go-units" ) @@ -176,7 +175,7 @@ func (c *statsContext) Name() string { func (c *statsContext) ID() string { if c.trunc { - return stringid.TruncateID(c.s.ID) + return formatter.TruncateID(c.s.ID) } return c.s.ID } diff --git a/cli/command/formatter/buildcache.go b/cli/command/formatter/buildcache.go index a170e0eaf..ade5de73f 100644 --- a/cli/command/formatter/buildcache.go +++ b/cli/command/formatter/buildcache.go @@ -7,7 +7,6 @@ import ( "time" "github.com/docker/docker/api/types/build" - "github.com/docker/docker/pkg/stringid" "github.com/docker/go-units" ) @@ -115,7 +114,7 @@ func (c *buildCacheContext) MarshalJSON() ([]byte, error) { func (c *buildCacheContext) ID() string { id := c.v.ID if c.trunc { - id = stringid.TruncateID(c.v.ID) + id = TruncateID(c.v.ID) } if c.v.InUse { return id + "*" @@ -131,7 +130,7 @@ func (c *buildCacheContext) Parent() string { parent = c.v.Parent //nolint:staticcheck // Ignore SA1019: Field was deprecated in API v1.42, but kept for backward compatibility } if c.trunc { - return stringid.TruncateID(parent) + return TruncateID(parent) } return parent } diff --git a/cli/command/formatter/container.go b/cli/command/formatter/container.go index 4480221fb..0a5c587af 100644 --- a/cli/command/formatter/container.go +++ b/cli/command/formatter/container.go @@ -14,7 +14,6 @@ import ( "github.com/containerd/platforms" "github.com/distribution/reference" "github.com/docker/docker/api/types/container" - "github.com/docker/docker/pkg/stringid" "github.com/docker/go-units" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) @@ -135,7 +134,7 @@ func (c *ContainerContext) MarshalJSON() ([]byte, error) { // option being set, the full or truncated ID is returned. func (c *ContainerContext) ID() string { if c.trunc { - return stringid.TruncateID(c.c.ID) + return TruncateID(c.c.ID) } return c.c.ID } @@ -172,7 +171,7 @@ func (c *ContainerContext) Image() string { return "" } if c.trunc { - if trunc := stringid.TruncateID(c.c.ImageID); trunc == stringid.TruncateID(c.c.Image) { + if trunc := TruncateID(c.c.ImageID); trunc == TruncateID(c.c.Image) { return trunc } // truncate digest if no-trunc option was not selected diff --git a/cli/command/formatter/container_test.go b/cli/command/formatter/container_test.go index 87928926a..3380a2f7e 100644 --- a/cli/command/formatter/container_test.go +++ b/cli/command/formatter/container_test.go @@ -34,7 +34,7 @@ func TestContainerPsContext(t *testing.T) { { container: container.Summary{ID: containerID}, trunc: true, - expValue: stringid.TruncateID(containerID), + expValue: TruncateID(containerID), call: ctx.ID, }, { diff --git a/cli/command/formatter/displayutils.go b/cli/command/formatter/displayutils.go index c6d2845c4..b062c3391 100644 --- a/cli/command/formatter/displayutils.go +++ b/cli/command/formatter/displayutils.go @@ -27,6 +27,25 @@ func charWidth(r rune) int { } } +const shortLen = 12 + +// TruncateID returns a shorthand version of a string identifier for presentation, +// after trimming digest algorithm prefix (if any). +// +// This function is a copy of [stringid.TruncateID] for presentation / formatting +// purposes. +// +// [stringid.TruncateID]: https://github.com/moby/moby/blob/v28.3.2/pkg/stringid/stringid.go#L19 +func TruncateID(id string) string { + if i := strings.IndexRune(id, ':'); i >= 0 { + id = id[i+1:] + } + if len(id) > shortLen { + id = id[:shortLen] + } + return id +} + // Ellipsis truncates a string to fit within maxDisplayWidth, and appends ellipsis (…). // For maxDisplayWidth of 1 and lower, no ellipsis is appended. // For maxDisplayWidth of 1, first char of string will return even if its width > 1. diff --git a/cli/command/formatter/displayutils_test.go b/cli/command/formatter/displayutils_test.go index 1dd3e75cb..131f90db1 100644 --- a/cli/command/formatter/displayutils_test.go +++ b/cli/command/formatter/displayutils_test.go @@ -7,6 +7,49 @@ import ( is "gotest.tools/v3/assert/cmp" ) +func TestTruncateID(t *testing.T) { + tests := []struct { + doc, id, expected string + }{ + { + doc: "empty ID", + id: "", + expected: "", + }, + { + // IDs are expected to be 12 (short) or 64 characters, and not be numeric only, + // but TruncateID should handle these gracefully. + doc: "invalid ID", + id: "1234", + expected: "1234", + }, + { + doc: "full ID", + id: "90435eec5c4e124e741ef731e118be2fc799a68aba0466ec17717f24ce2ae6a2", + expected: "90435eec5c4e", + }, + { + doc: "digest", + id: "sha256:90435eec5c4e124e741ef731e118be2fc799a68aba0466ec17717f24ce2ae6a2", + expected: "90435eec5c4e", + }, + { + doc: "very long ID", + id: "90435eec5c4e124e741ef731e118be2fc799a68aba0466ec17717f24ce2ae6a290435eec5c4e124e741ef731e118be2fc799a68aba0466ec17717f24ce2ae6a2", + expected: "90435eec5c4e", + }, + } + + for _, tc := range tests { + t.Run(tc.doc, func(t *testing.T) { + actual := TruncateID(tc.id) + if actual != tc.expected { + t.Errorf("expected: %q, got: %q", tc.expected, actual) + } + }) + } +} + func TestEllipsis(t *testing.T) { testcases := []struct { source string diff --git a/cli/command/formatter/image.go b/cli/command/formatter/image.go index d16f42b5b..c1b3b7217 100644 --- a/cli/command/formatter/image.go +++ b/cli/command/formatter/image.go @@ -6,7 +6,6 @@ import ( "github.com/distribution/reference" "github.com/docker/docker/api/types/image" - "github.com/docker/docker/pkg/stringid" units "github.com/docker/go-units" ) @@ -216,7 +215,7 @@ func (c *imageContext) MarshalJSON() ([]byte, error) { func (c *imageContext) ID() string { if c.trunc { - return stringid.TruncateID(c.i.ID) + return TruncateID(c.i.ID) } return c.i.ID } diff --git a/cli/command/formatter/image_test.go b/cli/command/formatter/image_test.go index e180f4dba..6f1e9ce75 100644 --- a/cli/command/formatter/image_test.go +++ b/cli/command/formatter/image_test.go @@ -27,7 +27,7 @@ func TestImageContext(t *testing.T) { }{ { imageCtx: imageContext{i: image.Summary{ID: imageID}, trunc: true}, - expValue: stringid.TruncateID(imageID), + expValue: TruncateID(imageID), call: ctx.ID, }, { diff --git a/cli/command/image/formatter_history.go b/cli/command/image/formatter_history.go index 08b7febe3..c051129e4 100644 --- a/cli/command/image/formatter_history.go +++ b/cli/command/image/formatter_history.go @@ -7,7 +7,6 @@ import ( "github.com/docker/cli/cli/command/formatter" "github.com/docker/docker/api/types/image" - "github.com/docker/docker/pkg/stringid" units "github.com/docker/go-units" ) @@ -72,7 +71,7 @@ func (c *historyContext) MarshalJSON() ([]byte, error) { func (c *historyContext) ID() string { if c.trunc { - return stringid.TruncateID(c.h.ID) + return formatter.TruncateID(c.h.ID) } return c.h.ID } diff --git a/cli/command/image/formatter_history_test.go b/cli/command/image/formatter_history_test.go index e67fa946f..3a6eeb976 100644 --- a/cli/command/image/formatter_history_test.go +++ b/cli/command/image/formatter_history_test.go @@ -35,7 +35,7 @@ func TestHistoryContext_ID(t *testing.T) { historyContext{ h: image.HistoryResponseItem{ID: id}, trunc: true, - }, stringid.TruncateID(id), ctx.ID, + }, formatter.TruncateID(id), ctx.ID, }, } diff --git a/cli/command/image/tree.go b/cli/command/image/tree.go index f12db4161..c12c7096b 100644 --- a/cli/command/image/tree.go +++ b/cli/command/image/tree.go @@ -12,10 +12,10 @@ import ( "github.com/containerd/platforms" "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/internal/tui" "github.com/docker/docker/api/types/filters" imagetypes "github.com/docker/docker/api/types/image" - "github.com/docker/docker/pkg/stringid" "github.com/docker/go-units" "github.com/morikuni/aec" "github.com/opencontainers/go-digest" @@ -222,7 +222,7 @@ func printImageTree(dockerCLI command.Cli, view treeView) error { Align: alignLeft, Width: 12, DetailsValue: func(d *imageDetails) string { - return stringid.TruncateID(d.ID) + return formatter.TruncateID(d.ID) }, }, { diff --git a/cli/command/network/formatter.go b/cli/command/network/formatter.go index fb2177e7a..6a15a6be1 100644 --- a/cli/command/network/formatter.go +++ b/cli/command/network/formatter.go @@ -6,7 +6,6 @@ import ( "github.com/docker/cli/cli/command/formatter" "github.com/docker/docker/api/types/network" - "github.com/docker/docker/pkg/stringid" ) const ( @@ -73,7 +72,7 @@ func (c *networkContext) MarshalJSON() ([]byte, error) { func (c *networkContext) ID() string { if c.trunc { - return stringid.TruncateID(c.n.ID) + return formatter.TruncateID(c.n.ID) } return c.n.ID } diff --git a/cli/command/network/formatter_test.go b/cli/command/network/formatter_test.go index ac40c2ffd..2e02a4739 100644 --- a/cli/command/network/formatter_test.go +++ b/cli/command/network/formatter_test.go @@ -35,7 +35,7 @@ func TestNetworkContext(t *testing.T) { {networkContext{ n: network.Summary{ID: networkID}, trunc: true, - }, stringid.TruncateID(networkID), ctx.ID}, + }, formatter.TruncateID(networkID), ctx.ID}, {networkContext{ n: network.Summary{Name: "network_name"}, }, "network_name", ctx.Name}, diff --git a/cli/command/plugin/formatter.go b/cli/command/plugin/formatter.go index c5c0c9c4e..27c24b52c 100644 --- a/cli/command/plugin/formatter.go +++ b/cli/command/plugin/formatter.go @@ -5,7 +5,6 @@ import ( "github.com/docker/cli/cli/command/formatter" "github.com/docker/docker/api/types" - "github.com/docker/docker/pkg/stringid" ) const ( @@ -66,7 +65,7 @@ func (c *pluginContext) MarshalJSON() ([]byte, error) { func (c *pluginContext) ID() string { if c.trunc { - return stringid.TruncateID(c.p.ID) + return formatter.TruncateID(c.p.ID) } return c.p.ID } diff --git a/cli/command/plugin/formatter_test.go b/cli/command/plugin/formatter_test.go index 6b2f83c4b..7442c093d 100644 --- a/cli/command/plugin/formatter_test.go +++ b/cli/command/plugin/formatter_test.go @@ -33,7 +33,7 @@ func TestPluginContext(t *testing.T) { {pluginContext{ p: types.Plugin{ID: pluginID}, trunc: true, - }, stringid.TruncateID(pluginID), ctx.ID}, + }, formatter.TruncateID(pluginID), ctx.ID}, {pluginContext{ p: types.Plugin{Name: "plugin_name"}, }, "plugin_name", ctx.Name}, diff --git a/cli/command/service/formatter.go b/cli/command/service/formatter.go index 499217ec1..2dc5f0f8b 100644 --- a/cli/command/service/formatter.go +++ b/cli/command/service/formatter.go @@ -14,7 +14,6 @@ import ( mounttypes "github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/pkg/stringid" units "github.com/docker/go-units" "github.com/fvbommel/sortorder" "github.com/pkg/errors" @@ -645,7 +644,7 @@ func (c *serviceContext) MarshalJSON() ([]byte, error) { } func (c *serviceContext) ID() string { - return stringid.TruncateID(c.service.ID) + return formatter.TruncateID(c.service.ID) } func (c *serviceContext) Name() string { diff --git a/cli/command/service/logs.go b/cli/command/service/logs.go index 7d394267b..7c651d859 100644 --- a/cli/command/service/logs.go +++ b/cli/command/service/logs.go @@ -13,13 +13,13 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" + "github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/idresolver" "github.com/docker/cli/internal/logdetails" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/client" "github.com/docker/docker/pkg/stdcopy" - "github.com/docker/docker/pkg/stringid" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -220,7 +220,7 @@ func (f *taskFormatter) format(ctx context.Context, logCtx logContext) (string, if f.opts.noTrunc { taskName += "." + task.ID } else { - taskName += "." + stringid.TruncateID(task.ID) + taskName += "." + formatter.TruncateID(task.ID) } } diff --git a/cli/command/service/progress/progress.go b/cli/command/service/progress/progress.go index c5c00e41c..5f87e2912 100644 --- a/cli/command/service/progress/progress.go +++ b/cli/command/service/progress/progress.go @@ -11,12 +11,12 @@ import ( "strings" "time" + "github.com/docker/cli/cli/command/formatter" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/client" "github.com/docker/docker/pkg/progress" "github.com/docker/docker/pkg/streamformatter" - "github.com/docker/docker/pkg/stringid" ) var ( @@ -505,7 +505,7 @@ func (u *globalProgressUpdater) writeTaskProgress(task swarm.Task, nodeCount int if task.Status.Err != "" { u.progressOut.WriteProgress(progress.Progress{ - ID: stringid.TruncateID(task.NodeID), + ID: formatter.TruncateID(task.NodeID), Action: truncError(task.Status.Err), }) return @@ -513,7 +513,7 @@ func (u *globalProgressUpdater) writeTaskProgress(task swarm.Task, nodeCount int if !terminalState(task.DesiredState) && !terminalState(task.Status.State) { u.progressOut.WriteProgress(progress.Progress{ - ID: stringid.TruncateID(task.NodeID), + ID: formatter.TruncateID(task.NodeID), Action: fmt.Sprintf("%-[1]*s", longestState, task.Status.State), Current: numberedStates[task.Status.State], Total: maxProgress, diff --git a/cli/command/task/formatter.go b/cli/command/task/formatter.go index 0ce03d669..b87ab3a4b 100644 --- a/cli/command/task/formatter.go +++ b/cli/command/task/formatter.go @@ -8,7 +8,6 @@ import ( "github.com/distribution/reference" "github.com/docker/cli/cli/command/formatter" "github.com/docker/docker/api/types/swarm" - "github.com/docker/docker/pkg/stringid" "github.com/docker/go-units" ) @@ -79,7 +78,7 @@ func (c *taskContext) MarshalJSON() ([]byte, error) { func (c *taskContext) ID() string { if c.trunc { - return stringid.TruncateID(c.task.ID) + return formatter.TruncateID(c.task.ID) } return c.task.ID } diff --git a/cli/command/trust/formatter.go b/cli/command/trust/formatter.go index 5cf9e9d33..9597cfbd7 100644 --- a/cli/command/trust/formatter.go +++ b/cli/command/trust/formatter.go @@ -5,7 +5,6 @@ import ( "strings" "github.com/docker/cli/cli/command/formatter" - "github.com/docker/docker/pkg/stringid" ) const ( @@ -119,7 +118,7 @@ func (c *signerInfoContext) Keys() string { truncatedKeys := []string{} if c.trunc { for _, keyID := range c.s.Keys { - truncatedKeys = append(truncatedKeys, stringid.TruncateID(keyID)) + truncatedKeys = append(truncatedKeys, formatter.TruncateID(keyID)) } return strings.Join(truncatedKeys, ", ") }