From 0be687acc0a2893c1ff5fd388f1fc1777749cba5 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sun, 13 Jul 2025 13:48:59 +0200 Subject: [PATCH 01/11] cli/command/container: don't set CopyToContainerOptions.AllowOverwriteDirWithFile The `AllowOverwriteDirWithFile` option was added when reimplementing the CLI using the API Client lib in [moby@1b2b91b]. Before that refactor, the `noOverwriteDirNonDir` query argument [would be set unconditionally][1] by the CLI, with no options to control the behavior. It's unclear why the `noOverwriteDirNonDir` was implemented as opt-in (not opt-out), as overwriting a file with a directory (or vice-versa) would generally be unexpected behavior. We're considering making `noOverwriteDirNonDir` unconditional on the daemon side, and to deprecate the `AllowOverwriteDirWithFile` option. This patch removes its use, as it was set to the default either way, and there's no options to configure it from the CLI. [1]: https://github.com/moby/moby/blob/8c9ad7b818c0a7b1e39f8df1fabba243a0961c2d/api/client/cp.go#L345-L346 [moby@1b2b91b]: https://github.com/moby/moby/commit/1b2b91ba43dc6fa1b4b758fc5a8090ce6cc597ff Signed-off-by: Sebastiaan van Stijn --- cli/command/container/cp.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cli/command/container/cp.go b/cli/command/container/cp.go index 9fd0eda9b3..d6e1aa601b 100644 --- a/cli/command/container/cp.go +++ b/cli/command/container/cp.go @@ -398,8 +398,7 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo } options := container.CopyToContainerOptions{ - AllowOverwriteDirWithFile: false, - CopyUIDGID: copyConfig.copyUIDGID, + CopyUIDGID: copyConfig.copyUIDGID, } if copyConfig.quiet { From 50fa436c213156a261ab0c2de6efc128b004ed58 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sun, 13 Jul 2025 17:43:50 +0200 Subject: [PATCH 02/11] Dockerfile: update compose to v2.38.2 Signed-off-by: Sebastiaan van Stijn --- Dockerfile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index f5213cbf18..3914870e49 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,11 @@ ARG GOTESTSUM_VERSION=v1.12.0 # It must be a tag in the docker.io/docker/buildx-bin image repository # on Docker Hub. ARG BUILDX_VERSION=0.24.0 -ARG COMPOSE_VERSION=v2.36.2 + +# COMPOSE_VERSION is the version of compose to install in the dev container. +# It must be a tag in the docker.io/docker/compose-bin image repository +# on Docker Hub. +ARG COMPOSE_VERSION=v2.38.2 FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx From e6b8cc1c7db42fcd1d4cd1c75c43b9f1d3ca2293 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sun, 13 Jul 2025 17:45:07 +0200 Subject: [PATCH 03/11] Dockerfile: update buildx to v0.25.0 Signed-off-by: Sebastiaan van Stijn --- Dockerfile | 2 +- dockerfiles/Dockerfile.dev | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3914870e49..ff7d2f1c27 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ ARG GOTESTSUM_VERSION=v1.12.0 # BUILDX_VERSION sets the version of buildx to use for the e2e tests. # It must be a tag in the docker.io/docker/buildx-bin image repository # on Docker Hub. -ARG BUILDX_VERSION=0.24.0 +ARG BUILDX_VERSION=0.25.0 # COMPOSE_VERSION is the version of compose to install in the dev container. # It must be a tag in the docker.io/docker/compose-bin image repository diff --git a/dockerfiles/Dockerfile.dev b/dockerfiles/Dockerfile.dev index 4067f16de8..67fc1f6477 100644 --- a/dockerfiles/Dockerfile.dev +++ b/dockerfiles/Dockerfile.dev @@ -6,7 +6,7 @@ ARG ALPINE_VERSION=3.21 # BUILDX_VERSION sets the version of buildx to install in the dev container. # It must be a valid tag in the docker.io/docker/buildx-bin image repository # on Docker Hub. -ARG BUILDX_VERSION=0.24.0 +ARG BUILDX_VERSION=0.25.0 FROM docker/buildx-bin:${BUILDX_VERSION} AS buildx FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS golang From 1264a59779f05357691855a3999f4ef709239b20 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sun, 13 Jul 2025 17:50:03 +0200 Subject: [PATCH 04/11] Dockerfile: bump gotest.tools/gotestsum v1.12.3 (for go1.25) full diff: https://github.com/gotestyourself/gotestsum/compare/v1.12.0...v1.12.3 Signed-off-by: Sebastiaan van Stijn --- Dockerfile | 5 ++++- dockerfiles/Dockerfile.dev | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index f5213cbf18..d56a5b504a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,10 @@ ARG BASE_DEBIAN_DISTRO=bookworm ARG GO_VERSION=1.24.5 ARG XX_VERSION=1.6.1 ARG GOVERSIONINFO_VERSION=v1.4.1 -ARG GOTESTSUM_VERSION=v1.12.0 + +# GOTESTSUM_VERSION sets the version of gotestsum to install in the dev container. +# It must be a valid tag in the https://github.com/gotestyourself/gotestsum repository. +ARG GOTESTSUM_VERSION=v1.12.3 # BUILDX_VERSION sets the version of buildx to use for the e2e tests. # It must be a tag in the docker.io/docker/buildx-bin image repository diff --git a/dockerfiles/Dockerfile.dev b/dockerfiles/Dockerfile.dev index 4067f16de8..3110e62db6 100644 --- a/dockerfiles/Dockerfile.dev +++ b/dockerfiles/Dockerfile.dev @@ -22,7 +22,9 @@ RUN --mount=type=cache,target=/root/.cache/go-build \ && gofumpt --version FROM golang AS gotestsum -ARG GOTESTSUM_VERSION=v1.12.0 +# GOTESTSUM_VERSION sets the version of gotestsum to install in the dev container. +# It must be a valid tag in the https://github.com/gotestyourself/gotestsum repository. +ARG GOTESTSUM_VERSION=v1.12.3 RUN --mount=type=cache,target=/root/.cache/go-build \ --mount=type=cache,target=/go/pkg/mod \ --mount=type=tmpfs,target=/go/src/ \ From e0f4bc699cbef4065df2ba8fa4a52d8b372b20f2 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 14 Jul 2025 19:51:30 +0200 Subject: [PATCH 05/11] 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 b7fdf47333..9816ca8347 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 a170e0eaf8..ade5de73f4 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 4480221fbf..0a5c587afe 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 87928926a5..3380a2f7e5 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 c6d2845c4e..b062c3391b 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 1dd3e75cb7..131f90db1c 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 d16f42b5ba..c1b3b7217c 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 e180f4dba0..6f1e9ce757 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 08b7febe3b..c051129e45 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 e67fa946f0..3a6eeb976d 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 f12db41619..c12c7096b5 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 fb2177e7ab..6a15a6be10 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 ac40c2ffd4..2e02a4739e 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 c5c0c9c4e4..27c24b52cd 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 6b2f83c4b6..7442c093de 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 499217ec11..2dc5f0f8bf 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 7d394267b8..7c651d859b 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 c5c00e41c6..5f87e29125 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 0ce03d6691..b87ab3a4b7 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 5cf9e9d335..9597cfbd76 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, ", ") } From 9b047a501ff2fb8bddafa3a6611ab9078e8a83ca Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 14 Jul 2025 20:05:02 +0200 Subject: [PATCH 06/11] remove uses of pkg/stringid.GenerateRandomID() This utility was only used for testing, and to generate a random suffix for Dockerfiles. As we don't need the same contract as pkg/stringid.GenerateRandomID() (not allow all-numeric IDs as they would not be usable for hostnames), we can use a local test-utility, and local implementation for the random suffix instead. Signed-off-by: Sebastiaan van Stijn --- cli/command/container/formatter_stats_test.go | 4 +- cli/command/formatter/container_test.go | 3 +- cli/command/formatter/image_test.go | 3 +- cli/command/formatter/volume_test.go | 3 +- cli/command/image/build/context.go | 14 ++++- cli/command/image/formatter_history_test.go | 3 +- cli/command/network/formatter_test.go | 3 +- cli/command/node/formatter_test.go | 3 +- cli/command/plugin/formatter_test.go | 3 +- cli/command/trust/formatter_test.go | 4 +- internal/test/randomid.go | 15 +++++ .../docker/docker/pkg/stringid/stringid.go | 63 ------------------- vendor/modules.txt | 1 - 13 files changed, 38 insertions(+), 84 deletions(-) create mode 100644 internal/test/randomid.go delete mode 100644 vendor/github.com/docker/docker/pkg/stringid/stringid.go diff --git a/cli/command/container/formatter_stats_test.go b/cli/command/container/formatter_stats_test.go index 8569ef4de4..91c69a5d7c 100644 --- a/cli/command/container/formatter_stats_test.go +++ b/cli/command/container/formatter_stats_test.go @@ -5,13 +5,13 @@ import ( "testing" "github.com/docker/cli/cli/command/formatter" - "github.com/docker/docker/pkg/stringid" + "github.com/docker/cli/internal/test" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) func TestContainerStatsContext(t *testing.T) { - containerID := stringid.GenerateRandomID() + containerID := test.RandomID() var ctx statsContext tt := []struct { diff --git a/cli/command/formatter/container_test.go b/cli/command/formatter/container_test.go index 3380a2f7e5..6974584003 100644 --- a/cli/command/formatter/container_test.go +++ b/cli/command/formatter/container_test.go @@ -13,7 +13,6 @@ import ( "github.com/docker/cli/internal/test" "github.com/docker/docker/api/types/container" - "github.com/docker/docker/pkg/stringid" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" @@ -21,7 +20,7 @@ import ( ) func TestContainerPsContext(t *testing.T) { - containerID := stringid.GenerateRandomID() + containerID := test.RandomID() unix := time.Now().Add(-65 * time.Second).Unix() var ctx ContainerContext diff --git a/cli/command/formatter/image_test.go b/cli/command/formatter/image_test.go index 6f1e9ce757..bb792f0439 100644 --- a/cli/command/formatter/image_test.go +++ b/cli/command/formatter/image_test.go @@ -9,13 +9,12 @@ import ( "github.com/docker/cli/internal/test" "github.com/docker/docker/api/types/image" - "github.com/docker/docker/pkg/stringid" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) func TestImageContext(t *testing.T) { - imageID := stringid.GenerateRandomID() + imageID := test.RandomID() unix := time.Now().Unix() zeroTime := int64(-62135596800) diff --git a/cli/command/formatter/volume_test.go b/cli/command/formatter/volume_test.go index 212c47d499..cae16c7cd1 100644 --- a/cli/command/formatter/volume_test.go +++ b/cli/command/formatter/volume_test.go @@ -12,13 +12,12 @@ import ( "github.com/docker/cli/internal/test" "github.com/docker/docker/api/types/volume" - "github.com/docker/docker/pkg/stringid" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) func TestVolumeContext(t *testing.T) { - volumeName := stringid.GenerateRandomID() + volumeName := test.RandomID() var ctx volumeContext cases := []struct { diff --git a/cli/command/image/build/context.go b/cli/command/image/build/context.go index 4a6abf1515..362d68a582 100644 --- a/cli/command/image/build/context.go +++ b/cli/command/image/build/context.go @@ -4,6 +4,8 @@ import ( "archive/tar" "bufio" "bytes" + "crypto/rand" + "encoding/hex" "fmt" "io" "net/http" @@ -18,7 +20,6 @@ import ( "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/progress" "github.com/docker/docker/pkg/streamformatter" - "github.com/docker/docker/pkg/stringid" "github.com/moby/go-archive" "github.com/moby/go-archive/compression" "github.com/moby/patternmatcher" @@ -379,7 +380,7 @@ func AddDockerfileToBuildContext(dockerfileCtx io.ReadCloser, buildCtx io.ReadCl return nil, "", err } now := time.Now() - randomName := ".dockerfile." + stringid.GenerateRandomID()[:20] + randomName := ".dockerfile." + randomSuffix() buildCtx = archive.ReplaceFileTarWrapper(buildCtx, map[string]archive.TarModifierFunc{ // Add the dockerfile with a random filename @@ -422,6 +423,15 @@ func AddDockerfileToBuildContext(dockerfileCtx io.ReadCloser, buildCtx io.ReadCl return buildCtx, randomName, nil } +// randomSuffix returns a unique, 20-character ID consisting of a-z, 0-9. +func randomSuffix() string { + b := make([]byte, 32) + if _, err := rand.Read(b); err != nil { + panic(err) // This shouldn't happen + } + return hex.EncodeToString(b)[:20] +} + // Compress the build context for sending to the API func Compress(buildCtx io.ReadCloser) (io.ReadCloser, error) { pipeReader, pipeWriter := io.Pipe() diff --git a/cli/command/image/formatter_history_test.go b/cli/command/image/formatter_history_test.go index 3a6eeb976d..611f7d5067 100644 --- a/cli/command/image/formatter_history_test.go +++ b/cli/command/image/formatter_history_test.go @@ -10,7 +10,6 @@ import ( "github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/internal/test" "github.com/docker/docker/api/types/image" - "github.com/docker/docker/pkg/stringid" "gotest.tools/v3/assert" ) @@ -21,7 +20,7 @@ type historyCase struct { } func TestHistoryContext_ID(t *testing.T) { - id := stringid.GenerateRandomID() + id := test.RandomID() var ctx historyContext cases := []historyCase{ diff --git a/cli/command/network/formatter_test.go b/cli/command/network/formatter_test.go index 2e02a4739e..ddeeb895fd 100644 --- a/cli/command/network/formatter_test.go +++ b/cli/command/network/formatter_test.go @@ -14,13 +14,12 @@ import ( "github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/internal/test" "github.com/docker/docker/api/types/network" - "github.com/docker/docker/pkg/stringid" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) func TestNetworkContext(t *testing.T) { - networkID := stringid.GenerateRandomID() + networkID := test.RandomID() var ctx networkContext cases := []struct { diff --git a/cli/command/node/formatter_test.go b/cli/command/node/formatter_test.go index c8e4658e77..8983f337a6 100644 --- a/cli/command/node/formatter_test.go +++ b/cli/command/node/formatter_test.go @@ -14,13 +14,12 @@ import ( "github.com/docker/cli/internal/test" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/system" - "github.com/docker/docker/pkg/stringid" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) func TestNodeContext(t *testing.T) { - nodeID := stringid.GenerateRandomID() + nodeID := test.RandomID() var ctx nodeContext cases := []struct { diff --git a/cli/command/plugin/formatter_test.go b/cli/command/plugin/formatter_test.go index 7442c093de..63f1ce29e9 100644 --- a/cli/command/plugin/formatter_test.go +++ b/cli/command/plugin/formatter_test.go @@ -12,13 +12,12 @@ import ( "github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/internal/test" "github.com/docker/docker/api/types" - "github.com/docker/docker/pkg/stringid" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) func TestPluginContext(t *testing.T) { - pluginID := stringid.GenerateRandomID() + pluginID := test.RandomID() var ctx pluginContext cases := []struct { diff --git a/cli/command/trust/formatter_test.go b/cli/command/trust/formatter_test.go index db1534526f..4c0c194f4c 100644 --- a/cli/command/trust/formatter_test.go +++ b/cli/command/trust/formatter_test.go @@ -5,13 +5,13 @@ import ( "testing" "github.com/docker/cli/cli/command/formatter" - "github.com/docker/docker/pkg/stringid" + "github.com/docker/cli/internal/test" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" ) func TestTrustTag(t *testing.T) { - digest := stringid.GenerateRandomID() + digest := test.RandomID() trustedTag := "tag" var ctx trustTagContext diff --git a/internal/test/randomid.go b/internal/test/randomid.go new file mode 100644 index 0000000000..fd5ed2ca73 --- /dev/null +++ b/internal/test/randomid.go @@ -0,0 +1,15 @@ +package test + +import ( + "crypto/rand" + "encoding/hex" +) + +// RandomID returns a unique, 64-character ID consisting of a-z, 0-9. +func RandomID() string { + b := make([]byte, 32) + if _, err := rand.Read(b); err != nil { + panic(err) // This shouldn't happen + } + return hex.EncodeToString(b) +} diff --git a/vendor/github.com/docker/docker/pkg/stringid/stringid.go b/vendor/github.com/docker/docker/pkg/stringid/stringid.go deleted file mode 100644 index 919cd5a500..0000000000 --- a/vendor/github.com/docker/docker/pkg/stringid/stringid.go +++ /dev/null @@ -1,63 +0,0 @@ -// Package stringid provides helper functions for dealing with string identifiers -package stringid - -import ( - "crypto/rand" - "encoding/hex" - "strings" -) - -const ( - shortLen = 12 - fullLen = 64 -) - -// TruncateID returns a shorthand version of a string identifier for convenience. -// A collision with other shorthands is very unlikely, but possible. -// In case of a collision a lookup with TruncIndex.Get() will fail, and the caller -// will need to use a longer prefix, or the full-length Id. -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 -} - -// GenerateRandomID returns a unique, 64-character ID consisting of a-z, 0-9. -// It guarantees that the ID, when truncated ([TruncateID]) does not consist -// of numbers only, so that the truncated ID can be used as hostname for -// containers. -func GenerateRandomID() string { - b := make([]byte, 32) - for { - if _, err := rand.Read(b); err != nil { - panic(err) // This shouldn't happen - } - id := hex.EncodeToString(b) - - // make sure that the truncated ID does not consist of only numeric - // characters, as it's used as default hostname for containers. - // - // See: - // - https://github.com/moby/moby/issues/3869 - // - https://bugzilla.redhat.com/show_bug.cgi?id=1059122 - if allNum(id[:shortLen]) { - // all numbers; try again - continue - } - return id - } -} - -// allNum checks whether id consists of only numbers (0-9). -func allNum(id string) bool { - for _, c := range []byte(id) { - if c > '9' || c < '0' { - return false - } - } - return true -} diff --git a/vendor/modules.txt b/vendor/modules.txt index 1abd3baaaa..c8d13ac032 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -102,7 +102,6 @@ github.com/docker/docker/pkg/process github.com/docker/docker/pkg/progress github.com/docker/docker/pkg/stdcopy github.com/docker/docker/pkg/streamformatter -github.com/docker/docker/pkg/stringid github.com/docker/docker/registry # github.com/docker/docker-credential-helpers v0.9.3 ## explicit; go 1.21 From 3600ebca76752c08fab9671f02ec0b1494aade5e Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 14 Jul 2025 22:09:31 +0200 Subject: [PATCH 07/11] remove uses of github.com/docker/docker/pkg/ioutils ReadCloserWrapper It was the only utility we consumed from the package, and it's trivial to implement, so let's create local copies of it. Signed-off-by: Sebastiaan van Stijn --- cli/command/container/hijack.go | 18 ++- cli/command/image/build/context.go | 29 ++++- .../pkg/ioutils/fswriters_deprecated.go | 44 ------- .../docker/docker/pkg/ioutils/readers.go | 118 ------------------ .../docker/docker/pkg/ioutils/writeflusher.go | 96 -------------- .../docker/docker/pkg/ioutils/writers.go | 28 ----- vendor/modules.txt | 1 - 7 files changed, 41 insertions(+), 293 deletions(-) delete mode 100644 vendor/github.com/docker/docker/pkg/ioutils/fswriters_deprecated.go delete mode 100644 vendor/github.com/docker/docker/pkg/ioutils/readers.go delete mode 100644 vendor/github.com/docker/docker/pkg/ioutils/writeflusher.go delete mode 100644 vendor/github.com/docker/docker/pkg/ioutils/writers.go diff --git a/cli/command/container/hijack.go b/cli/command/container/hijack.go index 4d5ad26786..8e725cb444 100644 --- a/cli/command/container/hijack.go +++ b/cli/command/container/hijack.go @@ -9,7 +9,6 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/docker/api/types" - "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/stdcopy" "github.com/moby/term" "github.com/sirupsen/logrus" @@ -19,6 +18,18 @@ import ( // TODO: This could be moved to `pkg/term`. var defaultEscapeKeys = []byte{16, 17} +// readCloserWrapper wraps an io.Reader, and implements an io.ReadCloser +// It calls the given callback function when closed. +type readCloserWrapper struct { + io.Reader + closer func() error +} + +// Close calls back the passed closer function +func (r *readCloserWrapper) Close() error { + return r.closer() +} + // A hijackedIOStreamer handles copying input to and output from streams to the // connection. type hijackedIOStreamer struct { @@ -100,7 +111,10 @@ func (h *hijackedIOStreamer) setupInput() (restore func(), err error) { } } - h.inputStream = ioutils.NewReadCloserWrapper(term.NewEscapeProxy(h.inputStream, escapeKeys), h.inputStream.Close) + h.inputStream = &readCloserWrapper{ + Reader: term.NewEscapeProxy(h.inputStream, escapeKeys), + closer: h.inputStream.Close, + } return restore, nil } diff --git a/cli/command/image/build/context.go b/cli/command/image/build/context.go index 4a6abf1515..f1ecfb6821 100644 --- a/cli/command/image/build/context.go +++ b/cli/command/image/build/context.go @@ -15,7 +15,6 @@ import ( "time" "github.com/docker/docker/builder/remotecontext/git" - "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/progress" "github.com/docker/docker/pkg/streamformatter" "github.com/docker/docker/pkg/stringid" @@ -108,7 +107,7 @@ func DetectArchiveReader(input io.ReadCloser) (rc io.ReadCloser, isArchive bool, return nil, false, errors.Errorf("failed to peek context header from STDIN: %v", err) } - return ioutils.NewReadCloserWrapper(buf, func() error { return input.Close() }), IsArchive(magic), nil + return newReadCloserWrapper(buf, func() error { return input.Close() }), IsArchive(magic), nil } // WriteTempDockerfile writes a Dockerfile stream to a temporary file with a @@ -169,7 +168,7 @@ func GetContextFromReader(rc io.ReadCloser, dockerfileName string) (out io.ReadC return nil, "", err } - return ioutils.NewReadCloserWrapper(tarArchive, func() error { + return newReadCloserWrapper(tarArchive, func() error { err := tarArchive.Close() os.RemoveAll(dockerfileDir) return err @@ -227,7 +226,7 @@ func GetContextFromURL(out io.Writer, remoteURL, dockerfileName string) (io.Read // Pass the response body through a progress reader. progReader := progress.NewProgressReader(response.Body, progressOutput, response.ContentLength, "", "Downloading build context from remote url: "+remoteURL) - return GetContextFromReader(ioutils.NewReadCloserWrapper(progReader, func() error { return response.Body.Close() }), dockerfileName) + return GetContextFromReader(newReadCloserWrapper(progReader, func() error { return response.Body.Close() }), dockerfileName) } // getWithStatusError does an http.Get() and returns an error if the @@ -444,3 +443,25 @@ func Compress(buildCtx io.ReadCloser) (io.ReadCloser, error) { return pipeReader, nil } + +// readCloserWrapper wraps an io.Reader, and implements an io.ReadCloser +// It calls the given callback function when closed. It should be constructed +// with [newReadCloserWrapper]. +type readCloserWrapper struct { + io.Reader + closer func() error +} + +// Close calls back the passed closer function +func (r *readCloserWrapper) Close() error { + return r.closer() +} + +// newReadCloserWrapper wraps an io.Reader, and implements an io.ReadCloser. +// It calls the given callback function when closed. +func newReadCloserWrapper(r io.Reader, closer func() error) io.ReadCloser { + return &readCloserWrapper{ + Reader: r, + closer: closer, + } +} diff --git a/vendor/github.com/docker/docker/pkg/ioutils/fswriters_deprecated.go b/vendor/github.com/docker/docker/pkg/ioutils/fswriters_deprecated.go deleted file mode 100644 index f34e06911c..0000000000 --- a/vendor/github.com/docker/docker/pkg/ioutils/fswriters_deprecated.go +++ /dev/null @@ -1,44 +0,0 @@ -package ioutils - -import ( - "io" - "os" - - "github.com/moby/sys/atomicwriter" -) - -// NewAtomicFileWriter returns WriteCloser so that writing to it writes to a -// temporary file and closing it atomically changes the temporary file to -// destination path. Writing and closing concurrently is not allowed. -// NOTE: umask is not considered for the file's permissions. -// -// Deprecated: use [atomicwriter.New] instead. -func NewAtomicFileWriter(filename string, perm os.FileMode) (io.WriteCloser, error) { - return atomicwriter.New(filename, perm) -} - -// AtomicWriteFile atomically writes data to a file named by filename and with the specified permission bits. -// NOTE: umask is not considered for the file's permissions. -// -// Deprecated: use [atomicwriter.WriteFile] instead. -func AtomicWriteFile(filename string, data []byte, perm os.FileMode) error { - return atomicwriter.WriteFile(filename, data, perm) -} - -// AtomicWriteSet is used to atomically write a set -// of files and ensure they are visible at the same time. -// Must be committed to a new directory. -// -// Deprecated: use [atomicwriter.WriteSet] instead. -type AtomicWriteSet = atomicwriter.WriteSet - -// NewAtomicWriteSet creates a new atomic write set to -// atomically create a set of files. The given directory -// is used as the base directory for storing files before -// commit. If no temporary directory is given the system -// default is used. -// -// Deprecated: use [atomicwriter.NewWriteSet] instead. -func NewAtomicWriteSet(tmpDir string) (*atomicwriter.WriteSet, error) { - return atomicwriter.NewWriteSet(tmpDir) -} diff --git a/vendor/github.com/docker/docker/pkg/ioutils/readers.go b/vendor/github.com/docker/docker/pkg/ioutils/readers.go deleted file mode 100644 index 21c7a2f688..0000000000 --- a/vendor/github.com/docker/docker/pkg/ioutils/readers.go +++ /dev/null @@ -1,118 +0,0 @@ -package ioutils - -import ( - "context" - "io" - "runtime/debug" - "sync/atomic" - - "github.com/containerd/log" -) - -// readCloserWrapper wraps an io.Reader, and implements an io.ReadCloser -// It calls the given callback function when closed. It should be constructed -// with NewReadCloserWrapper -type readCloserWrapper struct { - io.Reader - closer func() error - closed atomic.Bool -} - -// Close calls back the passed closer function -func (r *readCloserWrapper) Close() error { - if !r.closed.CompareAndSwap(false, true) { - subsequentCloseWarn("ReadCloserWrapper") - return nil - } - return r.closer() -} - -// NewReadCloserWrapper wraps an io.Reader, and implements an io.ReadCloser. -// It calls the given callback function when closed. -func NewReadCloserWrapper(r io.Reader, closer func() error) io.ReadCloser { - return &readCloserWrapper{ - Reader: r, - closer: closer, - } -} - -// cancelReadCloser wraps an io.ReadCloser with a context for cancelling read -// operations. -type cancelReadCloser struct { - cancel func() - pR *io.PipeReader // Stream to read from - pW *io.PipeWriter - closed atomic.Bool -} - -// NewCancelReadCloser creates a wrapper that closes the ReadCloser when the -// context is cancelled. The returned io.ReadCloser must be closed when it is -// no longer needed. -func NewCancelReadCloser(ctx context.Context, in io.ReadCloser) io.ReadCloser { - pR, pW := io.Pipe() - - // Create a context used to signal when the pipe is closed - doneCtx, cancel := context.WithCancel(context.Background()) - - p := &cancelReadCloser{ - cancel: cancel, - pR: pR, - pW: pW, - } - - go func() { - _, err := io.Copy(pW, in) - select { - case <-ctx.Done(): - // If the context was closed, p.closeWithError - // was already called. Calling it again would - // change the error that Read returns. - default: - p.closeWithError(err) - } - in.Close() - }() - go func() { - for { - select { - case <-ctx.Done(): - p.closeWithError(ctx.Err()) - case <-doneCtx.Done(): - return - } - } - }() - - return p -} - -// Read wraps the Read method of the pipe that provides data from the wrapped -// ReadCloser. -func (p *cancelReadCloser) Read(buf []byte) (int, error) { - return p.pR.Read(buf) -} - -// closeWithError closes the wrapper and its underlying reader. It will -// cause future calls to Read to return err. -func (p *cancelReadCloser) closeWithError(err error) { - _ = p.pW.CloseWithError(err) - p.cancel() -} - -// Close closes the wrapper its underlying reader. It will cause -// future calls to Read to return io.EOF. -func (p *cancelReadCloser) Close() error { - if !p.closed.CompareAndSwap(false, true) { - subsequentCloseWarn("cancelReadCloser") - return nil - } - p.closeWithError(io.EOF) - return nil -} - -func subsequentCloseWarn(name string) { - log.G(context.TODO()).Error("subsequent attempt to close " + name) - if log.GetLevel() >= log.DebugLevel { - log.G(context.TODO()).Errorf("stack trace: %s", string(debug.Stack())) - } -} diff --git a/vendor/github.com/docker/docker/pkg/ioutils/writeflusher.go b/vendor/github.com/docker/docker/pkg/ioutils/writeflusher.go deleted file mode 100644 index 8d60ef2f62..0000000000 --- a/vendor/github.com/docker/docker/pkg/ioutils/writeflusher.go +++ /dev/null @@ -1,96 +0,0 @@ -package ioutils - -import ( - "io" - "sync" -) - -// WriteFlusher wraps the Write and Flush operation ensuring that every write -// is a flush. In addition, the Close method can be called to intercept -// Read/Write calls if the targets lifecycle has already ended. -type WriteFlusher struct { - w io.Writer - flusher flusher - flushed chan struct{} - flushedOnce sync.Once - closed chan struct{} - closeLock sync.Mutex -} - -type flusher interface { - Flush() -} - -func (wf *WriteFlusher) Write(b []byte) (int, error) { - select { - case <-wf.closed: - return 0, io.EOF - default: - } - - n, err := wf.w.Write(b) - wf.Flush() // every write is a flush. - return n, err -} - -// Flush the stream immediately. -func (wf *WriteFlusher) Flush() { - select { - case <-wf.closed: - return - default: - } - - wf.flushedOnce.Do(func() { - close(wf.flushed) - }) - wf.flusher.Flush() -} - -// Flushed returns the state of flushed. -// If it's flushed, return true, or else it return false. -func (wf *WriteFlusher) Flushed() bool { - // BUG(stevvooe): Remove this method. Its use is inherently racy. Seems to - // be used to detect whether or a response code has been issued or not. - // Another hook should be used instead. - var flushed bool - select { - case <-wf.flushed: - flushed = true - default: - } - return flushed -} - -// Close closes the write flusher, disallowing any further writes to the -// target. After the flusher is closed, all calls to write or flush will -// result in an error. -func (wf *WriteFlusher) Close() error { - wf.closeLock.Lock() - defer wf.closeLock.Unlock() - - select { - case <-wf.closed: - return io.EOF - default: - close(wf.closed) - } - return nil -} - -// nopFlusher represents a type which flush operation is nop. -type nopFlusher struct{} - -// Flush is a nop operation. -func (f *nopFlusher) Flush() {} - -// NewWriteFlusher returns a new WriteFlusher. -func NewWriteFlusher(w io.Writer) *WriteFlusher { - var fl flusher - if f, ok := w.(flusher); ok { - fl = f - } else { - fl = &nopFlusher{} - } - return &WriteFlusher{w: w, flusher: fl, closed: make(chan struct{}), flushed: make(chan struct{})} -} diff --git a/vendor/github.com/docker/docker/pkg/ioutils/writers.go b/vendor/github.com/docker/docker/pkg/ioutils/writers.go deleted file mode 100644 index 4cf2d4de95..0000000000 --- a/vendor/github.com/docker/docker/pkg/ioutils/writers.go +++ /dev/null @@ -1,28 +0,0 @@ -package ioutils - -import ( - "io" - "sync/atomic" -) - -type writeCloserWrapper struct { - io.Writer - closer func() error - closed atomic.Bool -} - -func (r *writeCloserWrapper) Close() error { - if !r.closed.CompareAndSwap(false, true) { - subsequentCloseWarn("WriteCloserWrapper") - return nil - } - return r.closer() -} - -// NewWriteCloserWrapper returns a new io.WriteCloser. -func NewWriteCloserWrapper(r io.Writer, closer func() error) io.WriteCloser { - return &writeCloserWrapper{ - Writer: r, - closer: closer, - } -} diff --git a/vendor/modules.txt b/vendor/modules.txt index 1abd3baaaa..5d59a5f392 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -95,7 +95,6 @@ github.com/docker/docker/client github.com/docker/docker/internal/lazyregexp github.com/docker/docker/internal/multierror github.com/docker/docker/pkg/homedir -github.com/docker/docker/pkg/ioutils github.com/docker/docker/pkg/jsonmessage github.com/docker/docker/pkg/longpath github.com/docker/docker/pkg/process From 53d02ece89349a54e41c24d890cba85d09323fd2 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 14 Jul 2025 22:30:30 +0200 Subject: [PATCH 08/11] remove use of github.com/docker/docker/pkg/longpath Signed-off-by: Sebastiaan van Stijn --- cli/command/image/build/context_windows.go | 27 ++++++++++-- .../image/build/context_windows_test.go | 22 ++++++++++ .../docker/docker/pkg/longpath/longpath.go | 42 ------------------- vendor/modules.txt | 1 - 4 files changed, 46 insertions(+), 46 deletions(-) create mode 100644 cli/command/image/build/context_windows_test.go delete mode 100644 vendor/github.com/docker/docker/pkg/longpath/longpath.go diff --git a/cli/command/image/build/context_windows.go b/cli/command/image/build/context_windows.go index f127e5a140..ffd6ec53dd 100644 --- a/cli/command/image/build/context_windows.go +++ b/cli/command/image/build/context_windows.go @@ -2,8 +2,7 @@ package build import ( "path/filepath" - - "github.com/docker/docker/pkg/longpath" + "strings" ) func getContextRoot(srcPath string) (string, error) { @@ -11,5 +10,27 @@ func getContextRoot(srcPath string) (string, error) { if err != nil { return "", err } - return longpath.AddPrefix(cr), nil + return addPrefix(cr), nil +} + +// longPathPrefix is the longpath prefix for Windows file paths. +const longPathPrefix = `\\?\` + +// addPrefix adds the Windows long path prefix to the path provided if +// it does not already have it. +// +// See https://github.com/moby/moby/pull/15898 +// +// This is a copy of [longpath.AddPrefix]. +// +// [longpath.AddPrefix]:https://pkg.go.dev/github.com/docker/docker@v28.3.2+incompatible/pkg/longpath#AddPrefix +func addPrefix(path string) string { + if strings.HasPrefix(path, longPathPrefix) { + return path + } + if strings.HasPrefix(path, `\\`) { + // This is a UNC path, so we need to add 'UNC' to the path as well. + return longPathPrefix + `UNC` + path[1:] + } + return longPathPrefix + path } diff --git a/cli/command/image/build/context_windows_test.go b/cli/command/image/build/context_windows_test.go new file mode 100644 index 0000000000..bf91b8718f --- /dev/null +++ b/cli/command/image/build/context_windows_test.go @@ -0,0 +1,22 @@ +package build + +import ( + "strings" + "testing" +) + +func TestStandardLongPath(t *testing.T) { + c := `C:\simple\path` + longC := addPrefix(c) + if !strings.EqualFold(longC, `\\?\C:\simple\path`) { + t.Errorf("Wrong long path returned. Original = %s ; Long = %s", c, longC) + } +} + +func TestUNCLongPath(t *testing.T) { + c := `\\server\share\path` + longC := addPrefix(c) + if !strings.EqualFold(longC, `\\?\UNC\server\share\path`) { + t.Errorf("Wrong UNC long path returned. Original = %s ; Long = %s", c, longC) + } +} diff --git a/vendor/github.com/docker/docker/pkg/longpath/longpath.go b/vendor/github.com/docker/docker/pkg/longpath/longpath.go deleted file mode 100644 index e5f454c962..0000000000 --- a/vendor/github.com/docker/docker/pkg/longpath/longpath.go +++ /dev/null @@ -1,42 +0,0 @@ -// Package longpath introduces some constants and helper functions for handling -// long paths in Windows. -// -// Long paths are expected to be prepended with "\\?\" and followed by either a -// drive letter, a UNC server\share, or a volume identifier. -package longpath - -import ( - "os" - "runtime" - "strings" -) - -// longPathPrefix is the longpath prefix for Windows file paths. -const longPathPrefix = `\\?\` - -// AddPrefix adds the Windows long path prefix to the path provided if -// it does not already have it. -func AddPrefix(path string) string { - if strings.HasPrefix(path, longPathPrefix) { - return path - } - if strings.HasPrefix(path, `\\`) { - // This is a UNC path, so we need to add 'UNC' to the path as well. - return longPathPrefix + `UNC` + path[1:] - } - return longPathPrefix + path -} - -// MkdirTemp is the equivalent of [os.MkdirTemp], except that on Windows -// the result is in Windows longpath format. On Unix systems it is -// equivalent to [os.MkdirTemp]. -func MkdirTemp(dir, prefix string) (string, error) { - tempDir, err := os.MkdirTemp(dir, prefix) - if err != nil { - return "", err - } - if runtime.GOOS != "windows" { - return tempDir, nil - } - return AddPrefix(tempDir), nil -} diff --git a/vendor/modules.txt b/vendor/modules.txt index 1abd3baaaa..e1109075d2 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -97,7 +97,6 @@ github.com/docker/docker/internal/multierror github.com/docker/docker/pkg/homedir github.com/docker/docker/pkg/ioutils github.com/docker/docker/pkg/jsonmessage -github.com/docker/docker/pkg/longpath github.com/docker/docker/pkg/process github.com/docker/docker/pkg/progress github.com/docker/docker/pkg/stdcopy From 0f2b709c7cf4b89efdb222b39497712719420a94 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 15 Jul 2025 01:03:24 +0200 Subject: [PATCH 09/11] cli/command/container: diff: remove redundant validation and cleanup client.ContainerDiff already validates the given container name/ID, and produces an error when empty, so we don't have to check for this; https://github.com/moby/moby/blob/abba330bbfe10765822b59bb68af99db439736ba/client/container_diff.go#L13-L16 While updating, also; - remove the diffOptions type, as there were no other options, and make the container name/ID a string argument. - fix camelCase nameing of dockerCLI Before this patch: docker diff "" Container name cannot be empty With this patch: docker diff "" invalid container name or ID: value is empty Signed-off-by: Sebastiaan van Stijn --- cli/command/container/diff.go | 19 ++++--------------- cli/command/container/diff_test.go | 14 -------------- 2 files changed, 4 insertions(+), 29 deletions(-) diff --git a/cli/command/container/diff.go b/cli/command/container/diff.go index 15699f5dbb..93791fbd09 100644 --- a/cli/command/container/diff.go +++ b/cli/command/container/diff.go @@ -7,25 +7,17 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/formatter" - "github.com/pkg/errors" "github.com/spf13/cobra" ) -type diffOptions struct { - container string -} - // NewDiffCommand creates a new cobra.Command for `docker diff` func NewDiffCommand(dockerCli command.Cli) *cobra.Command { - var opts diffOptions - return &cobra.Command{ Use: "diff CONTAINER", Short: "Inspect changes to files or directories on a container's filesystem", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - opts.container = args[0] - return runDiff(cmd.Context(), dockerCli, &opts) + return runDiff(cmd.Context(), dockerCli, args[0]) }, Annotations: map[string]string{ "aliases": "docker container diff, docker diff", @@ -34,16 +26,13 @@ func NewDiffCommand(dockerCli command.Cli) *cobra.Command { } } -func runDiff(ctx context.Context, dockerCli command.Cli, opts *diffOptions) error { - if opts.container == "" { - return errors.New("Container name cannot be empty") - } - changes, err := dockerCli.Client().ContainerDiff(ctx, opts.container) +func runDiff(ctx context.Context, dockerCLI command.Cli, containerID string) error { + changes, err := dockerCLI.Client().ContainerDiff(ctx, containerID) if err != nil { return err } diffCtx := formatter.Context{ - Output: dockerCli.Out(), + Output: dockerCLI.Out(), Format: NewDiffFormat("{{.Type}} {{.Path}}"), } return DiffFormatWrite(diffCtx, changes) diff --git a/cli/command/container/diff_test.go b/cli/command/container/diff_test.go index fd2271e434..685a95d125 100644 --- a/cli/command/container/diff_test.go +++ b/cli/command/container/diff_test.go @@ -77,17 +77,3 @@ func TestRunDiffClientError(t *testing.T) { err := cmd.Execute() assert.ErrorIs(t, err, clientError) } - -func TestRunDiffEmptyContainerError(t *testing.T) { - cli := test.NewFakeCli(&fakeClient{}) - - cmd := NewDiffCommand(cli) - cmd.SetOut(io.Discard) - cmd.SetErr(io.Discard) - - containerID := "" - cmd.SetArgs([]string{containerID}) - - err := cmd.Execute() - assert.Error(t, err, "Container name cannot be empty") -} From 8b6436ecee8d4ad7d4ae9f1e5fcf19812dc92bdf Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Wed, 16 Jul 2025 09:46:05 +0200 Subject: [PATCH 10/11] Dockerfile: document ALPINE_VERSION build-arg docker build --call outline . TARGET: binary BUILD ARG VALUE DESCRIPTION BASE_VARIANT alpine ALPINE_VERSION 3.21 sets the version of the alpine base image to use, including for the golang image. GO_VERSION 1.24.5 XX_VERSION 1.6.1 GOVERSIONINFO_VERSION v1.4.1 GO_LINKMODE static defines if static or dynamic binary should be produced GO_BUILDTAGS defines additional build tags GO_STRIP strips debugging symbols if set CGO_ENABLED manually sets if cgo is used VERSION sets the version for the produced binary PACKAGER_NAME sets the company that produced the windows binary Signed-off-by: Sebastiaan van Stijn --- Dockerfile | 4 ++++ dockerfiles/Dockerfile.authors | 2 ++ dockerfiles/Dockerfile.dev | 4 ++++ dockerfiles/Dockerfile.lint | 4 ++++ dockerfiles/Dockerfile.vendor | 4 ++++ 5 files changed, 18 insertions(+) diff --git a/Dockerfile b/Dockerfile index f5213cbf18..1b35d5157f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,10 @@ # syntax=docker/dockerfile:1 ARG BASE_VARIANT=alpine + +# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image. +# It must be a supported tag in the docker.io/library/alpine image repository +# that's also available as alpine image variant for the Golang version used. ARG ALPINE_VERSION=3.21 ARG BASE_DEBIAN_DISTRO=bookworm diff --git a/dockerfiles/Dockerfile.authors b/dockerfiles/Dockerfile.authors index 35d7928314..e2a25e8416 100644 --- a/dockerfiles/Dockerfile.authors +++ b/dockerfiles/Dockerfile.authors @@ -1,5 +1,7 @@ # syntax=docker/dockerfile:1 +# ALPINE_VERSION sets the version of the alpine base image to use. +# It must be a supported tag in the docker.io/library/alpine image repository. ARG ALPINE_VERSION=3.21 FROM alpine:${ALPINE_VERSION} AS gen diff --git a/dockerfiles/Dockerfile.dev b/dockerfiles/Dockerfile.dev index 4067f16de8..4b481f2566 100644 --- a/dockerfiles/Dockerfile.dev +++ b/dockerfiles/Dockerfile.dev @@ -1,6 +1,10 @@ # syntax=docker/dockerfile:1 ARG GO_VERSION=1.24.5 + +# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image. +# It must be a supported tag in the docker.io/library/alpine image repository +# that's also available as alpine image variant for the Golang version used. ARG ALPINE_VERSION=3.21 # BUILDX_VERSION sets the version of buildx to install in the dev container. diff --git a/dockerfiles/Dockerfile.lint b/dockerfiles/Dockerfile.lint index 1f10951328..345943c2e5 100644 --- a/dockerfiles/Dockerfile.lint +++ b/dockerfiles/Dockerfile.lint @@ -1,6 +1,10 @@ # syntax=docker/dockerfile:1 ARG GO_VERSION=1.24.5 + +# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image. +# It must be a supported tag in the docker.io/library/alpine image repository +# that's also available as alpine image variant for the Golang version used. ARG ALPINE_VERSION=3.21 ARG GOLANGCI_LINT_VERSION=v2.1.5 diff --git a/dockerfiles/Dockerfile.vendor b/dockerfiles/Dockerfile.vendor index 4b9cea56b0..e70a592178 100644 --- a/dockerfiles/Dockerfile.vendor +++ b/dockerfiles/Dockerfile.vendor @@ -1,6 +1,10 @@ # syntax=docker/dockerfile:1 ARG GO_VERSION=1.24.5 + +# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image. +# It must be a supported tag in the docker.io/library/alpine image repository +# that's also available as alpine image variant for the Golang version used. ARG ALPINE_VERSION=3.21 ARG MODOUTDATED_VERSION=v0.8.0 From 3d985799d4c08389f36341e4c120ec84005eec31 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sun, 13 Jul 2025 17:14:14 +0200 Subject: [PATCH 11/11] cli/command: remove some redundant import-aliases Signed-off-by: Sebastiaan van Stijn --- cli/command/config/formatter.go | 2 +- cli/command/container/cp.go | 2 +- cli/command/container/formatter_stats.go | 2 +- cli/command/container/prune.go | 2 +- cli/command/formatter/disk_usage.go | 2 +- cli/command/formatter/image.go | 2 +- cli/command/formatter/volume.go | 2 +- cli/command/image/formatter_history.go | 2 +- cli/command/node/formatter.go | 2 +- cli/command/secret/formatter.go | 2 +- cli/command/service/formatter.go | 2 +- cli/compose/loader/loader.go | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/cli/command/config/formatter.go b/cli/command/config/formatter.go index 42072d7580..f2defa721b 100644 --- a/cli/command/config/formatter.go +++ b/cli/command/config/formatter.go @@ -8,7 +8,7 @@ import ( "github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/inspect" "github.com/docker/docker/api/types/swarm" - units "github.com/docker/go-units" + "github.com/docker/go-units" ) const ( diff --git a/cli/command/container/cp.go b/cli/command/container/cp.go index d6e1aa601b..40b0384587 100644 --- a/cli/command/container/cp.go +++ b/cli/command/container/cp.go @@ -16,7 +16,7 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/streams" "github.com/docker/docker/api/types/container" - units "github.com/docker/go-units" + "github.com/docker/go-units" "github.com/moby/go-archive" "github.com/morikuni/aec" "github.com/pkg/errors" diff --git a/cli/command/container/formatter_stats.go b/cli/command/container/formatter_stats.go index 9816ca8347..48371eedc8 100644 --- a/cli/command/container/formatter_stats.go +++ b/cli/command/container/formatter_stats.go @@ -5,7 +5,7 @@ import ( "sync" "github.com/docker/cli/cli/command/formatter" - units "github.com/docker/go-units" + "github.com/docker/go-units" ) const ( diff --git a/cli/command/container/prune.go b/cli/command/container/prune.go index 7a1d575d6e..d75338718e 100644 --- a/cli/command/container/prune.go +++ b/cli/command/container/prune.go @@ -9,7 +9,7 @@ import ( "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/internal/prompt" "github.com/docker/cli/opts" - units "github.com/docker/go-units" + "github.com/docker/go-units" "github.com/pkg/errors" "github.com/spf13/cobra" ) diff --git a/cli/command/formatter/disk_usage.go b/cli/command/formatter/disk_usage.go index 770697f3cf..b663c59b20 100644 --- a/cli/command/formatter/disk_usage.go +++ b/cli/command/formatter/disk_usage.go @@ -11,7 +11,7 @@ import ( "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/volume" - units "github.com/docker/go-units" + "github.com/docker/go-units" ) const ( diff --git a/cli/command/formatter/image.go b/cli/command/formatter/image.go index c1b3b7217c..74c2fe758f 100644 --- a/cli/command/formatter/image.go +++ b/cli/command/formatter/image.go @@ -6,7 +6,7 @@ import ( "github.com/distribution/reference" "github.com/docker/docker/api/types/image" - units "github.com/docker/go-units" + "github.com/docker/go-units" ) const ( diff --git a/cli/command/formatter/volume.go b/cli/command/formatter/volume.go index 85f0707931..bf9ea5d44e 100644 --- a/cli/command/formatter/volume.go +++ b/cli/command/formatter/volume.go @@ -6,7 +6,7 @@ import ( "strings" "github.com/docker/docker/api/types/volume" - units "github.com/docker/go-units" + "github.com/docker/go-units" ) const ( diff --git a/cli/command/image/formatter_history.go b/cli/command/image/formatter_history.go index c051129e45..e2fcd155ce 100644 --- a/cli/command/image/formatter_history.go +++ b/cli/command/image/formatter_history.go @@ -7,7 +7,7 @@ import ( "github.com/docker/cli/cli/command/formatter" "github.com/docker/docker/api/types/image" - units "github.com/docker/go-units" + "github.com/docker/go-units" ) const ( diff --git a/cli/command/node/formatter.go b/cli/command/node/formatter.go index 19c3530bb0..c49ad30abe 100644 --- a/cli/command/node/formatter.go +++ b/cli/command/node/formatter.go @@ -10,7 +10,7 @@ import ( "github.com/docker/cli/cli/command/inspect" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/api/types/system" - units "github.com/docker/go-units" + "github.com/docker/go-units" ) const ( diff --git a/cli/command/secret/formatter.go b/cli/command/secret/formatter.go index 2883bfcf4c..e305476286 100644 --- a/cli/command/secret/formatter.go +++ b/cli/command/secret/formatter.go @@ -8,7 +8,7 @@ import ( "github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/inspect" "github.com/docker/docker/api/types/swarm" - units "github.com/docker/go-units" + "github.com/docker/go-units" ) const ( diff --git a/cli/command/service/formatter.go b/cli/command/service/formatter.go index 2dc5f0f8bf..aee5453f10 100644 --- a/cli/command/service/formatter.go +++ b/cli/command/service/formatter.go @@ -14,7 +14,7 @@ import ( mounttypes "github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/swarm" - units "github.com/docker/go-units" + "github.com/docker/go-units" "github.com/fvbommel/sortorder" "github.com/pkg/errors" ) diff --git a/cli/compose/loader/loader.go b/cli/compose/loader/loader.go index 4085f2a28f..b2673394b5 100644 --- a/cli/compose/loader/loader.go +++ b/cli/compose/loader/loader.go @@ -21,7 +21,7 @@ import ( "github.com/docker/cli/opts/swarmopts" "github.com/docker/docker/api/types/versions" "github.com/docker/go-connections/nat" - units "github.com/docker/go-units" + "github.com/docker/go-units" "github.com/go-viper/mapstructure/v2" "github.com/google/shlex" "github.com/pkg/errors"