Compare commits

..
130 Commits
Author SHA1 Message Date
Sebastiaan van StijnandGitHub 9d7ad9ff18 Merge pull request #6911 from thaJeztah/bump_modules
vendor: moby/api v1.54.1, moby/client v0.4.0
2026-04-03 16:24:17 +02:00
Sebastiaan van Stijn c88681f8d8 vendor: moby/api v1.54.1, moby/client v0.4.0
full diffs:

- https://github.com/moby/moby/compare/ef0a1e449505...api/v1.54.1
- https://github.com/moby/moby/compare/ef0a1e449505...client/v0.4.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-03 15:54:11 +02:00
Paweł GronowskiandGitHub 84b884f383 Merge pull request #6909 from thaJeztah/update_authors_mailmap
update AUTHORS and .mailmap
2026-04-03 13:53:21 +02:00
Paweł GronowskiandGitHub d6169a5ea9 Merge pull request #6910 from thaJeztah/update_version
bump version to v29.4.0-dev
2026-04-03 13:46:49 +02:00
Sebastiaan van Stijn 5ddc1553ae bump version to v29.4.0-dev
This file is only used as default if no version is specified. We
should probably get rid of this, but let's update it to better
reflect the version that developer builds are building.

https://github.com/docker/cli/blob/d48fb9f9f7bdb6e0ef37dbde68612a1704cad46e/docker.Makefile#L22

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-03 13:28:17 +02:00
Sebastiaan van Stijn a347d9e103 update AUTHORS and .mailmap
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-03 13:26:29 +02:00
Sebastiaan van StijnandGitHub 699b029b57 Merge pull request #6908 from thaJeztah/bump_runewidth
vendor: github.com/mattn/go-runewidth v0.0.22
2026-04-03 12:58:59 +02:00
Paweł GronowskiandGitHub 512607a396 Merge pull request #6889 from YoanWai/docs/prune-filter-behavior
docs: clarify multiple --filter behavior in prune commands
2026-04-03 12:53:43 +02:00
Sebastiaan van Stijn 5fca671ef4 vendor: github.com/mattn/go-runewidth v0.0.22
full diff: https://github.com/mattn/go-runewidth/compare/v0.0.21...v0.0.22

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-03 12:53:08 +02:00
Paweł GronowskiandGitHub 753b10228f Merge pull request #6893 from thaJeztah/bump_moby
vendor: moby/client and moby/api master
2026-04-03 12:49:36 +02:00
Sebastiaan van Stijn 42da40a605 vendor: moby/client and moby/api master
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-03 12:44:53 +02:00
Paweł GronowskiandGitHub 7d4f9bd581 Merge pull request #6872 from thaJeztah/link_completions
shell completions: add shell completion for `docker rm --link` and exclude legacy links for container names
2026-04-03 12:37:11 +02:00
Paweł GronowskiandGitHub 2daa2c31e8 Merge pull request #6876 from thaJeztah/stats_optimize
docker stats: assorted fixes and optimizations in rendering
2026-04-03 12:34:04 +02:00
Paweł GronowskiandGitHub d8a85fba27 Merge pull request #6875 from thaJeztah/optimize_formatter
cli/command/formatter: assorted fixes and cleanups
2026-04-03 12:32:39 +02:00
Sebastiaan van StijnandGitHub efddff6549 Merge pull request #6907 from docker/dependabot/github_actions/codecov/codecov-action-6.0.0
build(deps): bump codecov/codecov-action from 5.5.3 to 6.0.0
2026-04-03 12:32:27 +02:00
Paweł GronowskiandGitHub 72beec9840 Merge pull request #6906 from thaJeztah/stream_preserve_file
cli/streams: Out, In: preserve original os.File when available
2026-04-03 12:29:30 +02:00
dependabot[bot]andGitHub 0029d5936a build(deps): bump codecov/codecov-action from 5.5.3 to 6.0.0
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.5.3 to 6.0.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/1af58845a975a7985b0beb0cbe6fbbb71a41dbad...57e3a136b779b570ffcdbf80b3bdc90e7fab3de2)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-03 08:44:33 +00:00
Sebastiaan van Stijn e7cbaafa9d cli/command/container: statsFormatWrite: inline render func
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-02 22:00:04 +02:00
Sebastiaan van Stijn c44a4d9758 cli/command/container: RunStats: avoid bytes to strings conversions
This code is using a `bytes.Buffer` to render the stats, before writing
the results to the CLI's output. Let's try to use bytes where possible
instead of converting to a string;

- Use the buffer's `Write` (and `Out().Write`) to write directly to the
  buffer/writer where possible.
- Use `io.WriteString` instead of `fmt.Printf`
- Use `bytes.SplitSeq` instead of `strings.SplitSeq`

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-02 21:59:49 +02:00
Sebastiaan van Stijn d92d1187fc cli/command/container: RunStats: rename buffer var for brevity
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-02 21:59:10 +02:00
Sebastiaan van Stijn ee88c60a5e cli/command/container: stats: add snapshot method
Move logic to capture a snapshot of the current stats to the stats struct.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-02 21:59:05 +02:00
Sebastiaan van Stijn 4c5efd61ea cli/command/container: fix buffer reuse when printing stats
Don't write lines back into the same buffer that's being read from when
clearing lines; add a separate output buffer to construct the output,
then write it to the CLI's output at once (to prevent terminal flicker).

Relates to / introduced in cb2f95ceee.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-02 21:50:31 +02:00
Sebastiaan van Stijn b309524f60 cli/command/formatter: NewStats: update GoDoc and add TODO
Update the GoDoc to better align with the actual implementation. The
"idOrName" is used for fuzzy-matching the container, which can result
in multiple stats for the same container:

    docker ps --format 'table {{.ID}}\t{{.Names}}'
    CONTAINER ID   NAMES
    b49e6c21d12e   quizzical_maxwell

    docker stats --no-stream quizzical_maxwell b49e6c21d12e b49e6
    CONTAINER ID   NAME                CPU %     MEM USAGE / LIMIT     MEM %     NET I/O           BLOCK I/O        PIDS
    b49e6c21d12e   quizzical_maxwell   0.10%     140.8MiB / 7.653GiB   1.80%     3.11MB / 13.4kB   115MB / 1.12MB   28
    b49e6c21d12e   quizzical_maxwell   0.10%     140.8MiB / 7.653GiB   1.80%     3.11MB / 13.4kB   115MB / 1.12MB   28
    b49e6c21d12e   quizzical_maxwell   0.10%     140.8MiB / 7.653GiB   1.80%     3.11MB / 13.4kB   115MB / 1.12MB   28

We should resolve the canonical ID once, then use that as reference
to prevent duplicates. Various  parts in the code compare Container
against "ID" only (not considering "name" or "ID-prefix").

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-02 21:11:48 +02:00
Sebastiaan van Stijn abd2e211b9 cli/command/formatter: add Format.templateString, remove Context.preFormat
The `Context.preFormat` method normalizes the Format as given by the user,
and handles (e.g.) stripping the "table" prefix and replacing the "json"
format for the actual format (`{{json .}}`).

The method used a `finalFormat` field on the Context as intermediate,
and was required to be called before executing the format.

This patch adds a `Format.templateString()` method that returns the
parsed format instead of storing it on the Context. It is currently
not exported, but something we could consider in future.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-02 21:11:40 +02:00
Sebastiaan van Stijn cb615a9772 cli/command/formatter: Context.postFormat: remove redundant buffer
A tabwriter is backed by a buffer already, because it needs to re-flow columns
based on content written to it. This buffer was added in [moby@ea61dac9e6] as
part of a new feature to allow for custom delimiters; neither the patch, nor
code-review on the PR mention the extra buffer, so it likely was just overlooked.

This patch;

- removes the redundant buffer
- adds an early return for cases where no tabwriter is used.

[moby@ea61dac9e6]: https://github.com/moby/moby/commit/ea61dac9e6d04879445f9c34729055ac1bb15050

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-02 20:59:30 +02:00
Sebastiaan van Stijn f7a909d56b cli/command/formatter: optimize ContainerContext.Names
Optimize formatting of container name(s);

- Inline `StripNamePrefix` in the loop, so that we don't have to
  construct a new slice with names (in most cases only to pick
  the first one).
- Don't use `strings.Split`, as it allocates a new slice and we only
  used it to check if the container-name was a legacy-link (contained
  slashes).
- Use a string-builder to concatenate names when not truncating instead
  of using an intermediate slice (and `strings.Join`).

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-02 20:59:29 +02:00
Sebastiaan van Stijn 94d4929a04 cli/streams: Out, In: preserve original os.File when available
Preserve the original *os.File, if available, and add a File() method
to return it.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-02 20:52:30 +02:00
Sebastiaan van StijnandGitHub 2cc9fe1438 Merge pull request #6900 from thaJeztah/cli_stream_cleanups
cli/streams: assorted cleanups
2026-04-02 20:52:06 +02:00
Paweł GronowskiandGitHub 38e44e4125 Merge pull request #6905 from thaJeztah/bump_trust_deps
cmd/docker-trust: bump dependencies
2026-04-02 18:15:34 +02:00
Sebastiaan van Stijn 526dfffc26 cli/streams: simplify CheckTty
This function is very specific to attaching to containers, and probably
helps clarity to inline it where used.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-02 18:11:50 +02:00
Sebastiaan van Stijn 48721c2340 cli/streams: don't depend on embedding
Define explicit wrapper methods instead of depending on the embedded
commonStreams struct.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-02 17:29:55 +02:00
Sebastiaan van Stijn 39e82e6524 cli/streams: move constructors to the start
It's more idiomatic to define the constructor before methods.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-02 17:29:55 +02:00
Sebastiaan van Stijn 34805dd013 cli/streams: (In|Out).SetRawTerminal: dry
Move the code to the commonStream type, which is where the actual
state field is kept.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-02 17:29:55 +02:00
Sebastiaan van Stijn 6e1f03c2e0 cmd/docker-trust: bump dependencies
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-02 17:06:57 +02:00
Sebastiaan van StijnandGitHub 7639343b30 Merge pull request #6867 from docker/update-swarm-docs
Updated example tokens in swarm docs
2026-04-02 15:42:59 +02:00
87a222158d Updated example tokens in swarm docs
Prevent security scanners from detecting them as eaked secrets.

Co-authored-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
Signed-off-by: Alexandre Vallières-Lagacé <alexandre.valliereslagace@docker.com>
Signed-off-by: Alexandre Vallières-Lagacé <alexandre@vallier.es>
2026-04-02 15:25:16 +02:00
Paweł GronowskiandGitHub fbfb69f7e9 Merge pull request #6873 from thaJeztah/simplify_chips
cli/command/image: getPossibleChips: simplify
2026-04-02 13:51:17 +02:00
Paweł GronowskiandGitHub 424955ddf7 Merge pull request #6904 from thaJeztah/bump_otels
vendor: go.opentelemetry.io/otel v1.42.0, otel/contrib v1.67.0
2026-04-02 13:43:10 +02:00
Paweł GronowskiandGitHub bbb6311c09 Merge pull request #6871 from thaJeztah/completions_no_dups
cli/command/completion: don't provide duplicate completions
2026-04-02 13:41:27 +02:00
Sebastiaan van StijnandGitHub 48afa03bba Merge pull request #6878 from zampani-docker/zampani/fix-plugin-force-exit-race
fix(cmd/docker): prevent race between force-exit goroutine and plugin wait
2026-04-02 12:54:38 +02:00
Sebastiaan van Stijn 98d978df6b vendor: go.opentelemetry.io/otel v1.42.0, otel/contrib v1.67.0
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-02 12:51:19 +02:00
Sebastiaan van Stijn a48ff6b591 cli/command/completion: don't provide duplicate completions
When completing for commands that accept multiple arguments, we did
not remove suggestions that were already consumed. This could be
confusing if there was only 1 suggestion, in which case every `<tab>`
would automatically suggest the same name again:

docker rm -fv magical_lumiere magical_lumiere  magical_lumiere

This patch adds a "Unique" helper to wrap a completion func to remove
completion results that are already consumed (i.e., appear in "args").

For example:

    # initial completion: args is empty, so all results are shown
    command <tab>
    one two three

    # "one" is already used so omitted
    command one <tab>
    two three

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-02 12:46:41 +02:00
Paweł GronowskiandGitHub 12b8ca4406 Merge pull request #6901 from thaJeztah/modernize
cli/command/formatter: modernize
2026-04-02 12:34:14 +02:00
Paweł GronowskiandGitHub dc9b6e553e Merge pull request #6874 from thaJeztah/future_proof_prefix
formatting: only strip "/" prefixes
2026-04-02 11:42:31 +02:00
Paweł GronowskiandGitHub f4d2906b56 Merge pull request #6894 from Rohan5commit/docs/fix-run-following-typo-20260331
docs: fix typo in run reference examples
2026-04-02 11:40:50 +02:00
Paweł GronowskiandGitHub 6998987257 Merge pull request #6866 from thaJeztah/bump_x_deps
vendor: update golang.org/x/* dependencies
2026-04-02 11:39:38 +02:00
Paweł GronowskiandGitHub 5e856302bf Merge pull request #6899 from thaJeztah/bump_compress
vendor: github.com/klauspost/compress v1.18.5
2026-04-02 11:37:51 +02:00
Paweł GronowskiandGitHub 253dc62658 Merge pull request #6903 from thaJeztah/bump_jose
vendor: github.com/go-jose/go-jose/v4 v4.1.4
2026-04-02 11:36:52 +02:00
Paweł GronowskiandGitHub a9ea8b23fa Merge pull request #6825 from thaJeztah/bump_go1.26
update to go1.26.1
2026-04-02 11:32:46 +02:00
Sebastiaan van Stijn 091afa4957 vendor: github.com/go-jose/go-jose/v4 v4.1.4
Fixes CVE-2026-34986 / GHSA-78h2-9frx-2jm8

full diff: https://github.com/go-jose/go-jose/compare/v4.1.3...v4.1.4

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-02 02:39:43 +02:00
Sebastiaan van Stijn 968ad0ea6c vendor: golang.org/x/net v0.52.0
full diff: https://cs.opensource.google/go/x/net/+/refs/tags/v0.50.0...refs/tags/v0.52.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-02 02:31:42 +02:00
Sebastiaan van Stijn 78fb018754 vendor: golang.org/x/time v0.15.0
full diff: https://cs.opensource.google/go/x/time/+/refs/tags/v0.14.0...refs/tags/v0.15.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-02 02:29:46 +02:00
Sebastiaan van Stijn 18739a5ef6 vendor: golang.org/x/term v0.41.0
full diff: https://cs.opensource.google/go/x/term/+/refs/tags/v0.40.0...refs/tags/v0.41.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-02 02:29:45 +02:00
Sebastiaan van Stijn 9b21846cde vendor: golang.org/x/text v0.35.0
full diff: https://cs.opensource.google/go/x/text/+/refs/tags/v0.34.0...refs/tags/v0.35.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-02 02:29:45 +02:00
Sebastiaan van Stijn c22bf3c77e vendor: golang.org/x/mod v0.34.0
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-02 02:29:32 +02:00
Sebastiaan van Stijn d792fc53b7 vendor: golang.org/x/sync v0.20.0
full diff: https://cs.opensource.google/go/x/sync/+/refs/tags/v0.19.0...refs/tags/v0.20.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-02 02:17:17 +02:00
Sebastiaan van Stijn e9664a72ea vendor: golang.org/x/sys v0.42.0
full diff: https://cs.opensource.google/go/x/sys/+/refs/tags/v0.41.0...refs/tags/v0.42.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-02 02:17:17 +02:00
Sebastiaan van Stijn fb776458cb update to go1.26.1
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-02 02:12:08 +02:00
Sebastiaan van Stijn 62d80156e1 ci: pin remaining actions
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-02 02:11:26 +02:00
Sebastiaan van StijnandGitHub a3c4d64755 Merge pull request #6897 from thaJeztah/bump_minimum_go
update minimum go version to go1.25
2026-04-01 20:12:45 +02:00
Paweł GronowskiandGitHub eecf81316d Merge pull request #6898 from thaJeztah/bump_patternmatcher
vendor: github.com/moby/patternmatcher v0.6.1
2026-04-01 18:39:33 +02:00
Paweł GronowskiandGitHub e67dba1189 Merge pull request #6896 from thaJeztah/bump_grpc
vendor: google.golang.org/grpc v1.79.3
2026-04-01 18:39:30 +02:00
Paweł GronowskiandGitHub fea2465d65 Merge pull request #4723 from thaJeztah/govalidator
ci: add module compatibility check
2026-04-01 16:37:45 +02:00
Sebastiaan van Stijn ea74248e8e cli/command/formatter: modernize
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-01 15:43:34 +02:00
Sebastiaan van Stijn 5efed5fa30 vendor: github.com/klauspost/compress v1.18.5
- zstd: Fix crash when changing encoder dictionary with same ID

full diff: https://github.com/klauspost/compress/compare/v1.18.4...v1.18.5

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-01 00:38:50 +02:00
Sebastiaan van Stijn cd9e5ae84f vendor: github.com/moby/patternmatcher v0.6.1
- fix panic / nil pointer dereference on invalid patterns

full diff: https://github.com/moby/patternmatcher/compare/v0.6.0...v0.6.1

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-01 00:24:02 +02:00
Sebastiaan van Stijn bf6a1e1fcf cli-plugins/socket: modernize
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-01 00:19:32 +02:00
Sebastiaan van Stijn 8f7dc04070 update minimum go version to go1.25
- drop support for go1.24
- update vendor.mod to go1.25.0
- update //go:build tags to go1.25

The golang.org/x/ dependencies now require go1.25 as a minimum,
so updating our build tags accordingly.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-04-01 00:19:04 +02:00
Sebastiaan van Stijn a14db81c9c vendor: google.golang.org/grpc v1.79.3
fixes [CVE-2026-33186] / [GHSA-p77j-4mvh-x3m3]

full diff: https://github.com/grpc/grpc-go/compare/v1.78.0...v1.79.3

[CVE-2026-33186]: https://www.cve.org/CVERecord?id=CVE-2026-33186
[GHSA-p77j-4mvh-x3m3]: https://github.com/grpc/grpc-go/security/advisories/GHSA-p77j-4mvh-x3m3

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-31 09:22:30 +02:00
Codex e660030f3a docs: fix typo in run reference
Signed-off-by: Codex <codex@openai.com>
2026-03-31 10:31:44 +08:00
Sebastiaan van Stijn 58c1585b49 gha: validate gocompat
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-26 17:53:31 +01:00
Sebastiaan van Stijn ea42337d01 implement module compatibility check
This package imports all "importable" packages, i.e., packages that:

- are not applications ("main")
- are not internal
- and that have non-test go-files

We do this to verify that our code can be consumed as a dependency
in "module mode". When using a dependency that does not have a go.mod
(i.e.; is not a "module"), go implicitly generates a go.mod. Lacking
information from the dependency itself, it assumes "go1.16" language
(see [DefaultGoModVersion]). Starting with Go1.21, go downgrades the
language version used for such dependencies, which means that any
language feature used that is not supported by go1.16 results in a
compile error;

    # github.com/docker/cli/cli/context/store
    /go/pkg/mod/github.com/docker/cli@v25.0.0-beta.2+incompatible/cli/context/store/storeconfig.go:6:24: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    /go/pkg/mod/github.com/docker/cli@v25.0.0-beta.2+incompatible/cli/context/store/store.go:74:12: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)

These errors do NOT occur when using GOPATH mode, nor do they occur
when using "pseudo module mode" (the "-mod=mod -modfile=vendor.mod"
approach used in this repository).

As a workaround for this situation, we must include "//go:build" comments
in any file that uses newer go-language features (such as the "any" type
or the "min()", "max()" builtins).

From the go toolchain docs (https://go.dev/doc/toolchain):

> The go line for each module sets the language version the compiler enforces
> when compiling packages in that module. The language version can be changed
> on a per-file basis by using a build constraint.
>
> For example, a module containing code that uses the Go 1.21 language version
> should have a go.mod file with a go line such as go 1.21 or go 1.21.3.
> If a specific source file should be compiled only when using a newer Go
> toolchain, adding //go:build go1.22 to that source file both ensures that
> only Go 1.22 and newer toolchains will compile the file and also changes
> the language version in that file to Go 1.22.

This file is a generated module that imports all packages provided in
the repository, which replicates an external consumer using our code
as a dependency in go-module mode, and verifies all files in those
packages have the correct "//go:build <go language version>" set.

To test this package:

    make shell
    make -C ./internal/gocompat/
    make: Entering directory '/go/src/github.com/docker/cli/internal/gocompat'
    GO111MODULE=off go generate .
    GO111MODULE=on go mod tidy
    GO111MODULE=on go test -v
    # github.com/docker/cli/templates
    ../../templates/templates.go:13:17: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    # github.com/docker/cli/cli/compose/template
    ../../cli/compose/template/template.go:98:45: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/compose/template/template.go:105:27: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/compose/template/template.go:141:28: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    # github.com/docker/cli/cli/compose/types
    ../../cli/compose/types/types.go:53:22: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/compose/types/types.go:86:34: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/compose/types/types.go:105:22: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/compose/types/types.go:137:34: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/compose/types/types.go:211:20: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/compose/types/types.go:343:35: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/compose/types/types.go:442:40: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/compose/types/types.go:469:24: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/compose/types/types.go:490:24: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/compose/types/types.go:587:28: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/compose/types/types.go:442:40: too many errors
    # github.com/docker/cli/cli/context/store
    ../../cli/context/store/storeconfig.go:6:24: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/context/store/store.go:74:12: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/context/store/store.go:75:23: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/context/store/metadatastore.go:43:58: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/context/store/metadatastore.go:48:22: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/context/store/metadatastore.go:80:30: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    # github.com/docker/cli/cli/command/idresolver
    ../../cli/command/idresolver/idresolver.go:6:2: "github.com/docker/docker/api/types" imported and not used
    ../../cli/command/idresolver/idresolver.go:7:2: "github.com/docker/docker/api/types/swarm" imported and not used
    ../../cli/command/idresolver/idresolver.go:9:2: "github.com/pkg/errors" imported and not used
    ../../cli/command/idresolver/idresolver.go:28:49: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/command/idresolver/idresolver.go:58:53: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    # github.com/docker/cli/cli/compose/schema
    ../../cli/compose/schema/schema.go:20:46: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/compose/schema/schema.go:27:53: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/compose/schema/schema.go:45:32: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    ../../cli/compose/schema/schema.go:66:33: predeclared any requires go1.18 or later (-lang was set to go1.16; check go.mod)
    FAIL	gocompat [build failed]
    make: *** [Makefile:3: verify] Error 1
    make: Leaving directory '/go/src/github.com/docker/cli/internal/gocompat'

[DefaultGoModVersion]: https://github.com/golang/go/blob/58c28ba286dd0e98fe4cca80f5d64bbcb824a685/src/cmd/go/internal/gover/version.go#L15-L24
[2]: https://go.dev/doc/toolchain

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-26 17:53:31 +01:00
Paweł GronowskiandGitHub 538ee85f39 Merge pull request #6888 from thaJeztah/add_build_tags
cli-plugins/hooks: add missing "go:build" comments
2026-03-26 16:32:44 +01:00
Yoan Wainmann d573c171fe docs: clarify multiple --filter behavior in prune commands
Document how multiple --filter flags interact in prune commands:
different filter keys are ANDed (all conditions must match), while
multiple values for the same key are ORed (any value can match).

This addresses a gap in the documentation where users could not
determine whether multiple filters were combined with AND or OR
logic, which is especially important for prune commands where
the wrong assumption could lead to unintended data removal.

Closes #5899

Signed-off-by: Yoan Wainmann <yoan@mreshet.co.il>
2026-03-25 18:13:49 +02:00
Sebastiaan van Stijn 07bb479a45 cli-plugins/hooks: add missing "go:build" comments
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-25 16:32:18 +01:00
Paweł GronowskiandGitHub 9637f1b364 Merge pull request #6886 from thaJeztah/pin_actions
ci: pin actions to digests
2026-03-25 15:22:16 +01:00
Sebastiaan van Stijn 97b9e04a94 ci: pin actions to digests
As a follow-up, we should use the full version (major.minor.patch).

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-25 14:58:57 +01:00
Michael ZampaniandClaude Sonnet 4.6 2bc4307816 fix(cmd/docker): prevent race between force-exit goroutine and plugin wait
When a plugin ignores context cancellation and the user sends 3 SIGINTs,
the CLI kills the plugin with SIGKILL. Previously the signal goroutine
called os.Exit(1) directly; a race existed where plugincmd.Run() could
return first (plugin was SIGKILL'd, so ws.ExitStatus() = -1) and the
main goroutine would call os.Exit(-1) = exit code 255 before the
goroutine reached os.Exit(1).

Fix by moving exit-code ownership to the main goroutine. The signal
goroutine closes forceExitCh before calling Kill(), guaranteeing the
channel is closed before plugincmd.Run() returns (the plugin can only
die after Kill() delivers SIGKILL; Run() only returns after the process
is reaped). The main goroutine checks forceExitCh after Run() returns
and performs the print + os.Exit(1) itself.

Also return from the signal goroutine after the force-kill to prevent
further loop iterations from calling close(forceExitCh) a second time
(which would panic), in case additional signals arrive while the kill
is in flight.

Fixes a flaky failure in TestPluginSocketCommunication/detached/
the_main_CLI_exits_after_3_signals where exit code 255 was observed
instead of 1 on loaded CI runners (RC Docker on Alpine).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Michael Zampani <michael.zampani@docker.com>
2026-03-21 15:31:12 -07:00
Sebastiaan van Stijn 9c117d3c5d cli/command/container: add shell completion for docker rm --link
When linking containers through legacy links, a container can get multiple
names; its own name, and a name for each link it's providing:

    # create two containers with links between them
    docker run -d --name one nginx:alpine
    docker run -d --name two --link one:link1 --link one:link2 --link one:link3 nginx:alpine

    docker rm --link <tab>
    two/link1 two/link2 two/link3

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-20 18:24:19 +01:00
Sebastiaan van Stijn df57ff7201 cli/command/completion: ContainerNames: skip legacy link names
Inline StripNamePrefix and skip legacy links for completion.
Legacy links can be removed from a container, but only when
using `docker [container] rm --link <link-name>`.

When linking containers through legacy links, a container can get multiple
names; its own name, and a name for each link it's providing:

    # create two containers with links between them

    docker run -d --name one nginx:alpine
    docker run -d --name two --link one:link1 --link one:link2 --link one:link3 nginx:alpine

    # container "one" now has multiple names
    docker ps --no-trunc --format '{{.Names}}'
    two
    one,two/link1,two/link2,two/link3

    # running `docker rm --link` with a link-name removes a link:

    docker rm --link two/link3
    docker ps --no-trunc --format '{{.Names}}'
    two
    one,two/link1,two/link2

    # but without `--link`, it resolves the linked container and removes it:
    docker rm -fv two/link2
    two/link2
    docker ps --no-trunc --format '{{.Names}}'
    two

Legacy links are deprecated, and this can be confusing, so let's not provide
completion for secondary names.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-20 18:17:43 +01:00
Sebastiaan van Stijn dfda342e51 cli/command/formatter: StripNamePrefix only strip "/" prefix
This code was assuming the API always returns container names with
a "/" prefix. While this is currently correct, we may at some point
stop doing so.

This patch changes the code to only trim "/" as prefix and not any
other character.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-20 17:43:37 +01:00
Sebastiaan van Stijn b5efd66ba6 cli/command/container: stats: make stripping "/" prefix deterministic
This code was assuming the API always returns container names with
a "/" prefix. While this is currently correct, we may at some point
stop doing so.

This patch changes the code to only trim "/" as prefix and not any
other character.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-20 17:43:36 +01:00
Sebastiaan van Stijn 64c8d68045 cli/command/image: getPossibleChips: simplify
Extract the check to a closure that filters in-place, and
run the check in a single loop.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-20 17:33:14 +01:00
Sebastiaan van StijnandGitHub 7922984193 Merge pull request #6863 from thaJeztah/fix_stats
fix: docker stats --all: remove containers when removed
2026-03-19 15:44:15 +01:00
Sebastiaan van StijnandGitHub bcfb717a31 Merge pull request #6859 from thaJeztah/plugin_metadata
cli-plugins: separate hook types from manager and refactor
2026-03-19 14:47:29 +01:00
Sebastiaan van Stijn 4bf4d567bd cli-plugins/hooks: PrintNextSteps: slight cleanup
- skip aec to construct the formatting and use a const instead
- skip fmt.Println and write directly to the writer
- move newlines outside of the "bold" formatting

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-19 12:24:36 +01:00
Sebastiaan van Stijn dd1f7f5856 cli-plugins/hooks: simplify templating formats
This allows for slighly cleaner / more natural placeholders, as it
doesn't require the context (`.`) to be specified;

- `{{command}}` instead of `{{.Command}}` or `{{command .}}`
- `{{flagValue "my-flag"}}` instead of `{{.FlagValue "my-flag"}} or `{{flagValue . "my-flag"}}`

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-19 12:24:35 +01:00
Sebastiaan van Stijn 9243240346 cli-plugins/hooks: add commandInfo type for templating
Define a local type for methods to expose to the template, instead of
passing the cobra.Cmd. This avoids templates depending on features
exposed by Cobra that are not part of the contract, and slightly
decouples the templat from the Cobra implementation.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-19 12:24:35 +01:00
Sebastiaan van Stijn 4a1b2ef2c5 cli-plugins/hooks: update godoc
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-19 12:24:35 +01:00
Sebastiaan van Stijn 4142d4026e cli-plugins/hooks: detect if templating is needed
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-19 12:24:35 +01:00
Sebastiaan van Stijn cd053606a6 cli-plugins/hooks: slight tweaks in templates
- use `%q` instead of manually quoting the string
- use `%d` instead of manually converting the number to a string

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-19 12:24:35 +01:00
Sebastiaan van Stijn aadfe6214f cli-plugins/hooks: update tests
- add basic unit-test for the template utilities
- make sure the template parsing tests test both the current
  template produced by the utilities, as well as a fixture
- rewrite the printer test to use fixtures
- use blackbox testing ("hooks_test")

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-19 12:24:34 +01:00
Sebastiaan van Stijn dce201d6ee cli-plugins/hooks: move template utils separate from render code
These utilities are used by CLI-plugins; separate them from the render
code, which is used by teh CLI-plugin manager.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-19 12:24:34 +01:00
Sebastiaan van Stijn e26f94d823 cli-plugins/hooks: add JSON labels, omitzero
Add labels to define the expected casing and don't serialize
empty fields.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-19 12:24:27 +01:00
Sebastiaan van Stijn 0431e4d23c cli-plugins/hooks: rename HookType to ResponseType
Rename the type to match the struct it's used for. Also;

- Fix the type of the NextSteps const
- Don't use iota for values; the ResponseType is used as
  part of the "wire" format, which means that plugins using
  the value can use a different version of the module code;
  using iota increases the risk of (accidentally) changing
  values, which would break the wire format.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-18 12:20:48 +01:00
Sebastiaan van Stijn 607ebfca5d cli-plugins/hooks: rename HookMessage to Response
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-18 12:20:48 +01:00
Sebastiaan van Stijn 60180924e3 cli-plugins/manager: move HookPluginData to hooks.Request
Separate types used by plugins from the manager code.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-18 12:20:48 +01:00
Sebastiaan van Stijn dd91ed3f2d cli-plugins/manager: refactor for easier debugging
Extract the code inside the loop to a closure, so that we can more
easily set up debug-logging.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-18 12:20:42 +01:00
Sebastiaan van Stijn 0501cf8293 cli-plugins/manager: simplify ctx-cancel check
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-18 12:18:46 +01:00
Sebastiaan van Stijn 5343bdc792 cli-plugins/manager: Plugin.RunHook: improve error message
Currently, the error was a plain "exit status 1"; make the error
message more informative if we need it :)

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-18 12:18:41 +01:00
Sebastiaan van Stijn 5fb5e0b0da docker stats --all: remove containers when removed
Before this patch, running `docker stats --all` would continue showing
all containers once observed. For example;

    CONTAINER ID   NAME                 CPU %     MEM USAGE / LIMIT     MEM %     NET I/O         BLOCK I/O        PIDS
    f2a785b0cd5f   foo1                 0.00%     8.535MiB / 7.653GiB   0.11%     1.12kB / 126B   0B / 12.3kB      11
    fc191b27517f   foo2                 0.00%     8.531MiB / 7.653GiB   0.11%     998B / 126B     0B / 12.3kB      11
    5040185fba53   foo3                 0.00%     8.578MiB / 7.653GiB   0.11%     872B / 126B     0B / 8.19kB      11

WHen removing `foo2`, the container would continue to be listed:

    CONTAINER ID   NAME                 CPU %     MEM USAGE / LIMIT     MEM %     NET I/O         BLOCK I/O        PIDS
    f2a785b0cd5f   foo1                 0.00%     8.535MiB / 7.653GiB   0.11%     1.12kB / 126B   0B / 12.3kB      11
    fc191b27517f   foo2                 --        -- / --               --        --              --               --
    5040185fba53   foo3                 0.00%     8.578MiB / 7.653GiB   0.11%     872B / 126B     0B / 12.3kB      11

Starting a new `foo2` container would now produce multiple entries:

    CONTAINER ID   NAME                 CPU %     MEM USAGE / LIMIT     MEM %     NET I/O         BLOCK I/O        PIDS
    f2a785b0cd5f   foo1                 0.00%     8.535MiB / 7.653GiB   0.11%     1.25kB / 126B   0B / 12.3kB      11
    fc191b27517f   foo2                 --        -- / --               --        --              --               --
    5040185fba53   foo3                 0.00%     8.578MiB / 7.653GiB   0.11%     998B / 126B     0B / 12.3kB      11
    dba11b9e1ba9   foo2                 0.00%     8.578MiB / 7.653GiB   0.11%     872B / 126B     0B / 8.19kB      11

Repeat that, and the list would continue to grow;

    CONTAINER ID   NAME                 CPU %     MEM USAGE / LIMIT     MEM %     NET I/O         BLOCK I/O        PIDS
    f2a785b0cd5f   foo1                 0.00%     8.535MiB / 7.653GiB   0.11%     1.25kB / 126B   0B / 12.3kB      11
    fc191b27517f   foo2                 --        -- / --               --        --              --               --
    5040185fba53   foo3                 0.00%     8.578MiB / 7.653GiB   0.11%     998B / 126B     0B / 12.3kB      11
    dba11b9e1ba9   foo2                 --        -- / --               --        --              --               --
    193a6dcfaa2d   foo2                 --        -- / --               --        --              --               --
    bf50e58085c6   foo2                 0.00%     8.539MiB / 7.653GiB   0.11%     872B / 126B     0B / 8.19kB      11

After this patch, containers are removed when we observe a `destroy` event;

    CONTAINER ID   NAME                 CPU %     MEM USAGE / LIMIT     MEM %     NET I/O         BLOCK I/O        PIDS
    f2a785b0cd5f   foo1                 0.00%     8.535MiB / 7.653GiB   0.11%     1.5kB / 126B    0B / 12.3kB      11
    5040185fba53   foo3                 0.00%     8.578MiB / 7.653GiB   0.11%     1.25kB / 126B   0B / 12.3kB      11
    bf50e58085c6   foo2                 0.00%     8.539MiB / 7.653GiB   0.11%     872B / 126B     0B / 12.3kB      11

Containers are added when created, so in the example above, the new `foo2`
is added at the end:

    CONTAINER ID   NAME                 CPU %     MEM USAGE / LIMIT     MEM %     NET I/O         BLOCK I/O        PIDS
    f2a785b0cd5f   foo1                 0.00%     8.535MiB / 7.653GiB   0.11%     1.5kB / 126B    0B / 12.3kB      11
    5040185fba53   foo3                 0.00%     8.578MiB / 7.653GiB   0.11%     1.25kB / 126B   0B / 12.3kB      11
    bf50e58085c6   foo2                 0.00%     8.539MiB / 7.653GiB   0.11%     872B / 126B     0B / 12.3kB      11

If a container dies, and `--all` is set, we continue listing it, but stats
are not updated while the container is stopped (we should consider resetting
the stats and show `-- / --` to be more clear that we don't have the container
running).

Here's with `foo3` stopped:

    CONTAINER ID   NAME                 CPU %     MEM USAGE / LIMIT     MEM %     NET I/O         BLOCK I/O        PIDS
    f2a785b0cd5f   foo1                 0.00%     8.535MiB / 7.653GiB   0.11%     1.5kB / 126B    0B / 12.3kB      11
    5040185fba53   foo3                 0.00%     0B / 0B               0.00%     0B / 0B         0B / 0B          0
    bf50e58085c6   foo2                 0.00%     8.539MiB / 7.653GiB   0.11%     872B / 126B     0B / 12.3kB      11

Starting the container continues updating its stats:

    CONTAINER ID   NAME                 CPU %     MEM USAGE / LIMIT     MEM %     NET I/O         BLOCK I/O        PIDS
    f2a785b0cd5f   foo1                 0.00%     8.535MiB / 7.653GiB   0.11%     1.63kB / 126B   0B / 12.3kB      11
    5040185fba53   foo3                 0.00%     8.496MiB / 7.653GiB   0.11%     872B / 126B     0B / 0B          11
    bf50e58085c6   foo2                 0.00%     8.539MiB / 7.653GiB   0.11%     998B / 126B     0B / 12.3kB      11

When running without `--all`, we continue to remove containers as soon as
possible (`die` events), but with `--all`, those events are ignored with
the expectation that the container might come back.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-17 21:05:44 +01:00
Sebastiaan van StijnandGitHub 743c385f78 Merge pull request #6862 from thaJeztah/update_golangci_lint_config
fix linting and update golangci-lint config
2026-03-17 21:05:01 +01:00
Sebastiaan van StijnandGitHub 434193ff78 Merge pull request #6865 from thaJeztah/cleanup_stats
cli/command/container: RunStats: pass ctx to stats event handlers and refractor to DRY
2026-03-17 21:03:38 +01:00
Sebastiaan van StijnandGitHub adc5466cba Merge pull request #6864 from thaJeztah/bump_runewidth
vendor: github.com/mattn/go-runewidth v0.0.21
2026-03-17 21:02:53 +01:00
Sebastiaan van Stijn 560db7d451 vendor: github.com/mattn/go-runewidth v0.0.21
full diff: https://github.com/mattn/go-runewidth/compare/v0.0.20...v0.0.21

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-17 18:36:02 +01:00
Sebastiaan van Stijn dc4abf8b99 golangci-lint: gocheckcompilerdirectives: ignore "//go:fix"
The linter has not been updated yet to recognize this directive.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-17 18:14:57 +01:00
Sebastiaan van Stijn 7f781688ed golangci-lint: remove outdated exclusion
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-17 18:14:54 +01:00
Sebastiaan van Stijn 21293265b1 cli/command/image/build: use t.Chdir() in tests
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-17 18:14:48 +01:00
Sebastiaan van Stijn 3f51d0a9d2 cli/command/container: RunStats: refactor to DRY
- update setHandler to accept multiple event-types
- pass a logger to the event-handlers with the common fields
  already set.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-17 18:05:33 +01:00
Sebastiaan van Stijn 9645db767a cli/command/container: RunStats: pass ctx to stats event handlers
Wire up the context explicitly instead of capturing it in the closures.
Also pass through the context to `watch` to replace the context.TODO()

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-17 18:05:33 +01:00
Sebastiaan van StijnandGitHub 499a4c50bd Merge pull request #6800 from 4RH1T3CT0R7/master
docker cp: report both content size and transferred size
2026-03-16 17:41:05 +01:00
Paweł GronowskiandGitHub bb0f76343a Merge pull request #6804 from icemc/update-docs-flocker-plugin
Removed EOL Flocker plugin reference from plugin documentations.
2026-03-16 09:40:12 +00:00
4RH1T3CT0R7 2bc66ecbc7 docker cp: report both content size and transferred size
When copying files with `docker cp`, the success message now shows both
the actual content size and the transferred (tar stream) size when they
differ, making it easier to understand compression and overhead:

    Successfully copied 2.01MB (transferred 2.53MB) to ctr:/dir

Extract copySummary helper to keep copyToContainer under the gocyclo
complexity threshold. Add unit tests for copySummary and stdin path.

Signed-off-by: 4RH1T3CT0R7 <iprintercanon@gmail.com>
2026-03-13 21:32:58 +03:00
Sebastiaan van StijnandGitHub 26d4525d46 Merge pull request #6817 from luojiyin1987/fix-plugin-cobra
fix: restore os.Args after plugin completion and fix error return
2026-03-13 15:28:03 +01:00
Ludovic Temgoua AbandaandSebastiaan van Stijn 33790e88d0 docs: use generic myplugin example for plugin documentation
- Removed EOL Flocker plugin reference from plugin documentations.
- docs: use generic myplugin example instead of VolumeDriver

Co-authored-by: Ludovic Temgoua Abanda <abandaludovic500@gmail.com>
Signed-off-by: Ludovic Temgoua Abanda <abandaludovic500@gmail.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-13 15:27:02 +01:00
luojiyinandSebastiaan van Stijn 6b1ba1ad84 fix: restore os.Args after plugin completion and fix error return
Signed-off-by: luojiyin <luojiyin@hotmail.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-13 14:48:45 +01:00
Sebastiaan van StijnandGitHub c3a17b9def Merge pull request #6839 from literally-user/6838-fix-font-representation
scripts/warn-outside-container: fix font representation
2026-03-13 14:36:20 +01:00
Sebastiaan van StijnandGitHub 9ca766c489 Merge pull request #6858 from gounthar/feat/add-riscv64-to-bin-image-cross
Add linux/riscv64 to bin-image-cross release target
2026-03-13 13:13:37 +01:00
Bruno Verachten 300d8231da feat: add linux/riscv64 to bin-image-cross release target
Add linux/riscv64 to the bin-image-cross platforms list so that
official release images include riscv64 binaries.

riscv64 is already present in the _platforms variable (line 38) and
used by the cross, dynbinary-cross, and plugins-cross targets. CI
already builds riscv64 binaries, but they are excluded from the
release image because bin-image-cross has its own platform list.

Closes #6857

Signed-off-by: Bruno Verachten <gounthar@gmail.com>
2026-03-12 10:53:47 +01:00
Sebastiaan van StijnandGitHub 5d8cc2c987 Merge pull request #6854 from thaJeztah/missing_buildtags
cli/command: add missing "go:build" comments
2026-03-10 17:31:48 +01:00
Sebastiaan van StijnandGitHub d0442edbfe Merge pull request #6845 from thaJeztah/cleanup_godoc
cli/config/credentials: ConvertToHostname: update godoc
2026-03-10 17:31:14 +01:00
Sebastiaan van StijnandGitHub 3897d9cc09 Merge pull request #6850 from thaJeztah/update_go1.25.8
update to go1.25.8
2026-03-10 17:30:40 +01:00
Sebastiaan van Stijn 9a471180cb cli/command: add missing "go:build" comments
- commit e8dc2fce32 modernized loops to
  range over int, which requires go1.22 or later.
- commit 85ebca52fd modernized code to
  use stdlib min/max, which requires go1.21 or later.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-10 11:18:58 +01:00
Sebastiaan van StijnandGitHub e2f38c4947 Merge pull request #6848 from docker/dependabot/github_actions/docker/metadata-action-6
build(deps): bump docker/metadata-action from 5 to 6
2026-03-06 16:30:00 +01:00
Sebastiaan van StijnandGitHub 13ec581924 Merge pull request #6847 from docker/dependabot/github_actions/docker/bake-action-7
build(deps): bump docker/bake-action from 6 to 7
2026-03-06 16:29:20 +01:00
Sebastiaan van Stijn f7d83cbae8 update to go1.25.8
go1.25.8 (released 2026-03-05) includes security fixes to the html/template,
net/url, and os packages, as well as bug fixes to the go command, the compiler,
and the os package. See the Go 1.25.8 milestone on our issue tracker for details.

- 1.25.8 https://github.com/golang/go/issues?q=milestone%3AGo1.25.8+label%3ACherryPickApproved
- diff: https://github.com/golang/go/compare/go1.25.7...go1.25.8
- 1.26.1 https://github.com/golang/go/issues?q=milestone%3AGo1.26.1+label%3ACherryPickApproved
- diff: https://github.com/golang/go/compare/go1.26.0...go1.26.1

---

We have just released Go versions 1.26.1 and 1.25.8, minor point releases.

These releases include 5 security fixes following the security policy:

crypto/x509: incorrect enforcement of email constraints

- When verifying a certificate chain which contains a certificate containing
  multiple email address constraints (composed of the full email address) which
  share common local portions (the portion of the address before the '@'
  character) but different domain portions (the portion of the address after the
  '@' character), these constraints will not be properly applied, and only the
  last constraint will be considered.

  This can allow certificates in the chain containing email addresses which are
  either not permitted or excluded by the relevant constraints to be returned by
  calls to Certificate.Verify. Since the name constraint checks happen after chain
  building is complete, this only applies to certificate chains which chain to
  trusted roots (root certificates either in VerifyOptions.Roots or in the system
  root certificate pool), requiring a trusted CA to issue certificates containing
  either not permitted or excluded email addresses.

  This issue only affects Go 1.26.

  Thanks to Jakub Ciolek for reporting this issue.

  This is CVE-2026-27137 and Go issue https://go.dev/issue/77952.

- crypto/x509: panic in name constraint checking for malformed certificates

  Certificate verification can panic when a certificate in the chain has an empty
  DNS name and another certificate in the chain has excluded name constraints.
  This can crash programs that are either directly verifying X.509 certificate
  chains, or those that use TLS.

  Since the name constraint checks happen after chain building is complete, this
  only applies to certificate chains which chain to trusted roots (root
  certificates either in VerifyOptions.Roots or in the system root certificate
  pool), requiring a trusted CA to issue certificates containing malformed DNS
  names.

  This issue only affects Go 1.26.

  Thanks to Jakub Ciolek for reporting this issue.

  This is CVE-2026-27138 and Go issue https://go.dev/issue/77953.

- html/template: URLs in meta content attribute actions are not escaped

  Actions which insert URLs into the content attribute of HTML meta tags are not
  escaped. This can allow XSS if the meta tag also has an http-equiv attribute
  with the value "refresh".

  A new GODEBUG setting has been added, htmlmetacontenturlescape, which can be
  used to disable escaping URLs in actions in the meta content attribute which
  follow "url=" by setting htmlmetacontenturlescape=0.

  This is CVE-2026-27142 and Go issue https://go.dev/issue/77954.

- net/url: reject IPv6 literal not at start of host

  The Go standard library function net/url.Parse insufficiently
  validated the host/authority component and accepted some invalid URLs
  by effectively treating garbage before an IP-literal as ignorable.
  The function should have rejected this as invalid.

  To prevent this behavior, net/url.Parse now rejects IPv6 literals
  that do not appear at the start of the host subcomponent of a URL.

  Thanks to Masaki Hara (https://github.com/qnighy) of Wantedly.

  This is CVE-2026-25679 and Go issue https://go.dev/issue/77578.

- os: FileInfo can escape from a Root

  On Unix platforms, when listing the contents of a directory using
  File.ReadDir or File.Readdir the returned FileInfo could reference
  a file outside of the Root in which the File was opened.

  The contents of the FileInfo were populated using the lstat system
  call, which takes the path to the file as a parameter. If a component
  of the full path of the file described by the FileInfo is replaced with
  a symbolic link, the target of the lstat can be directed to another
  location on the filesystem.

  The impact of this escape is limited to reading metadata provided by
  lstat from arbitrary locations on the filesystem. This could be used
  to probe for the presence or absence of files as well as gleaning
  metadata like file sizes, but does not permit reading or writing files
  outside the root.

  The FileInfo is now populated using fstatat.

  Thank you to Miloslav Trmač of Red Hat for reporting this issue.

  This is CVE-2026-27139 and Go issue https://go.dev/issue/77827.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-06 13:52:18 +01:00
dependabot[bot]andGitHub 39d676c72d build(deps): bump docker/metadata-action from 5 to 6
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5 to 6.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](https://github.com/docker/metadata-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-06 08:42:45 +00:00
dependabot[bot]andGitHub 6453c4c3a7 build(deps): bump docker/bake-action from 6 to 7
Bumps [docker/bake-action](https://github.com/docker/bake-action) from 6 to 7.
- [Release notes](https://github.com/docker/bake-action/releases)
- [Commits](https://github.com/docker/bake-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: docker/bake-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-06 08:42:41 +00:00
Sebastiaan van Stijn eef3c957be cli/config/credentials: ConvertToHostname: update godoc
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-03-05 19:02:03 +01:00
Davlat Davydov 71db1520de scripts/warn-outside-container: fix font representation
Signed-off-by: Davlat Davydov <literally_user@hotmail.com>

fix CI

review changes
2026-03-05 19:34:01 +03:00
336 changed files with 10132 additions and 2138 deletions
+1 -1
View File
@@ -63,7 +63,7 @@ jobs:
name: Update Go
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
with:
go-version: "1.25.8"
go-version: "1.26.1"
cache: false
-
name: Initialize CodeQL
+1 -1
View File
@@ -74,7 +74,7 @@ jobs:
TESTFLAGS: -coverprofile=/tmp/coverage/coverage.txt
-
name: Send to Codecov
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
with:
files: ./build/coverage/coverage.txt
token: ${{ secrets.CODECOV_TOKEN }}
+3 -3
View File
@@ -38,7 +38,7 @@ jobs:
targets: test-coverage
-
name: Send to Codecov
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
with:
files: ./build/coverage/coverage.txt
token: ${{ secrets.CODECOV_TOKEN }}
@@ -67,7 +67,7 @@ jobs:
name: Set up Go
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
with:
go-version: "1.25.8"
go-version: "1.26.1"
cache: false
-
name: Test
@@ -81,7 +81,7 @@ jobs:
shell: bash
-
name: Send to Codecov
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v5
with:
files: /tmp/coverage.txt
working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli
+24
View File
@@ -80,3 +80,27 @@ jobs:
shell: 'script --return --quiet --command "bash {0}"'
run: |
make -f docker.Makefile ${{ matrix.target }}
validate-gocompat:
runs-on: ubuntu-24.04
env:
GOPATH: ${{ github.workspace }}
GO111MODULE: off
steps:
-
name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
path: src/github.com/docker/cli
-
name: Set up Go
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
with:
go-version: "1.26.1"
cache: false
-
name: Run gocompat check
shell: 'script --return --quiet --command "bash {0}"'
working-directory: ${{ github.workspace }}/src/github.com/docker/cli
run: |
make -C ./internal/gocompat verify
+4 -12
View File
@@ -5,7 +5,7 @@ run:
# which causes it to fallback to go1.17 semantics.
#
# TODO(thaJeztah): update "usetesting" settings to enable go1.24 features once our minimum version is go1.24
go: "1.25.8"
go: "1.26.1"
timeout: 5m
@@ -158,11 +158,6 @@ linters:
- name: use-any # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#use-any
- name: use-errors-new # https://github.com/mgechev/revive/blob/HEAD/RULES_DESCRIPTIONS.md#use-errors-new
usetesting:
os-chdir: false # FIXME(thaJeztah): Disable `os.Chdir()` detections; should be automatically disabled on Go < 1.24; see https://github.com/docker/cli/pull/5835#issuecomment-2665302478
context-background: false # FIXME(thaJeztah): Disable `context.Background()` detections; should be automatically disabled on Go < 1.24; see https://github.com/docker/cli/pull/5835#issuecomment-2665302478
context-todo: false # FIXME(thaJeztah): Disable `context.TODO()` detections; should be automatically disabled on Go < 1.24; see https://github.com/docker/cli/pull/5835#issuecomment-2665302478
exclusions:
# We prefer to use an "linters.exclusions.rules" so that new "default" exclusions are not
# automatically inherited. We can decide whether or not to follow upstream
@@ -225,13 +220,10 @@ linters:
linters:
- staticcheck
# Ignore deprecation linting for cli/command/stack/*.
#
# FIXME(thaJeztah): remove exception once these functions are un-exported or internal; see https://github.com/docker/cli/pull/6389
- text: '^(SA1019): '
path: "cli/command/stack"
# TODO(thaJeztah): remove once https://github.com/leighmcculloch/gocheckcompilerdirectives/issues/7 is fixed.
- text: "compiler directive unrecognized: //go:fix"
linters:
- staticcheck
- gocheckcompilerdirectives
# Log a warning if an exclusion rule is unused.
# Default: false
+1
View File
@@ -354,6 +354,7 @@ Lorenzo Fontana <lo@linux.com> <fontanalorenzo@me.com>
Louis Opter <kalessin@kalessin.fr>
Louis Opter <kalessin@kalessin.fr> <louis@dotcloud.com>
Lovekesh Kumar <lovekesh.kumar@rtcamp.com>
Luo Jiyin <luojiyin@hotmail.com>
Luca Favatella <luca.favatella@erlang-solutions.com> <lucafavatella@users.noreply.github.com>
Luke Marsden <me@lukemarsden.net> <luke@digital-crocus.com>
Lyn <energylyn@zju.edu.cn>
+16 -1
View File
@@ -2,6 +2,7 @@
# This file lists all contributors to the repository.
# See scripts/docs/generate-authors.sh to make modifications.
4RH1T3CT0R7 <iprintercanon@gmail.com>
A. Lester Buck III <github-reg@nbolt.com>
Aanand Prasad <aanand.prasad@gmail.com>
Aaron L. Xu <liker.xu@foxmail.com>
@@ -42,6 +43,7 @@ Alexander Larsson <alexl@redhat.com>
Alexander Morozov <lk4d4math@gmail.com>
Alexander Ryabov <i@sepa.spb.ru>
Alexandre González <agonzalezro@gmail.com>
Alexandre Vallières-Lagacé <alexandre.valliereslagace@docker.com>
Alexey Igrychev <alexey.igrychev@flant.com>
Alexis Couvreur <alexiscouvreur.pro@gmail.com>
Alfred Landrum <alfred.landrum@docker.com>
@@ -64,6 +66,7 @@ Andres G. Aragoneses <knocte@gmail.com>
Andres Leon Rangel <aleon1220@gmail.com>
Andrew France <andrew@avito.co.uk>
Andrew He <he.andrew.mail@gmail.com>
Andrew Hopp <andrew.hopp@me.com>
Andrew Hsu <andrewhsu@docker.com>
Andrew Macpherson <hopscotch23@gmail.com>
Andrew McDonnell <bugs@andrewmcdonnell.net>
@@ -127,6 +130,7 @@ Brian Goff <cpuguy83@gmail.com>
Brian Tracy <brian.tracy33@gmail.com>
Brian Wieder <brian@4wieders.com>
Bruno Sousa <bruno.sousa@docker.com>
Bruno Verachten <gounthar@gmail.com>
Bryan Bess <squarejaw@bsbess.com>
Bryan Boreham <bjboreham@gmail.com>
Bryan Murphy <bmurphy1976@gmail.com>
@@ -178,6 +182,7 @@ Christopher Svensson <stoffus@stoffus.com>
Christy Norman <christy@linux.vnet.ibm.com>
Chun Chen <ramichen@tencent.com>
Clinton Kitson <clintonskitson@gmail.com>
Codex <codex@openai.com>
Coenraad Loubser <coenraad@wish.org.za>
Colin Hebert <hebert.colin@gmail.com>
Collin Guarino <collin.guarino@gmail.com>
@@ -234,6 +239,7 @@ David Sheets <dsheets@docker.com>
David Williamson <david.williamson@docker.com>
David Xia <dxia@spotify.com>
David Young <yangboh@cn.ibm.com>
Davlat Davydov <literally_user@hotmail.com>
Deng Guangxing <dengguangxing@huawei.com>
Denis Defreyne <denis@soundcloud.com>
Denis Gladkikh <denis@gladkikh.email>
@@ -241,6 +247,7 @@ Denis Ollier <larchunix@users.noreply.github.com>
Dennis Docter <dennis@d23.nl>
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Derek McGowan <derek@mcg.dev>
Derek Misler <derek.misler@docker.com>
Des Preston <despreston@gmail.com>
Deshi Xiao <dxiao@redhat.com>
Dharmit Shah <shahdharmit@gmail.com>
@@ -260,6 +267,7 @@ Dominik Braun <dominik.braun@nbsp.de>
Don Kjer <don.kjer@gmail.com>
Dong Chen <dongluo.chen@docker.com>
DongGeon Lee <secmatth1996@gmail.com>
Dorin Geman <dorin.geman@docker.com>
Doug Davis <dug@us.ibm.com>
Drew Erny <derny@mirantis.com>
Ed Costello <epc@epcostello.com>
@@ -358,7 +366,7 @@ Hugo Gabriel Eyherabide <hugogabriel.eyherabide@gmail.com>
huqun <huqun@zju.edu.cn>
Huu Nguyen <huu@prismskylabs.com>
Hyzhou Zhy <hyzhou.zhy@alibaba-inc.com>
Iain MacDonald <IJMacD@gmail.com>
Iain MacDonald <ijmacd@gmail.com>
Iain Samuel McLean Elder <iain@isme.es>
Ian Campbell <ian.campbell@docker.com>
Ian Philpot <ian.philpot@microsoft.com>
@@ -471,6 +479,7 @@ Justyn Temme <justyntemme@gmail.com>
Jyrki Puttonen <jyrkiput@gmail.com>
Jérémie Drouet <jeremie.drouet@gmail.com>
Jérôme Petazzoni <jerome.petazzoni@docker.com>
Jörg Sommer <joerg@jo-so.de>
Jörg Thalheim <joerg@higgsboson.tk>
Kai Blin <kai@samba.org>
Kai Qiang Wu (Kennan) <wkq5325@gmail.com>
@@ -539,10 +548,12 @@ Lovekesh Kumar <lovekesh.kumar@rtcamp.com>
Luca Favatella <luca.favatella@erlang-solutions.com>
Luca Marturana <lucamarturana@gmail.com>
Lucas Chan <lucas-github@lucaschan.com>
Ludovic Temgoua Abanda <abandaludovic500@gmail.com>
Luis Henrique Mulinari <luis.mulinari@gmail.com>
Luka Hartwig <mail@lukahartwig.de>
Lukas Heeren <lukas-heeren@hotmail.com>
Lukasz Zajaczkowski <Lukasz.Zajaczkowski@ts.fujitsu.com>
Luo Jiyin <luojiyin@hotmail.com>
Lydell Manganti <LydellManganti@users.noreply.github.com>
Lénaïc Huard <lhuard@amadeus.com>
Ma Shimiao <mashimiao.fnst@cn.fujitsu.com>
@@ -603,6 +614,7 @@ Michael Spetsiotis <michael_spets@hotmail.com>
Michael Steinert <mike.steinert@gmail.com>
Michael Tews <michael@tews.dev>
Michael West <mwest@mdsol.com>
Michael Zampani <michael.zampani@docker.com>
Michal Minář <miminar@redhat.com>
Michał Czeraszkiewicz <czerasz@gmail.com>
Miguel Angel Alvarez Cabrerizo <doncicuto@gmail.com>
@@ -617,6 +629,7 @@ Mike Goelzer <mike.goelzer@docker.com>
Mike MacCana <mike.maccana@gmail.com>
mikelinjie <294893458@qq.com>
Mikhail Vasin <vasin@cloud-tv.ru>
Milas Bowman <milas.bowman@docker.com>
Milind Chawre <milindchawre@gmail.com>
Mindaugas Rukas <momomg@gmail.com>
Miroslav Gula <miroslav.gula@naytrolabs.com>
@@ -887,6 +900,7 @@ Vincent Batts <vbatts@redhat.com>
Vincent Bernat <Vincent.Bernat@exoscale.ch>
Vincent Demeester <vincent.demeester@docker.com>
Vincent Woo <me@vincentwoo.com>
Vineet Kumar <vineetkumar17112004@gmail.com>
Vishnu Kannan <vishnuk@google.com>
Vivek Goyal <vgoyal@redhat.com>
Wang Jie <wangjie5@chinaskycloud.com>
@@ -916,6 +930,7 @@ Yanqiang Miao <miao.yanqiang@zte.com.cn>
Yassine Tijani <yasstij11@gmail.com>
Yi EungJun <eungjun.yi@navercorp.com>
Ying Li <ying.li@docker.com>
Yoan Wainmann <thebook90yw@gmail.com>
Yong Tang <yong.tang.github@outlook.com>
Yosef Fertel <yfertel@gmail.com>
Yu Peng <yu.peng36@zte.com.cn>
+1 -1
View File
@@ -8,7 +8,7 @@ ARG BASE_VARIANT=alpine
ARG ALPINE_VERSION=3.23
ARG BASE_DEBIAN_DISTRO=bookworm
ARG GO_VERSION=1.25.8
ARG GO_VERSION=1.26.1
# XX_VERSION specifies the version of the xx utility to use.
# It must be a valid tag in the docker.io/tonistiigi/xx image repository.
+1 -1
View File
@@ -1 +1 @@
29.0.0-dev
29.4.0-dev
+85
View File
@@ -0,0 +1,85 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.25
// Package hooks defines the contract between the Docker CLI and CLI plugin hook
// implementations.
//
// # Audience
//
// This package is intended to be imported by CLI plugin implementations that
// implement a "hooks" subcommand, and by the Docker CLI when invoking those
// hooks.
//
// # Contract and wire format
//
// Hook inputs (see [Request]) are serialized as JSON and passed to the plugin hook
// subcommand (currently as a command-line argument). Hook outputs are emitted by
// the plugin as JSON (see [Response]).
//
// # Stability
//
// The types that represent the hook contract ([Request], [Response] and related
// constants) are considered part of Docker CLI's public Go API.
// Fields and values may be extended in a backwards-compatible way (for example,
// adding new fields), but existing fields and their meaning should remain stable.
// Plugins should ignore unknown fields and unknown hook types to remain
// forwards-compatible.
package hooks
// ResponseType is the type of response from the plugin.
type ResponseType int
const (
NextSteps ResponseType = 0
)
// Request is the type representing the information
// that plugins declaring support for hooks get passed when
// being invoked following a CLI command execution.
type Request struct {
// RootCmd is a string representing the matching hook configuration
// which is currently being invoked. If a hook for "docker context"
// is configured and the user executes "docker context ls", the plugin
// is invoked with "context".
RootCmd string `json:"RootCmd,omitzero"`
// Flags contains flags that were set on the command for which the
// hook was invoked. It uses flag names as key, with leading hyphens
// removed ("--flag" and "-flag" are included as "flag" and "f").
//
// Flag values are not included and are set to an empty string,
// except for boolean flags known to the CLI itself, for which
// the value is either "true", or "false".
//
// Plugins can use this information to adjust their [Response]
// based on whether the command triggering the hook was invoked
// with.
Flags map[string]string `json:"Flags,omitzero"`
// CommandError is a string containing the error output (if any)
// of the command for which the hook was invoked.
CommandError string `json:"CommandError,omitzero"`
}
// Response represents a plugin hook response. Plugins
// declaring support for CLI hooks need to print a JSON
// representation of this type when their hook subcommand
// is invoked.
type Response struct {
Type ResponseType `json:"Type"`
Template string `json:"Template,omitzero"`
}
// HookType is the type of response from the plugin.
//
// Deprecated: use [ResponseType] instead.
//
//go:fix inline
type HookType = ResponseType
// HookMessage represents a plugin hook response.
//
// Deprecated: use [Response] instead.
//
//go:fix inline
type HookMessage = Response
+75
View File
@@ -0,0 +1,75 @@
package hooks
import (
"fmt"
)
const (
hookTemplateCommandName = `{{command}}`
hookTemplateFlagValue = `{{flagValue %q}}`
hookTemplateArg = `{{argValue %d}}`
)
// TemplateReplaceSubcommandName returns a hook template string
// that will be replaced by the CLI subcommand being executed
//
// Example:
//
// Response{
// Type: NextSteps,
// Template: "you ran the subcommand: " + TemplateReplaceSubcommandName(),
// }
//
// When being executed after the command:
//
// docker run --name "my-container" alpine
//
// It results in the message:
//
// you ran the subcommand: run
func TemplateReplaceSubcommandName() string {
return hookTemplateCommandName
}
// TemplateReplaceFlagValue returns a hook template string that will be
// replaced with the flags value when printed by the CLI.
//
// Example:
//
// Response{
// Type: NextSteps,
// Template: "you ran a container named: " + TemplateReplaceFlagValue("name"),
// }
//
// when executed after the command:
//
// docker run --name "my-container" alpine
//
// it results in the message:
//
// you ran a container named: my-container
func TemplateReplaceFlagValue(flag string) string {
return fmt.Sprintf(hookTemplateFlagValue, flag)
}
// TemplateReplaceArg takes an index i and returns a hook
// template string that the CLI will replace the template with
// the ith argument after processing the passed flags.
//
// Example:
//
// Response{
// Type: NextSteps,
// Template: "run this image with `docker run " + TemplateReplaceArg(0) + "`",
// }
//
// when being executed after the command:
//
// docker pull alpine
//
// It results in the message:
//
// Run this image with `docker run alpine`
func TemplateReplaceArg(i int) string {
return fmt.Sprintf(hookTemplateArg, i)
}
+50
View File
@@ -0,0 +1,50 @@
package hooks_test
import (
"testing"
"github.com/docker/cli/cli-plugins/hooks"
)
func TestTemplateHelpers(t *testing.T) {
tests := []struct {
doc string
got func() string
want string
}{
{
doc: "subcommand name",
got: hooks.TemplateReplaceSubcommandName,
want: `{{command}}`,
},
{
doc: "flag value",
got: func() string {
return hooks.TemplateReplaceFlagValue("name")
},
want: `{{flagValue "name"}}`,
},
{
doc: "arg",
got: func() string {
return hooks.TemplateReplaceArg(0)
},
want: `{{argValue 0}}`,
},
{
doc: "arg",
got: func() string {
return hooks.TemplateReplaceArg(3)
},
want: `{{argValue 3}}`,
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
if got := tc.got(); got != tc.want {
t.Fatalf("expected %q, got %q", tc.want, got)
}
})
}
}
+12 -7
View File
@@ -1,18 +1,23 @@
package hooks
import (
"fmt"
"io"
import "io"
"github.com/morikuni/aec"
const (
whatsNext = "\n\033[1mWhat's next:\033[0m\n"
indent = " "
)
// PrintNextSteps renders list of [NextSteps] messages and writes them
// to out. It is a no-op if messages is empty.
func PrintNextSteps(out io.Writer, messages []string) {
if len(messages) == 0 {
return
}
_, _ = fmt.Fprintln(out, aec.Bold.Apply("\nWhat's next:"))
for _, n := range messages {
_, _ = fmt.Fprintln(out, " ", n)
_, _ = io.WriteString(out, whatsNext)
for _, msg := range messages {
_, _ = io.WriteString(out, indent)
_, _ = io.WriteString(out, msg)
_, _ = io.WriteString(out, "\n")
}
}
+19 -12
View File
@@ -1,38 +1,45 @@
package hooks
package hooks_test
import (
"bytes"
"strings"
"testing"
"github.com/morikuni/aec"
"github.com/docker/cli/cli-plugins/hooks"
"gotest.tools/v3/assert"
)
func TestPrintHookMessages(t *testing.T) {
testCases := []struct {
const header = "\n\x1b[1mWhat's next:\x1b[0m\n"
tests := []struct {
doc string
messages []string
expectedOutput string
}{
{
messages: []string{},
doc: "no messages",
messages: nil,
expectedOutput: "",
},
{
doc: "single message",
messages: []string{"Bork!"},
expectedOutput: aec.Bold.Apply("\nWhat's next:") + "\n" +
expectedOutput: header +
" Bork!\n",
},
{
doc: "multiple messages",
messages: []string{"Foo", "bar"},
expectedOutput: aec.Bold.Apply("\nWhat's next:") + "\n" +
expectedOutput: header +
" Foo\n" +
" bar\n",
},
}
for _, tc := range testCases {
w := bytes.Buffer{}
PrintNextSteps(&w, tc.messages)
assert.Equal(t, w.String(), tc.expectedOutput)
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
var w strings.Builder
hooks.PrintNextSteps(&w, tc.messages)
assert.Equal(t, w.String(), tc.expectedOutput)
})
}
}
+65 -91
View File
@@ -1,116 +1,90 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.25
package hooks
import (
"bytes"
"errors"
"fmt"
"strconv"
"strings"
"text/template"
"github.com/spf13/cobra"
)
type HookType int
const (
NextSteps = iota
)
// HookMessage represents a plugin hook response. Plugins
// declaring support for CLI hooks need to print a json
// representation of this type when their hook subcommand
// is invoked.
type HookMessage struct {
Type HookType
Template string
}
// TemplateReplaceSubcommandName returns a hook template string
// that will be replaced by the CLI subcommand being executed
//
// Example:
//
// "you ran the subcommand: " + TemplateReplaceSubcommandName()
//
// when being executed after the command:
// `docker run --name "my-container" alpine`
// will result in the message:
// `you ran the subcommand: run`
func TemplateReplaceSubcommandName() string {
return hookTemplateCommandName
}
// TemplateReplaceFlagValue returns a hook template string
// that will be replaced by the flags value.
//
// Example:
//
// "you ran a container named: " + TemplateReplaceFlagValue("name")
//
// when being executed after the command:
// `docker run --name "my-container" alpine`
// will result in the message:
// `you ran a container named: my-container`
func TemplateReplaceFlagValue(flag string) string {
return fmt.Sprintf(hookTemplateFlagValue, flag)
}
// TemplateReplaceArg takes an index i and returns a hook
// template string that the CLI will replace the template with
// the ith argument, after processing the passed flags.
//
// Example:
//
// "run this image with `docker run " + TemplateReplaceArg(0) + "`"
//
// when being executed after the command:
// `docker pull alpine`
// will result in the message:
// "Run this image with `docker run alpine`"
func TemplateReplaceArg(i int) string {
return fmt.Sprintf(hookTemplateArg, strconv.Itoa(i))
}
func ParseTemplate(hookTemplate string, cmd *cobra.Command) ([]string, error) {
tmpl := template.New("").Funcs(commandFunctions)
tmpl, err := tmpl.Parse(hookTemplate)
if err != nil {
return nil, err
out := hookTemplate
if strings.Contains(hookTemplate, "{{") {
// Message may be a template.
msgContext := commandInfo{cmd: cmd}
tmpl, err := template.New("").Funcs(template.FuncMap{
"command": msgContext.command,
"flagValue": msgContext.flagValue,
"argValue": msgContext.argValue,
// kept for backward-compatibility with old templates.
"flag": func(_ any, flagName string) (string, error) { return msgContext.flagValue(flagName) },
"arg": func(_ any, i int) (string, error) { return msgContext.argValue(i) },
}).Parse(hookTemplate)
if err != nil {
return nil, err
}
var b bytes.Buffer
err = tmpl.Execute(&b, msgContext)
if err != nil {
return nil, err
}
out = b.String()
}
b := bytes.Buffer{}
err = tmpl.Execute(&b, cmd)
if err != nil {
return nil, err
}
return strings.Split(b.String(), "\n"), nil
return strings.Split(out, "\n"), nil
}
var ErrHookTemplateParse = errors.New("failed to parse hook template")
const (
hookTemplateCommandName = "{{.Name}}"
hookTemplateFlagValue = `{{flag . "%s"}}`
hookTemplateArg = "{{arg . %s}}"
)
var commandFunctions = template.FuncMap{
"flag": getFlagValue,
"arg": getArgValue,
// commandInfo provides info about the command for which the hook was invoked.
// It is used for templated hook-messages.
type commandInfo struct {
cmd *cobra.Command
}
func getFlagValue(cmd *cobra.Command, flag string) (string, error) {
cmdFlag := cmd.Flag(flag)
if cmdFlag == nil {
return "", ErrHookTemplateParse
// Name returns the name of the (sub)command for which the hook was invoked.
//
// It's used for backward-compatibility with old templates.
func (c commandInfo) Name() string {
return c.command()
}
// command returns the name of the (sub)command for which the hook was invoked.
func (c commandInfo) command() string {
if c.cmd == nil {
return ""
}
return cmdFlag.Value.String(), nil
return c.cmd.Name()
}
func getArgValue(cmd *cobra.Command, i int) (string, error) {
flags := cmd.Flags()
if flags == nil {
return "", ErrHookTemplateParse
// flagValue returns the value that was set for the given flag when the hook was invoked.
func (c commandInfo) flagValue(flagName string) (string, error) {
if c.cmd == nil {
return "", fmt.Errorf("%w: flagValue: cmd is nil", ErrHookTemplateParse)
}
return flags.Arg(i), nil
f := c.cmd.Flag(flagName)
if f == nil {
return "", fmt.Errorf("%w: flagValue: no flags found", ErrHookTemplateParse)
}
return f.Value.String(), nil
}
// argValue returns the value of the nth argument.
func (c commandInfo) argValue(n int) (string, error) {
if c.cmd == nil {
return "", fmt.Errorf("%w: arg: cmd is nil", ErrHookTemplateParse)
}
flags := c.cmd.Flags()
v := flags.Arg(n)
if v == "" && n >= flags.NArg() {
return "", fmt.Errorf("%w: arg: %dth argument not set", ErrHookTemplateParse, n)
}
return v, nil
}
+64 -25
View File
@@ -1,43 +1,67 @@
package hooks
package hooks_test
import (
"testing"
"github.com/docker/cli/cli-plugins/hooks"
"github.com/spf13/cobra"
"gotest.tools/v3/assert"
)
// TestParseTemplate tests parsing templates as returned by plugins.
//
// It uses fixed string fixtures to lock in compatibility with existing
// plugin templates, so older formats continue to work even if we add new
// template forms.
//
// For helper-backed cases, it also verifies that templates produced by the
// current TemplateReplace* helpers parse to the same output. This lets us
// evolve the emitted template format without breaking older plugins.
func TestParseTemplate(t *testing.T) {
type testFlag struct {
name string
value string
}
testCases := []struct {
template string
tests := []struct {
doc string
template string // compatibility fixture; keep even if helpers emit a newer form
templateFunc func() string
flags []testFlag
args []string
expectedOutput []string
}{
{
doc: "empty template",
template: "",
expectedOutput: []string{""},
},
{
doc: "plain message",
template: "a plain template message",
expectedOutput: []string{"a plain template message"},
},
{
template: TemplateReplaceFlagValue("tag"),
doc: "subcommand name",
template: "hello {{.Name}}", // NOTE: fixture; do not modify without considering plugin compatibility
templateFunc: func() string { return "hello " + hooks.TemplateReplaceSubcommandName() },
expectedOutput: []string{"hello pull"},
},
{
doc: "single flag",
template: `{{flag . "tag"}}`, // NOTE: fixture; do not modify without considering plugin compatibility
templateFunc: func() string { return hooks.TemplateReplaceFlagValue("tag") },
flags: []testFlag{
{
name: "tag",
value: "my-tag",
},
{name: "tag", value: "my-tag"},
},
expectedOutput: []string{"my-tag"},
},
{
template: TemplateReplaceFlagValue("test-one") + " " + TemplateReplaceFlagValue("test2"),
doc: "multiple flags",
template: `{{flag . "test-one"}} {{flag . "test2"}}`, // NOTE: fixture; do not modify without considering plugin compatibility
templateFunc: func() string {
return hooks.TemplateReplaceFlagValue("test-one") + " " + hooks.TemplateReplaceFlagValue("test2")
},
flags: []testFlag{
{
name: "test-one",
@@ -51,36 +75,51 @@ func TestParseTemplate(t *testing.T) {
expectedOutput: []string{"value value2"},
},
{
template: TemplateReplaceArg(0) + " " + TemplateReplaceArg(1),
doc: "multiple args",
template: `{{arg . 0}} {{arg . 1}}`, // NOTE: fixture; do not modify without considering plugin compatibility
templateFunc: func() string { return hooks.TemplateReplaceArg(0) + " " + hooks.TemplateReplaceArg(1) },
args: []string{"zero", "one"},
expectedOutput: []string{"zero one"},
},
{
template: "You just pulled " + TemplateReplaceArg(0),
doc: "arg in sentence",
template: "You just pulled {{arg . 0}}", // NOTE: fixture; do not modify without considering plugin compatibility
templateFunc: func() string { return "You just pulled " + hooks.TemplateReplaceArg(0) },
args: []string{"alpine"},
expectedOutput: []string{"You just pulled alpine"},
},
{
doc: "multiline output",
template: "one line\nanother line!",
expectedOutput: []string{"one line", "another line!"},
},
}
for _, tc := range testCases {
testCmd := &cobra.Command{
Use: "pull",
Args: cobra.ExactArgs(len(tc.args)),
}
for _, f := range tc.flags {
_ = testCmd.Flags().String(f.name, "", "")
err := testCmd.Flag(f.name).Value.Set(f.value)
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
testCmd := &cobra.Command{
Use: "pull",
Args: cobra.ExactArgs(len(tc.args)),
}
for _, f := range tc.flags {
_ = testCmd.Flags().String(f.name, "", "")
err := testCmd.Flag(f.name).Value.Set(f.value)
assert.NilError(t, err)
}
err := testCmd.Flags().Parse(tc.args)
assert.NilError(t, err)
}
err := testCmd.Flags().Parse(tc.args)
assert.NilError(t, err)
out, err := ParseTemplate(tc.template, testCmd)
assert.NilError(t, err)
assert.DeepEqual(t, out, tc.expectedOutput)
// Validate using fixtures.
out, err := hooks.ParseTemplate(tc.template, testCmd)
assert.NilError(t, err)
assert.DeepEqual(t, out, tc.expectedOutput)
if tc.templateFunc != nil {
// Validate using the current template function equivalent.
out, err = hooks.ParseTemplate(tc.templateFunc(), testCmd)
assert.NilError(t, err)
assert.DeepEqual(t, out, tc.expectedOutput)
}
})
}
}
+5 -2
View File
@@ -45,8 +45,7 @@ func AddPluginCommandStubs(dockerCLI config.Provider, rootCmd *cobra.Command) (e
RunE: func(cmd *cobra.Command, args []string) error {
flags := rootCmd.PersistentFlags()
flags.SetOutput(nil)
perr := flags.Parse(args)
if perr != nil {
if err := flags.Parse(args); err != nil {
return err
}
if flags.Changed("help") {
@@ -60,7 +59,11 @@ func AddPluginCommandStubs(dockerCLI config.Provider, rootCmd *cobra.Command) (e
cargs := []string{p.Path, cobra.ShellCompRequestCmd, p.Name} //nolint:prealloc // no need to over-complicate things.
cargs = append(cargs, args...)
cargs = append(cargs, toComplete)
origArgs := os.Args
os.Args = cargs
defer func() {
os.Args = origArgs
}()
runCommand, runErr := PluginRunCommand(dockerCLI, p.Name, cmd)
if runErr != nil {
return nil, cobra.ShellCompDirectiveError
+59
View File
@@ -1,8 +1,12 @@
package manager
import (
"os"
"path/filepath"
"sync"
"testing"
"github.com/docker/cli/internal/test"
"github.com/spf13/cobra"
"gotest.tools/v3/assert"
)
@@ -24,3 +28,58 @@ func TestPluginResourceAttributesEnvvar(t *testing.T) {
env = appendPluginResourceAttributesEnvvar(nil, cmd, Plugin{Name: "compose"})
assert.DeepEqual(t, []string{"OTEL_RESOURCE_ATTRIBUTES=a.b.c=foo,docker.cli.cobra.command_path=docker%20compose"}, env)
}
func TestPluginStubRunEReturnsParseError(t *testing.T) {
cmd, err := preparePluginStubCommand(t)
assert.NilError(t, err)
err = cmd.RunE(cmd, []string{"--definitely-not-a-real-flag"})
assert.ErrorContains(t, err, "unknown flag: --definitely-not-a-real-flag")
}
func TestPluginStubCompletionRestoresOSArgs(t *testing.T) {
cmd, err := preparePluginStubCommand(t)
assert.NilError(t, err)
savedArgs := os.Args
t.Cleanup(func() { os.Args = savedArgs })
originalArgs := []string{"docker", "image", "ls"}
os.Args = append([]string(nil), originalArgs...)
_, directive := cmd.ValidArgsFunction(cmd, []string{"--all"}, "alp")
assert.Equal(t, directive, cobra.ShellCompDirectiveError)
assert.DeepEqual(t, os.Args, originalArgs)
}
func preparePluginStubCommand(t *testing.T) (*cobra.Command, error) {
t.Helper()
pluginCommandStubsOnce = sync.Once{}
tmpDir := t.TempDir()
const cliPlugin = `#!/bin/sh
printf '%s' '{"SchemaVersion":"0.1.0"}'
`
if err := os.WriteFile(filepath.Join(tmpDir, "docker-testplugin"), []byte(cliPlugin), 0o777); err != nil {
return nil, err
}
cli := test.NewFakeCli(nil)
cli.ConfigFile().CLIPluginsExtraDirs = []string{tmpDir}
root := &cobra.Command{Use: "docker"}
root.PersistentFlags().Bool("debug", false, "")
if err := AddPluginCommandStubs(cli, root); err != nil {
return nil, err
}
cmd, _, err := root.Find([]string{"testplugin"})
if err != nil {
return nil, err
}
if cmd == nil {
return nil, os.ErrNotExist
}
return cmd, nil
}
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package manager
+45 -31
View File
@@ -1,11 +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.24
//go:build go1.25
package manager
import (
"context"
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"github.com/docker/cli/cli-plugins/hooks"
@@ -19,15 +22,11 @@ import (
// HookPluginData is the type representing the information
// that plugins declaring support for hooks get passed when
// being invoked following a CLI command execution.
type HookPluginData struct {
// RootCmd is a string representing the matching hook configuration
// which is currently being invoked. If a hook for `docker context` is
// configured and the user executes `docker context ls`, the plugin will
// be invoked with `context`.
RootCmd string
Flags map[string]string
CommandError string
}
//
// Deprecated: use [hooks.Request] instead.
//
//go:fix inline
type HookPluginData = hooks.Request
// RunCLICommandHooks is the entrypoint into the hooks execution flow after
// a main CLI command was executed. It calls the hook subcommand for all
@@ -55,11 +54,8 @@ func runHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd, subComma
}
func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd, subCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []string {
// check if the context was cancelled before invoking hooks
select {
case <-ctx.Done():
if ctx.Err() != nil {
return nil
default:
}
pluginsCfg := cfg.Plugins
@@ -69,47 +65,65 @@ func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, root
pluginDirs := getPluginDirs(cfg)
nextSteps := make([]string, 0, len(pluginsCfg))
for pluginName, pluginCfg := range pluginsCfg {
match, ok := pluginMatch(pluginCfg, subCmdStr, cmdErrorMessage)
if !ok {
continue
tryInvokeHook := func(pluginName string, pluginCfg map[string]string) (messages []string, ok bool, err error) {
match, matched := pluginMatch(pluginCfg, subCmdStr, cmdErrorMessage)
if !matched {
return nil, false, nil
}
p, err := getPlugin(pluginName, pluginDirs, rootCmd)
if err != nil {
continue
return nil, false, err
}
hookReturn, err := p.RunHook(ctx, HookPluginData{
resp, err := p.RunHook(ctx, hooks.Request{
RootCmd: match,
Flags: flags,
CommandError: cmdErrorMessage,
})
if err != nil {
// skip misbehaving plugins, but don't halt execution
continue
return nil, false, err
}
var hookMessageData hooks.HookMessage
err = json.Unmarshal(hookReturn, &hookMessageData)
if err != nil {
continue
var message hooks.Response
if err := json.Unmarshal(resp, &message); err != nil {
return nil, false, fmt.Errorf("failed to unmarshal hook response (%q): %w", string(resp), err)
}
// currently the only hook type
if hookMessageData.Type != hooks.NextSteps {
continue
if message.Type != hooks.NextSteps {
return nil, false, errors.New("unexpected hook response type: " + strconv.Itoa(int(message.Type)))
}
processedHook, err := hooks.ParseTemplate(hookMessageData.Template, subCmd)
messages, err = hooks.ParseTemplate(message.Template, subCmd)
if err != nil {
return nil, false, err
}
return messages, true, nil
}
for pluginName, pluginCfg := range pluginsCfg {
messages, ok, err := tryInvokeHook(pluginName, pluginCfg)
if err != nil {
// skip misbehaving plugins, but don't halt execution
logrus.WithFields(logrus.Fields{
"error": err,
"plugin": pluginName,
}).Debug("Plugin hook invocation failed")
continue
}
if !ok {
continue
}
var appended bool
nextSteps, appended = appendNextSteps(nextSteps, processedHook)
nextSteps, appended = appendNextSteps(nextSteps, messages)
if !appended {
logrus.Debugf("Plugin %s responded with an empty hook message %q. Ignoring.", pluginName, string(hookReturn))
logrus.WithFields(logrus.Fields{
"plugin": pluginName,
}).Debug("Plugin responded with an empty hook message; ignoring")
}
}
return nextSteps
+11 -6
View File
@@ -12,6 +12,7 @@ import (
"strconv"
"strings"
"github.com/docker/cli/cli-plugins/hooks"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/spf13/cobra"
)
@@ -154,7 +155,7 @@ func validateSchemaVersion(version string) error {
// RunHook executes the plugin's hooks command
// and returns its unprocessed output.
func (p *Plugin) RunHook(ctx context.Context, hookData HookPluginData) ([]byte, error) {
func (p *Plugin) RunHook(ctx context.Context, hookData hooks.Request) ([]byte, error) {
hDataBytes, err := json.Marshal(hookData)
if err != nil {
return nil, wrapAsPluginError(err, "failed to marshall hook data")
@@ -163,12 +164,16 @@ func (p *Plugin) RunHook(ctx context.Context, hookData HookPluginData) ([]byte,
pCmd := exec.CommandContext(ctx, p.Path, p.Name, metadata.HookSubcommandName, string(hDataBytes)) // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments"
pCmd.Env = os.Environ()
pCmd.Env = append(pCmd.Env, metadata.ReexecEnvvar+"="+os.Args[0])
hookCmdOutput, err := pCmd.Output()
if err != nil {
return nil, wrapAsPluginError(err, "failed to execute plugin hook subcommand")
}
return hookCmdOutput, nil
out, err := pCmd.Output()
if err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
return nil, wrapAsPluginError(err, "plugin hook subcommand exited unsuccessfully")
}
return nil, wrapAsPluginError(err, "failed to execute plugin hook subcommand: "+pCmd.String())
}
return out, nil
}
// pluginNameFormat is used as part of errors for invalid plugin-names.
+3 -3
View File
@@ -54,9 +54,9 @@ func TestPluginServer(t *testing.T) {
})
t.Run("allows reconnects", func(t *testing.T) {
var calls int32
var calls atomic.Int32
h := func(_ net.Conn) {
atomic.AddInt32(&calls, 1)
calls.Add(1)
}
srv, err := NewPluginServer(h)
@@ -70,7 +70,7 @@ func TestPluginServer(t *testing.T) {
waitForCalls := func(n int) {
poll.WaitOn(t, func(t poll.LogT) poll.Result {
if atomic.LoadInt32(&calls) == int32(n) {
if calls.Load() == int32(n) {
return poll.Success()
}
return poll.Continue("waiting for handler to be called")
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package command
+55 -15
View File
@@ -5,7 +5,6 @@ import (
"strings"
"github.com/distribution/reference"
"github.com/docker/cli/cli/command/formatter"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
@@ -23,7 +22,7 @@ type APIClientProvider interface {
// ImageNames offers completion for images present within the local store
func ImageNames(dockerCLI APIClientProvider, limit int) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return Unique(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if limit > 0 && len(args) >= limit {
return nil, cobra.ShellCompDirectiveNoFileComp
}
@@ -36,14 +35,14 @@ func ImageNames(dockerCLI APIClientProvider, limit int) cobra.CompletionFunc {
names = append(names, img.RepoTags...)
}
return names, cobra.ShellCompDirectiveNoFileComp
}
})
}
// ImageNamesWithBase offers completion for images present within the local store,
// including both full image names with tags and base image names (repository names only)
// when multiple tags exist for the same base name
func ImageNamesWithBase(dockerCLI APIClientProvider, limit int) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return Unique(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if limit > 0 && len(args) >= limit {
return nil, cobra.ShellCompDirectiveNoFileComp
}
@@ -69,14 +68,14 @@ func ImageNamesWithBase(dockerCLI APIClientProvider, limit int) cobra.Completion
}
}
return names, cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
}
})
}
// ContainerNames offers completion for container names and IDs
// By default, only names are returned.
// Set DOCKER_COMPLETION_SHOW_CONTAINER_IDS=yes to also complete IDs.
func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(container.Summary) bool) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return Unique(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
res, err := dockerCLI.Client().ContainerList(cmd.Context(), client.ContainerListOptions{
All: all,
})
@@ -101,15 +100,21 @@ func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(conta
if showContainerIDs {
names = append(names, ctr.ID)
}
names = append(names, formatter.StripNamePrefix(ctr.Names)...)
for _, n := range ctr.Names {
// Skip legacy link names: "/linked-container/link-name"
if len(n) <= 1 || strings.IndexByte(n[1:], '/') != -1 {
continue
}
names = append(names, strings.TrimPrefix(n, "/"))
}
}
return names, cobra.ShellCompDirectiveNoFileComp
}
})
}
// VolumeNames offers completion for volumes
func VolumeNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return Unique(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
res, err := dockerCLI.Client().VolumeList(cmd.Context(), client.VolumeListOptions{})
if err != nil {
return nil, cobra.ShellCompDirectiveError
@@ -119,12 +124,12 @@ func VolumeNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
names = append(names, vol.Name)
}
return names, cobra.ShellCompDirectiveNoFileComp
}
})
}
// NetworkNames offers completion for networks
func NetworkNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return Unique(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
res, err := dockerCLI.Client().NetworkList(cmd.Context(), client.NetworkListOptions{})
if err != nil {
return nil, cobra.ShellCompDirectiveError
@@ -134,7 +139,7 @@ func NetworkNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
names = append(names, nw.Name)
}
return names, cobra.ShellCompDirectiveNoFileComp
}
})
}
// EnvVarNames offers completion for environment-variable names. This
@@ -151,7 +156,7 @@ func NetworkNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
// docker run --rm --env MY_VAR alpine printenv MY_VAR
// hello
func EnvVarNames() cobra.CompletionFunc {
return func(_ *cobra.Command, _ []string, _ string) (names []string, _ cobra.ShellCompDirective) {
return Unique(func(_ *cobra.Command, _ []string, _ string) (names []string, _ cobra.ShellCompDirective) {
envs := os.Environ()
names = make([]string, 0, len(envs))
for _, env := range envs {
@@ -159,12 +164,12 @@ func EnvVarNames() cobra.CompletionFunc {
names = append(names, name)
}
return names, cobra.ShellCompDirectiveNoFileComp
}
})
}
// FromList offers completion for the given list of options.
func FromList(options ...string) cobra.CompletionFunc {
return cobra.FixedCompletions(options, cobra.ShellCompDirectiveNoFileComp)
return Unique(cobra.FixedCompletions(options, cobra.ShellCompDirectiveNoFileComp))
}
// FileNames is a convenience function to use [cobra.ShellCompDirectiveDefault],
@@ -218,3 +223,38 @@ func Platforms() cobra.CompletionFunc {
return commonPlatforms, cobra.ShellCompDirectiveNoFileComp
}
}
// Unique wraps a completion func and removes completion results that are
// already consumed (i.e., appear in "args").
//
// For example:
//
// # initial completion: args is empty, so all results are shown
// command <tab>
// one two three
//
// # "one" is already used so omitted
// command one <tab>
// two three
func Unique(fn cobra.CompletionFunc) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
all, dir := fn(cmd, args, toComplete)
if len(all) == 0 || len(args) == 0 {
return all, dir
}
alreadyCompleted := make(map[string]struct{}, len(args))
for _, a := range args {
alreadyCompleted[a] = struct{}{}
}
out := make([]string, 0, len(all))
for _, c := range all {
if _, ok := alreadyCompleted[c]; !ok {
out = append(out, c)
}
}
return out, dir
}
}
+25 -5
View File
@@ -84,7 +84,7 @@ func TestCompleteContainerNames(t *testing.T) {
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}},
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
},
expOut: []string{"container-c", "container-c/link-b", "container-b", "container-a"},
expOut: []string{"container-c", "container-b", "container-a"},
expOpts: client.ContainerListOptions{All: true},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
@@ -97,7 +97,7 @@ func TestCompleteContainerNames(t *testing.T) {
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}},
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
},
expOut: []string{"id-c", "container-c", "container-c/link-b", "id-b", "container-b", "id-a", "container-a"},
expOut: []string{"id-c", "container-c", "id-b", "container-b", "id-a", "container-a"},
expOpts: client.ContainerListOptions{All: true},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
@@ -107,7 +107,7 @@ func TestCompleteContainerNames(t *testing.T) {
containers: []container.Summary{
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b"}},
},
expOut: []string{"container-c", "container-c/link-b"},
expOut: []string{"container-c"},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
@@ -117,7 +117,7 @@ func TestCompleteContainerNames(t *testing.T) {
func(ctr container.Summary) bool { return ctr.State == container.StateCreated },
},
containers: []container.Summary{
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b"}},
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c"}},
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}},
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
},
@@ -133,7 +133,7 @@ func TestCompleteContainerNames(t *testing.T) {
func(ctr container.Summary) bool { return ctr.State == container.StateCreated },
},
containers: []container.Summary{
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b"}},
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c"}},
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}},
{ID: "id-a", State: container.StateCreated, Names: []string{"/container-a"}},
},
@@ -351,3 +351,23 @@ func TestCompleteVolumeNames(t *testing.T) {
})
}
}
func TestUnique(t *testing.T) {
base := []string{"alpha", "beta", "gamma"}
comp := Unique(func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
return base, cobra.ShellCompDirectiveNoFileComp
})
values, directives := comp(&cobra.Command{}, []string{"beta"}, "")
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp))
assert.Check(t, is.DeepEqual(values, []string{"alpha", "gamma"}))
assert.Check(t, is.DeepEqual(base, []string{"alpha", "beta", "gamma"}))
values, directives = comp(&cobra.Command{}, []string{"gamma"}, "")
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp))
assert.Check(t, is.DeepEqual(values, []string{"alpha", "beta"}))
assert.Check(t, is.DeepEqual(base, []string{"alpha", "beta", "gamma"}))
}
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package config
+8
View File
@@ -36,6 +36,7 @@ type fakeClient struct {
infoFunc func() (client.SystemInfoResult, error)
containerStatPathFunc func(containerID, path string) (client.ContainerStatPathResult, error)
containerCopyFromFunc func(containerID, srcPath string) (client.CopyFromContainerResult, error)
containerCopyToFunc func(containerID string, options client.CopyToContainerOptions) (client.CopyToContainerResult, error)
logFunc func(string, client.ContainerLogsOptions) (client.ContainerLogsResult, error)
waitFunc func(string) client.ContainerWaitResult
containerListFunc func(client.ContainerListOptions) (client.ContainerListResult, error)
@@ -128,6 +129,13 @@ func (f *fakeClient) CopyFromContainer(_ context.Context, containerID string, op
return client.CopyFromContainerResult{}, nil
}
func (f *fakeClient) CopyToContainer(_ context.Context, containerID string, options client.CopyToContainerOptions) (client.CopyToContainerResult, error) {
if f.containerCopyToFunc != nil {
return f.containerCopyToFunc(containerID, options)
}
return client.CopyToContainerResult{}, nil
}
func (f *fakeClient) ContainerLogs(_ context.Context, containerID string, options client.ContainerLogsOptions) (client.ContainerLogsResult, error) {
if f.logFunc != nil {
return f.logFunc(containerID, options)
+30 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package container
@@ -182,6 +182,35 @@ func completeLink(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
}
}
// completeLinks implements shell completion for the `--link` option of `rm --link`.
//
// It contacts the API to get names of legacy links on containers.
// In case of an error, an empty list is returned.
func completeLinks(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
res, err := dockerCLI.Client().ContainerList(cmd.Context(), client.ContainerListOptions{
All: true,
})
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
var names []string
for _, ctr := range res.Items {
if len(ctr.Names) <= 1 {
// Container has no links names.
continue
}
for _, n := range ctr.Names {
// Skip legacy link names: "/linked-container/link-name"
if len(n) > 1 && strings.IndexByte(n[1:], '/') != -1 {
names = append(names, strings.TrimPrefix(n, "/"))
}
}
}
return names, cobra.ShellCompDirectiveNoFileComp
}
}
// completeLogDriver implements shell completion for the `--log-driver` option of `run` and `create`.
// The log drivers are collected from a call to the Info endpoint with a fallback to a hard-coded list
// of the build-in log drivers.
+41
View File
@@ -135,3 +135,44 @@ func TestCompleteSignals(t *testing.T) {
assert.Check(t, len(values) > 1)
assert.Check(t, is.Len(values, len(signal.SignalMap)))
}
func TestCompleteLinks(t *testing.T) {
tests := []struct {
doc string
showAll, showIDs bool
filters []func(container.Summary) bool
containers []container.Summary
expOut []string
expDirective cobra.ShellCompDirective
}{
{
doc: "no results",
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "all containers",
showAll: true,
containers: []container.Summary{
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b", "/container-c/link-c"}},
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b", "/container-b/link-a"}},
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
},
expOut: []string{"container-c/link-b", "container-c/link-c", "container-b/link-a"},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
comp := completeLinks(test.NewFakeCli(&fakeClient{
containerListFunc: func(client.ContainerListOptions) (client.ContainerListResult, error) {
return client.ContainerListResult{Items: tc.containers}, nil
},
}))
containers, directives := comp(&cobra.Command{}, nil, "")
assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective))
assert.Check(t, is.DeepEqual(containers, tc.expOut))
})
}
}
+59 -2
View File
@@ -168,6 +168,50 @@ func progressHumanSize(n int64) string {
return units.HumanSizeWithPrecision(float64(n), 3)
}
// localContentSize returns the total size of regular file content at path.
// For a regular file it returns the file size. For a directory it walks
// the tree and sums sizes of all regular files.
func localContentSize(path string) (int64, error) {
fi, err := os.Lstat(path)
if err != nil {
return -1, err
}
if !fi.IsDir() {
if fi.Mode().IsRegular() {
return fi.Size(), nil
}
return 0, nil
}
var total int64
err = filepath.WalkDir(path, func(_ string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if d.Type().IsRegular() {
info, err := d.Info()
if err != nil {
return err
}
total += info.Size()
}
return nil
})
return total, err
}
// copySummary formats the "Successfully copied ..." message.
// When contentSize differs from transferredSize, both values are shown.
func copySummary(contentSize, transferredSize int64, dest string) string {
if contentSize != transferredSize {
return fmt.Sprintf("Successfully copied %s (transferred %s) to %s\n",
progressHumanSize(contentSize), progressHumanSize(transferredSize), dest,
)
}
return fmt.Sprintf("Successfully copied %s to %s\n",
progressHumanSize(contentSize), dest,
)
}
func runCopy(ctx context.Context, dockerCli command.Cli, opts copyOptions) error {
srcContainer, srcPath := splitCpArg(opts.source)
destContainer, destPath := splitCpArg(opts.destination)
@@ -295,7 +339,11 @@ func copyFromContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cp
cancel()
<-done
restore()
_, _ = fmt.Fprintln(dockerCLI.Err(), "Successfully copied", progressHumanSize(copiedSize), "to", dstPath)
reportedSize := copiedSize
if !cpRes.Stat.Mode.IsDir() {
reportedSize = cpRes.Stat.Size
}
_, _ = fmt.Fprint(dockerCLI.Err(), copySummary(reportedSize, copiedSize, dstPath))
return res
}
@@ -354,11 +402,14 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo
content io.ReadCloser
resolvedDstPath string
copiedSize int64
contentSize int64
sizeErr error
)
if srcPath == "-" {
content = os.Stdin
resolvedDstPath = dstInfo.Path
sizeErr = errors.New("content size not available for stdin")
if !dstInfo.IsDir {
return fmt.Errorf(`destination "%s:%s" must be a directory`, copyConfig.container, dstPath)
}
@@ -369,6 +420,8 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo
return err
}
contentSize, sizeErr = localContentSize(srcInfo.Path)
srcArchive, err := archive.TarResource(srcInfo)
if err != nil {
return err
@@ -421,7 +474,11 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo
cancel()
<-done
restore()
_, _ = fmt.Fprintln(dockerCLI.Err(), "Successfully copied", progressHumanSize(copiedSize), "to", copyConfig.container+":"+dstInfo.Path)
reportedSize := copiedSize
if sizeErr == nil {
reportedSize = contentSize
}
_, _ = fmt.Fprint(dockerCLI.Err(), copySummary(reportedSize, copiedSize, copyConfig.container+":"+dstInfo.Path))
return err
}
+235
View File
@@ -11,6 +11,7 @@ import (
"github.com/docker/cli/internal/test"
"github.com/moby/go-archive"
"github.com/moby/go-archive/compression"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/client"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
@@ -211,3 +212,237 @@ func TestRunCopyFromContainerToFilesystemIrregularDestination(t *testing.T) {
expected := `"/dev/random" must be a directory or a regular file`
assert.ErrorContains(t, err, expected)
}
func TestCopySummary(t *testing.T) {
tests := []struct {
name string
contentSize int64
transferredSize int64
dest string
wantContains string
wantNoContain string
}{
{
name: "different sizes shows both",
contentSize: 5,
transferredSize: 2048,
dest: "/dst",
wantContains: "(transferred",
},
{
name: "equal sizes shows single value",
contentSize: 100,
transferredSize: 100,
dest: "/dst",
wantNoContain: "(transferred",
},
{
name: "both zero",
contentSize: 0,
transferredSize: 0,
dest: "ctr:/dst",
wantContains: "Successfully copied 0B to ctr:/dst",
wantNoContain: "(transferred",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := copySummary(tc.contentSize, tc.transferredSize, tc.dest)
if tc.wantContains != "" {
assert.Check(t, is.Contains(got, tc.wantContains))
}
if tc.wantNoContain != "" {
assert.Check(t, !strings.Contains(got, tc.wantNoContain), "unexpected substring %q in %q", tc.wantNoContain, got)
}
})
}
}
func TestCopyFromContainerReportsFileSize(t *testing.T) {
// The file content is "hello" (5 bytes), but the TAR archive wrapping
// it is much larger due to headers and padding. The success message
// should report the actual file size (5B), not the TAR stream size.
srcDir := fs.NewDir(t, "cp-test-from",
fs.WithFile("file1", "hello"))
destDir := fs.NewDir(t, "cp-test-from-dest")
const fileSize int64 = 5
fakeCli := test.NewFakeCli(&fakeClient{
containerCopyFromFunc: func(ctr, srcPath string) (client.CopyFromContainerResult, error) {
readCloser, err := archive.Tar(srcDir.Path(), compression.None)
return client.CopyFromContainerResult{
Content: readCloser,
Stat: container.PathStat{
Name: "file1",
Size: fileSize,
},
}, err
},
})
err := runCopy(context.TODO(), fakeCli, copyOptions{
source: "container:/file1",
destination: destDir.Path(),
})
assert.NilError(t, err)
errOut := fakeCli.ErrBuffer().String()
assert.Check(t, is.Contains(errOut, "Successfully copied 5B"))
assert.Check(t, is.Contains(errOut, "(transferred"))
}
func TestCopyToContainerReportsFileSize(t *testing.T) {
// Create a temp file with known content ("hello" = 5 bytes).
// The TAR archive sent to the container is larger, but the success
// message should report the actual content size.
srcFile := fs.NewFile(t, "cp-test-to", fs.WithContent("hello"))
fakeCli := test.NewFakeCli(&fakeClient{
containerStatPathFunc: func(containerID, path string) (client.ContainerStatPathResult, error) {
return client.ContainerStatPathResult{
Stat: container.PathStat{
Name: "tmp",
Mode: os.ModeDir | 0o755,
},
}, nil
},
containerCopyToFunc: func(containerID string, options client.CopyToContainerOptions) (client.CopyToContainerResult, error) {
_, _ = io.Copy(io.Discard, options.Content)
return client.CopyToContainerResult{}, nil
},
})
err := runCopy(context.TODO(), fakeCli, copyOptions{
source: srcFile.Path(),
destination: "container:/tmp",
})
assert.NilError(t, err)
errOut := fakeCli.ErrBuffer().String()
assert.Check(t, is.Contains(errOut, "Successfully copied 5B"))
assert.Check(t, is.Contains(errOut, "(transferred"))
}
func TestCopyToContainerReportsEmptyFileSize(t *testing.T) {
srcFile := fs.NewFile(t, "cp-test-empty", fs.WithContent(""))
fakeCli := test.NewFakeCli(&fakeClient{
containerStatPathFunc: func(containerID, path string) (client.ContainerStatPathResult, error) {
return client.ContainerStatPathResult{
Stat: container.PathStat{
Name: "tmp",
Mode: os.ModeDir | 0o755,
},
}, nil
},
containerCopyToFunc: func(containerID string, options client.CopyToContainerOptions) (client.CopyToContainerResult, error) {
_, _ = io.Copy(io.Discard, options.Content)
return client.CopyToContainerResult{}, nil
},
})
err := runCopy(context.TODO(), fakeCli, copyOptions{
source: srcFile.Path(),
destination: "container:/tmp",
})
assert.NilError(t, err)
errOut := fakeCli.ErrBuffer().String()
assert.Check(t, is.Contains(errOut, "Successfully copied 0B"))
assert.Check(t, is.Contains(errOut, "(transferred"))
}
func TestCopyToContainerReportsDirectorySize(t *testing.T) {
// Create a temp directory with files "aaa" (3 bytes) + "bbb" (3 bytes) = 6 bytes.
// The TAR archive is much larger, but the success message should report 6B.
srcDir := fs.NewDir(t, "cp-test-dir",
fs.WithFile("aaa", "aaa"),
fs.WithFile("bbb", "bbb"),
)
fakeCli := test.NewFakeCli(&fakeClient{
containerStatPathFunc: func(containerID, path string) (client.ContainerStatPathResult, error) {
return client.ContainerStatPathResult{
Stat: container.PathStat{
Name: "tmp",
Mode: os.ModeDir | 0o755,
},
}, nil
},
containerCopyToFunc: func(containerID string, options client.CopyToContainerOptions) (client.CopyToContainerResult, error) {
_, _ = io.Copy(io.Discard, options.Content)
return client.CopyToContainerResult{}, nil
},
})
err := runCopy(context.TODO(), fakeCli, copyOptions{
source: srcDir.Path() + string(os.PathSeparator),
destination: "container:/tmp",
})
assert.NilError(t, err)
errOut := fakeCli.ErrBuffer().String()
assert.Check(t, is.Contains(errOut, "Successfully copied 6B"))
assert.Check(t, is.Contains(errOut, "(transferred"))
}
func TestCopyFromContainerReportsDirectorySize(t *testing.T) {
// When copying a directory from a container, cpRes.Stat.Mode.IsDir() is true,
// so reportedSize falls back to copiedSize (the tar stream bytes).
srcDir := fs.NewDir(t, "cp-test-fromdir",
fs.WithFile("file1", "hello"))
destDir := fs.NewDir(t, "cp-test-fromdir-dest")
fakeCli := test.NewFakeCli(&fakeClient{
containerCopyFromFunc: func(ctr, srcPath string) (client.CopyFromContainerResult, error) {
readCloser, err := archive.Tar(srcDir.Path(), compression.None)
return client.CopyFromContainerResult{
Content: readCloser,
Stat: container.PathStat{
Name: "mydir",
Mode: os.ModeDir | 0o755,
},
}, err
},
})
err := runCopy(context.TODO(), fakeCli, copyOptions{
source: "container:/mydir",
destination: destDir.Path(),
})
assert.NilError(t, err)
errOut := fakeCli.ErrBuffer().String()
assert.Check(t, is.Contains(errOut, "Successfully copied"))
// For directories from container, content size is unknown so
// reportedSize == copiedSize and "(transferred ...)" is omitted.
assert.Check(t, !strings.Contains(errOut, "(transferred"))
}
func TestCopyToContainerStdinReportsTransferredSize(t *testing.T) {
// When copying from stdin, content size is unknown.
// The message should report transferred bytes without "(transferred ...)".
r, w, _ := os.Pipe()
_, _ = w.WriteString("some data from stdin")
w.Close()
oldStdin := os.Stdin
os.Stdin = r
t.Cleanup(func() { os.Stdin = oldStdin })
fakeCli := test.NewFakeCli(&fakeClient{
containerStatPathFunc: func(containerID, path string) (client.ContainerStatPathResult, error) {
return client.ContainerStatPathResult{
Stat: container.PathStat{
Name: "tmp",
Mode: os.ModeDir | 0o755,
},
}, nil
},
containerCopyToFunc: func(containerID string, options client.CopyToContainerOptions) (client.CopyToContainerResult, error) {
_, _ = io.Copy(io.Discard, options.Content)
return client.CopyToContainerResult{}, nil
},
})
err := runCopy(context.TODO(), fakeCli, copyOptions{
source: "-",
destination: "container:/tmp",
})
assert.NilError(t, err)
errOut := fakeCli.ErrBuffer().String()
assert.Check(t, is.Contains(errOut, "Successfully copied"))
// stdin has no content size, so reportedSize == copiedSize and
// "(transferred ...)" should not appear.
assert.Check(t, !strings.Contains(errOut, "(transferred"))
}
+29 -22
View File
@@ -2,6 +2,7 @@ package container
import (
"strconv"
"strings"
"sync"
"github.com/docker/cli/cli/command/formatter"
@@ -111,31 +112,27 @@ func NewStatsFormat(source, osType string) formatter.Format {
return formatter.Format(source)
}
// NewStats returns a new Stats entity and sets in it the given name
func NewStats(container string) *Stats {
return &Stats{StatsEntry: StatsEntry{Container: container}}
// NewStats returns a new Stats entity using the given ID, ID-prefix, or
// name to resolve the container.
func NewStats(idOrName string) *Stats {
// FIXME(thaJeztah): "idOrName" is used for fuzzy-matching the container, which can result in multiple stats for the same container.
// We should resolve the canonical ID once, then use that as reference
// to prevent duplicates. Various parts in the code compare Container
// against "ID" only (not considering "name" or "ID-prefix").
return &Stats{StatsEntry: StatsEntry{Container: idOrName}}
}
// statsFormatWrite renders the context for a list of containers statistics
func statsFormatWrite(ctx formatter.Context, stats []StatsEntry, osType string, trunc bool) error {
render := func(format func(subContext formatter.SubContext) error) error {
for _, cstats := range stats {
statsCtx := &statsContext{
s: cstats,
os: osType,
trunc: trunc,
}
if err := format(statsCtx); err != nil {
return err
}
}
return nil
}
// TODO(thaJeztah): this should be taken from the (first) StatsEntry instead.
// also, assuming all stats are for the same platform (and basing the
// column headers on that) won't allow aggregated results, which could
// be mixed platform.
memUsage := memUseHeader
if osType == winOSType {
memUsage = winMemUseHeader
}
statsCtx := statsContext{}
statsCtx := statsContext{os: osType}
statsCtx.Header = formatter.SubHeaderContext{
"Container": containerHeader,
"Name": formatter.NameHeader,
@@ -147,8 +144,18 @@ func statsFormatWrite(ctx formatter.Context, stats []StatsEntry, osType string,
"BlockIO": blockIOHeader,
"PIDs": pidsHeader,
}
statsCtx.os = osType
return ctx.Write(&statsCtx, render)
return ctx.Write(&statsCtx, func(format func(subContext formatter.SubContext) error) error {
for _, cstats := range stats {
if err := format(&statsContext{
s: cstats,
os: osType,
trunc: trunc,
}); err != nil {
return err
}
}
return nil
})
}
type statsContext struct {
@@ -167,9 +174,9 @@ func (c *statsContext) Container() string {
}
func (c *statsContext) Name() string {
// TODO(thaJeztah): make this explicitly trim the "/" prefix, not just any char.
if len(c.s.Name) > 1 {
return c.s.Name[1:]
// Trim the "/" prefix (if present).
if name := strings.TrimPrefix(c.s.Name, "/"); name != "" {
return name
}
return noValue
}
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package container
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package container
+12 -3
View File
@@ -27,6 +27,11 @@ type rmOptions struct {
func newRmCommand(dockerCLI command.Cli) *cobra.Command {
var opts rmOptions
completeLinkNames := completeLinks(dockerCLI)
completeNames := completion.ContainerNames(dockerCLI, true, func(ctr container.Summary) bool {
return opts.force || ctr.State == container.StateExited || ctr.State == container.StateCreated
})
cmd := &cobra.Command{
Use: "rm [OPTIONS] CONTAINER [CONTAINER...]",
Short: "Remove one or more containers",
@@ -38,9 +43,13 @@ func newRmCommand(dockerCLI command.Cli) *cobra.Command {
Annotations: map[string]string{
"aliases": "docker container rm, docker container remove, docker rm",
},
ValidArgsFunction: completion.ContainerNames(dockerCLI, true, func(ctr container.Summary) bool {
return opts.force || ctr.State == container.StateExited || ctr.State == container.StateCreated
}),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if opts.rmLink {
// "--link" (remove link) is set; provide link names instead of container (primary) names.
return completeLinkNames(cmd, args, toComplete)
}
return completeNames(cmd, args, toComplete)
},
DisableFlagsInUseLine: true,
}
+67 -65
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package container
@@ -7,9 +7,7 @@ import (
"bytes"
"context"
"errors"
"fmt"
"io"
"strings"
"sync"
"time"
@@ -144,39 +142,32 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
}
eh := newEventHandler()
addEvents := []events.Action{events.ActionStart}
if options.All {
eh.setHandler(events.ActionCreate, func(e events.Message) {
if s := NewStats(e.Actor.ID); cStats.add(s) {
waitFirst.Add(1)
log.G(ctx).WithFields(log.Fields{
"event": e.Action,
"container": e.Actor.ID,
}).Debug("collecting stats for container")
go collect(ctx, s, apiClient, !options.NoStream, waitFirst)
}
})
addEvents = append(addEvents, events.ActionCreate)
}
eh.setHandler(events.ActionStart, func(e events.Message) {
eh.setHandler(addEvents, func(ctx context.Context, e events.Message) {
if s := NewStats(e.Actor.ID); cStats.add(s) {
waitFirst.Add(1)
log.G(ctx).WithFields(log.Fields{
"event": e.Action,
"container": e.Actor.ID,
}).Debug("collecting stats for container")
log.G(ctx).Debug("collecting stats for container")
go collect(ctx, s, apiClient, !options.NoStream, waitFirst)
}
})
// Remove containers when they are removed ("destroyed"); containers
// do not emit [events.ActionRemove], only [events.ActionDestroy].
//
// When running with "--all" we don't remove containers when they die,
// because they may come back, but without "--all" we remove them
// on the first possible occasion (either "die" or "destroy").
rmEvents := []events.Action{events.ActionDestroy}
if !options.All {
eh.setHandler(events.ActionDie, func(e events.Message) {
log.G(ctx).WithFields(log.Fields{
"event": e.Action,
"container": e.Actor.ID,
}).Debug("stop collecting stats for container")
cStats.remove(e.Actor.ID)
})
rmEvents = append(rmEvents, events.ActionDie)
}
eh.setHandler(rmEvents, func(ctx context.Context, e events.Message) {
log.G(ctx).Debug("stop collecting stats for container")
cStats.remove(e.Actor.ID)
})
// monitorContainerEvents watches for container creation and removal (only
// used when calling `docker stats` without arguments).
@@ -216,7 +207,7 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
}
eventChan := make(chan events.Message)
go eh.watch(eventChan)
go eh.watch(ctx, eventChan)
stopped := make(chan struct{})
go monitorContainerEvents(started, eventChan, stopped)
defer close(stopped)
@@ -294,30 +285,32 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
}
}
// Buffer to store formatted stats text.
// Once formatted, it will be printed in one write to avoid screen flickering.
var statsTextBuffer bytes.Buffer
// renderBuf holds the formatted stats output produced by statsFormatWrite.
// It does not include any terminal control sequences.
var renderBuf bytes.Buffer
// frameBuf holds the final terminal frame, including cursor movement and
// line-clearing escape sequences, written in a single pass to avoid flicker.
var frameBuf bytes.Buffer
statsCtx := formatter.Context{
Output: &statsTextBuffer,
Output: &renderBuf,
Format: NewStatsFormat(format, daemonOSType),
}
if options.NoStream {
cStats.mu.RLock()
ccStats := make([]StatsEntry, 0, len(cStats.cs))
for _, c := range cStats.cs {
ccStats = append(ccStats, c.GetStatistics())
}
cStats.mu.RUnlock()
if len(ccStats) == 0 {
statsList := cStats.snapshot()
if len(statsList) == 0 {
return nil
}
ccStats := make([]StatsEntry, 0, len(statsList))
for _, c := range statsList {
ccStats = append(ccStats, c.GetStatistics())
}
if err := statsFormatWrite(statsCtx, ccStats, daemonOSType, !options.NoTrunc); err != nil {
return err
}
_, _ = fmt.Fprint(dockerCLI.Out(), statsTextBuffer.String())
_, _ = dockerCLI.Out().Write(renderBuf.Bytes())
return nil
}
@@ -326,34 +319,38 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
for {
select {
case <-ticker.C:
cStats.mu.RLock()
ccStats := make([]StatsEntry, 0, len(cStats.cs))
for _, c := range cStats.cs {
renderBuf.Reset()
frameBuf.Reset()
statsList := cStats.snapshot()
if len(statsList) == 0 && !showAll {
// Clear screen
_, _ = io.WriteString(dockerCLI.Out(), "\033[H\033[J")
return nil
}
ccStats := make([]StatsEntry, 0, len(statsList))
for _, c := range statsList {
ccStats = append(ccStats, c.GetStatistics())
}
cStats.mu.RUnlock()
// Start by moving the cursor to the top-left
_, _ = fmt.Fprint(&statsTextBuffer, "\033[H")
if err := statsFormatWrite(statsCtx, ccStats, daemonOSType, !options.NoTrunc); err != nil {
return err
}
for line := range strings.SplitSeq(statsTextBuffer.String(), "\n") {
// Start by moving the cursor to the top-left
_, _ = io.WriteString(&frameBuf, "\033[H")
// TODO(thaJeztah): consider wrapping the writer to inject ANSI (line-clearing) during formatting.
// instead of post-processing the results.
for line := range bytes.SplitSeq(renderBuf.Bytes(), []byte{'\n'}) {
// In case the new text is shorter than the one we are writing over,
// we'll append the "erase line" escape sequence to clear the remaining text.
_, _ = fmt.Fprintln(&statsTextBuffer, line, "\033[K")
_, _ = frameBuf.Write(line)
_, _ = io.WriteString(&frameBuf, "\033[K")
_ = frameBuf.WriteByte('\n')
}
// We might have fewer containers than before, so let's clear the remaining text
_, _ = fmt.Fprint(&statsTextBuffer, "\033[J")
_, _ = fmt.Fprint(dockerCLI.Out(), statsTextBuffer.String())
statsTextBuffer.Reset()
if len(ccStats) == 0 && !showAll {
return nil
}
_, _ = io.WriteString(&frameBuf, "\033[J")
_, _ = dockerCLI.Out().Write(frameBuf.Bytes())
case err, ok := <-closeChan:
if !ok || err == nil || errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
// Suppress "unexpected EOF" errors in the CLI so that
@@ -369,33 +366,38 @@ func RunStats(ctx context.Context, dockerCLI command.Cli, options *StatsOptions)
// newEventHandler initializes and returns an eventHandler
func newEventHandler() *eventHandler {
return &eventHandler{handlers: make(map[events.Action]func(events.Message))}
return &eventHandler{handlers: make(map[events.Action]func(context.Context, events.Message))}
}
// eventHandler allows for registering specific events to setHandler.
type eventHandler struct {
handlers map[events.Action]func(events.Message)
handlers map[events.Action]func(context.Context, events.Message)
}
func (eh *eventHandler) setHandler(action events.Action, handler func(events.Message)) {
eh.handlers[action] = handler
func (eh *eventHandler) setHandler(actions []events.Action, handler func(context.Context, events.Message)) {
for _, action := range actions {
eh.handlers[action] = handler
}
}
// watch ranges over the passed in event chan and processes the events based on the
// handlers created for a given action.
// To stop watching, close the event chan.
func (eh *eventHandler) watch(c <-chan events.Message) {
func (eh *eventHandler) watch(ctx context.Context, c <-chan events.Message) {
for e := range c {
h, exists := eh.handlers[e.Action]
if !exists {
continue
}
if e.Actor.ID == "" {
log.G(context.TODO()).WithField("event", e).Errorf("event handler: received %s event with empty ID", e.Action)
log.G(ctx).WithField("event", e).Errorf("event handler: received %s event with empty ID", e.Action)
continue
}
logger := log.G(ctx).WithFields(log.Fields{
"event": e.Action,
"container": e.Actor.ID,
})
log.G(context.TODO()).WithField("event", e).Debugf("event handler: received %s event for: %s", e.Action, e.Actor.ID)
go h(e)
go h(log.WithLogger(ctx, logger), e)
}
}
+16
View File
@@ -49,6 +49,22 @@ func (s *stats) isKnownContainer(cid string) (int, bool) {
return -1, false
}
// snapshot returns a point-in-time copy of the tracked container list
// (the slice of *Stats pointers). The returned slice is safe for use
// without holding the stats lock, but the underlying Stats values may
// continue to change concurrently.
func (s *stats) snapshot() []*Stats {
s.mu.RLock()
defer s.mu.RUnlock()
if len(s.cs) == 0 {
return nil
}
// https://github.com/golang/go/issues/53643
cp := make([]*Stats, len(s.cs))
copy(cp, s.cs)
return cp
}
func collect(ctx context.Context, s *Stats, cli client.ContainerAPIClient, streamStats bool, waitFirst *sync.WaitGroup) { //nolint:gocyclo
var getFirst bool
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package container
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package command
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package context
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package context
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package context
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package context
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package context
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package command
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package command
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package command
+22 -11
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package formatter
@@ -141,25 +141,36 @@ func (c *ContainerContext) ID() string {
// Names returns a comma-separated string of the container's names, with their
// slash (/) prefix stripped. Additional names for the container (related to the
// legacy `--link` feature) are omitted.
// legacy `--link` feature) are omitted when formatting "truncated".
func (c *ContainerContext) Names() string {
names := StripNamePrefix(c.c.Names)
if c.trunc {
for _, name := range names {
if len(strings.Split(name, "/")) == 1 {
names = []string{name}
break
var b strings.Builder
for i, n := range c.c.Names {
name := strings.TrimPrefix(n, "/")
if c.trunc {
// When printing truncated, we only print a single name.
//
// Pick the first name that's not a legacy link (does not have
// slashes inside the name itself (e.g., "/other-container/link")).
// Normally this would be the first name found.
if strings.IndexByte(name, '/') == -1 {
return name
}
continue
}
if i > 0 {
b.WriteByte(',')
}
b.WriteString(name)
}
return strings.Join(names, ",")
return b.String()
}
// StripNamePrefix removes prefix from string, typically container names as returned by `ContainersList` API
// StripNamePrefix removes any "/" prefix from container names returned
// by the "ContainersList" API.
func StripNamePrefix(ss []string) []string {
sss := make([]string, len(ss))
for i, s := range ss {
sss[i] = s[1:]
sss[i] = strings.TrimPrefix(s, "/")
}
return sss
}
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package formatter
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package formatter
-3
View File
@@ -46,7 +46,6 @@ func (ctx *DiskUsageContext) startSubsection(format Format) (*template.Template,
ctx.buffer = &bytes.Buffer{}
ctx.header = ""
ctx.Format = format
ctx.preFormat()
return ctx.parseFormat()
}
@@ -88,7 +87,6 @@ func (ctx *DiskUsageContext) Write() (err error) {
return ctx.verboseWrite()
}
ctx.buffer = &bytes.Buffer{}
ctx.preFormat()
tmpl, err := ctx.parseFormat()
if err != nil {
@@ -213,7 +211,6 @@ func (ctx *DiskUsageContext) verboseWrite() error {
return ctx.verboseWriteTable(duc)
}
ctx.preFormat()
tmpl, err := ctx.parseFormat()
if err != nil {
return err
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package formatter
+43 -35
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package formatter
@@ -33,7 +33,7 @@ func (f Format) IsTable() bool {
return strings.HasPrefix(string(f), TableFormatKey)
}
// IsJSON returns true if the format is the json format
// IsJSON returns true if the format is the JSON format
func (f Format) IsJSON() bool {
return string(f) == JSONFormatKey
}
@@ -43,6 +43,31 @@ func (f Format) Contains(sub string) bool {
return strings.Contains(string(f), sub)
}
// templateString pre-processes the format and returns it as a string
// for templating.
func (f Format) templateString() string {
out := string(f)
switch out {
case TableFormatKey:
// A bare "--format table" should already be handled before we
// hit this; a literal "table" here means a custom "table" format
// without template.
return ""
case JSONFormatKey:
// "--format json" only; not JSON formats ("--format '{{json .Field}}'").
return JSONFormat
}
// "--format 'table {{.Field}}\t{{.Field}}'" -> "{{.Field}}\t{{.Field}}"
if after, isTable := strings.CutPrefix(out, TableFormatKey); isTable {
out = after
}
out = strings.Trim(out, " ") // trim spaces, but preserve other whitespace.
out = strings.NewReplacer(`\t`, "\t", `\n`, "\n").Replace(out)
return out
}
// Context contains information required by the formatter to print the output as desired.
type Context struct {
// Output is the output stream to which the formatted string is written.
@@ -53,28 +78,12 @@ type Context struct {
Trunc bool
// internal element
finalFormat string
header any
buffer *bytes.Buffer
}
func (c *Context) preFormat() {
c.finalFormat = string(c.Format)
// TODO: handle this in the Format type
switch {
case c.Format.IsTable():
c.finalFormat = c.finalFormat[len(TableFormatKey):]
case c.Format.IsJSON():
c.finalFormat = JSONFormat
}
c.finalFormat = strings.Trim(c.finalFormat, " ")
r := strings.NewReplacer(`\t`, "\t", `\n`, "\n")
c.finalFormat = r.Replace(c.finalFormat)
header any
buffer *bytes.Buffer
}
func (c *Context) parseFormat() (*template.Template, error) {
tmpl, err := templates.Parse(c.finalFormat)
tmpl, err := templates.Parse(c.Format.templateString())
if err != nil {
return nil, fmt.Errorf("template parsing error: %w", err)
}
@@ -82,20 +91,21 @@ func (c *Context) parseFormat() (*template.Template, error) {
}
func (c *Context) postFormat(tmpl *template.Template, subContext SubContext) {
if c.Output == nil {
c.Output = io.Discard
out := c.Output
if out == nil {
out = io.Discard
}
if c.Format.IsTable() {
t := tabwriter.NewWriter(c.Output, 10, 1, 3, ' ', 0)
buffer := bytes.NewBufferString("")
tmpl.Funcs(templates.HeaderFunctions).Execute(buffer, subContext.FullHeader())
buffer.WriteTo(t)
t.Write([]byte("\n"))
c.buffer.WriteTo(t)
t.Flush()
} else {
c.buffer.WriteTo(c.Output)
if !c.Format.IsTable() {
_, _ = c.buffer.WriteTo(out)
return
}
// Write column-headers and rows to the tab-writer buffer, then flush the output.
tw := tabwriter.NewWriter(out, 10, 1, 3, ' ', 0)
_ = tmpl.Funcs(templates.HeaderFunctions).Execute(tw, subContext.FullHeader())
_, _ = tw.Write([]byte{'\n'})
_, _ = c.buffer.WriteTo(tw)
_ = tw.Flush()
}
func (c *Context) contextFormat(tmpl *template.Template, subContext SubContext) error {
@@ -115,8 +125,6 @@ type SubFormat func(func(SubContext) error) error
// Write the template to the buffer using this Context
func (c *Context) Write(sub SubContext, f SubFormat) error {
c.buffer = &bytes.Buffer{}
c.preFormat()
tmpl, err := c.parseFormat()
if err != nil {
return err
+66 -11
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package formatter
@@ -8,20 +8,75 @@ import (
"testing"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestFormat(t *testing.T) {
f := Format("json")
assert.Assert(t, f.IsJSON())
assert.Assert(t, !f.IsTable())
tests := []struct {
doc string
f Format
isJSON bool
isTable bool
template string
}{
{
doc: "json format",
f: "json",
isJSON: true,
isTable: false,
template: JSONFormat,
},
{
doc: "empty table format (no template)",
f: "table",
isJSON: false,
isTable: true,
template: "",
},
{
doc: "table with escaped tabs",
f: "table {{.Field}}\\t{{.Field2}}",
isJSON: false,
isTable: true,
template: "{{.Field}}\t{{.Field2}}",
},
{
doc: "table with raw string",
f: `table {{.Field}}\t{{.Field2}}`,
isJSON: false,
isTable: true,
template: "{{.Field}}\t{{.Field2}}",
},
{
doc: "other format",
f: "other",
isJSON: false,
isTable: false,
template: "other",
},
{
doc: "other with spaces",
f: " other ",
isJSON: false,
isTable: false,
template: "other",
},
{
doc: "other with newline preserved",
f: " other\n ",
isJSON: false,
isTable: false,
template: "other\n",
},
}
f = Format("table")
assert.Assert(t, !f.IsJSON())
assert.Assert(t, f.IsTable())
f = Format("other")
assert.Assert(t, !f.IsJSON())
assert.Assert(t, !f.IsTable())
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
assert.Check(t, is.Equal(tc.f.IsJSON(), tc.isJSON))
assert.Check(t, is.Equal(tc.f.IsTable(), tc.isTable))
assert.Check(t, is.Equal(tc.f.templateString(), tc.template))
})
}
}
type fakeSubContext struct {
+2 -2
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package formatter
@@ -24,7 +24,7 @@ func MarshalJSON(x any) ([]byte, error) {
// marshalMap marshals x to map[string]any
func marshalMap(x any) (map[string]any, error) {
val := reflect.ValueOf(x)
if val.Kind() != reflect.Ptr {
if val.Kind() != reflect.Pointer {
return nil, fmt.Errorf("expected a pointer to a struct, got %v", val.Kind())
}
if val.IsNil() {
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package formatter
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package formatter
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package idresolver
+2 -16
View File
@@ -68,7 +68,7 @@ func TestGetContextFromLocalDirWithNoDirectory(t *testing.T) {
contextDir := createTestTempDir(t)
createTestTempFile(t, contextDir, defaultDockerfileName, dockerfileContents)
chdir(t, contextDir)
t.Chdir(contextDir)
absContextDir, relDockerfile, err := GetContextFromLocalDir(contextDir, "")
assert.NilError(t, err)
@@ -110,7 +110,7 @@ func TestGetContextFromLocalDirLocalFile(t *testing.T) {
func TestGetContextFromLocalDirWithCustomDockerfile(t *testing.T) {
contextDir := createTestTempDir(t)
chdir(t, contextDir)
t.Chdir(contextDir)
createTestTempFile(t, contextDir, defaultDockerfileName, dockerfileContents)
@@ -248,20 +248,6 @@ func createTestTempFile(t *testing.T, dir, filename, contents string) string {
return filePath
}
// chdir changes current working directory to dir.
// It returns a function which changes working directory back to the previous one.
// This function is meant to be executed as a deferred call.
// When an error occurs, it terminates the test.
func chdir(t *testing.T, dir string) {
t.Helper()
workingDirectory, err := os.Getwd()
assert.NilError(t, err)
assert.NilError(t, os.Chdir(dir))
t.Cleanup(func() {
assert.NilError(t, os.Chdir(workingDirectory))
})
}
func TestIsArchive(t *testing.T) {
tests := []struct {
doc string
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package image
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package image
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package image
+25 -17
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package image
@@ -206,30 +206,38 @@ var allChips = []imageChip{
chipInUse,
}
// getPossibleChips returns the list of chips used by at least one image.
// It is used to determine which columns to print (and how much width to
// reserve).
func getPossibleChips(view treeView) (chips []imageChip) {
remaining := make([]imageChip, len(allChips))
copy(remaining, allChips)
remaining := slices.Clone(allChips)
var possible []imageChip
for _, img := range view.images {
details := []imageDetails{img.Details}
for _, c := range img.Children {
details = append(details, c.Details)
check := func(d imageDetails) (done bool) {
// filter without allocating
out := remaining[:0]
for _, chip := range remaining {
if chip.check(&d) {
chips = append(chips, chip)
continue
}
out = append(out, chip)
}
remaining = out
return len(remaining) == 0
}
for _, d := range details {
for idx := len(remaining) - 1; idx >= 0; idx-- {
chip := remaining[idx]
if chip.check(&d) {
possible = append(possible, chip)
remaining = append(remaining[:idx], remaining[idx+1:]...)
}
for _, img := range view.images {
if check(img.Details) {
return chips
}
for _, c := range img.Children {
if check(c.Details) {
return chips
}
}
}
return possible
return chips
}
func printImageTree(outs command.Streams, view treeView) {
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package inspect
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package manifest
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package network
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package network
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package node
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package node
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package node
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package plugin
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package plugin
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package secret
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package service
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package service
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package service
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
// Package genericresource is a local fork of SwarmKit's [genericresource] package,
// without protobuf dependencies.
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package service
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package progress
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package service
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package stack
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package system
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package system
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package system
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
// Package pruner registers "prune" functions to be included as part of
// "docker system prune".
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package command
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package command
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package volume
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package volume
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package volume
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package convert
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package interpolation
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package interpolation
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package loader
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package loader
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package loader
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package loader
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package loader
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package loader
+1 -1
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.24
//go:build go1.25
package schema

Some files were not shown because too many files have changed in this diff Show More