Compare commits

...

53 Commits

Author SHA1 Message Date
980b856816 Merge pull request #6183 from thaJeztah/diff_simplify
Some checks failed
build / bin-image (push) Has been cancelled
build / prepare-plugins (push) Has been cancelled
build / plugins (push) Has been cancelled
codeql / codeql (push) Has been cancelled
e2e / tests (alpine, 23, connhelper-ssh) (push) Has been cancelled
e2e / tests (alpine, 23, local) (push) Has been cancelled
e2e / tests (alpine, 26, connhelper-ssh) (push) Has been cancelled
e2e / tests (alpine, 26, local) (push) Has been cancelled
e2e / tests (alpine, 27, connhelper-ssh) (push) Has been cancelled
e2e / tests (alpine, 27, local) (push) Has been cancelled
e2e / tests (alpine, 28, connhelper-ssh) (push) Has been cancelled
e2e / tests (alpine, 28, local) (push) Has been cancelled
e2e / tests (debian, 23, connhelper-ssh) (push) Has been cancelled
e2e / tests (debian, 23, local) (push) Has been cancelled
e2e / tests (debian, 26, connhelper-ssh) (push) Has been cancelled
e2e / tests (debian, 26, local) (push) Has been cancelled
e2e / tests (debian, 27, connhelper-ssh) (push) Has been cancelled
e2e / tests (debian, 27, local) (push) Has been cancelled
e2e / tests (debian, 28, connhelper-ssh) (push) Has been cancelled
e2e / tests (debian, 28, local) (push) Has been cancelled
test / ctn (push) Has been cancelled
test / host (macos-13) (push) Has been cancelled
test / host (macos-14) (push) Has been cancelled
validate / validate (lint) (push) Has been cancelled
validate / validate (shellcheck) (push) Has been cancelled
validate / validate (update-authors) (push) Has been cancelled
validate / validate (validate-vendor) (push) Has been cancelled
validate / validate-md (push) Has been cancelled
validate / validate-make (manpages) (push) Has been cancelled
validate / validate-make (yamldocs) (push) Has been cancelled
cli/command/container: diff: remove redundant validation and cleanup
2025-07-16 12:32:48 +02:00
9c256146ac Merge pull request #6181 from thaJeztah/fork_readCloserWrapper
remove uses of github.com/docker/docker/pkg/ioutils ReadCloserWrapper
2025-07-16 12:19:14 +02:00
bc01f8489d Merge pull request #6182 from thaJeztah/fork_longpath
remove use of github.com/docker/docker/pkg/longpath
2025-07-16 12:18:06 +02:00
ea2a0c3b8a Merge pull request #6177 from thaJeztah/rm_aliases
cli/command: remove some redundant import-aliases
2025-07-16 12:17:23 +02:00
3d985799d4 cli/command: remove some redundant import-aliases
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-07-16 12:12:41 +02:00
f5f3b027e8 Merge pull request #6178 from thaJeztah/bump_dev_tools
Dockerfile: update Buildx v0.25.0, compose v2.38.2
2025-07-16 12:07:46 +02:00
143f36133a Merge pull request #6179 from thaJeztah/bump_gotestsum
Dockerfile: bump gotest.tools/gotestsum v1.12.3 (for go1.25)
2025-07-16 12:07:17 +02:00
d7181e47e2 Merge pull request #6185 from thaJeztah/alpine_doc
Dockerfile: document ALPINE_VERSION build-arg
2025-07-16 12:06:17 +02:00
8b6436ecee Dockerfile: document ALPINE_VERSION build-arg
docker build --call outline .

    TARGET: binary

    BUILD ARG               VALUE    DESCRIPTION
    BASE_VARIANT            alpine
    ALPINE_VERSION          3.21     sets the version of the alpine base image to use, including for the golang image.
    GO_VERSION              1.24.5
    XX_VERSION              1.6.1
    GOVERSIONINFO_VERSION   v1.4.1
    GO_LINKMODE             static   defines if static or dynamic binary should be produced
    GO_BUILDTAGS                     defines additional build tags
    GO_STRIP                         strips debugging symbols if set
    CGO_ENABLED                      manually sets if cgo is used
    VERSION                          sets the version for the produced binary
    PACKAGER_NAME                    sets the company that produced the windows binary

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-07-16 09:51:14 +02:00
7d574b816d Merge pull request #6180 from thaJeztah/truncate_id
remove uses of github.com/docker/docker/pkg/stringid
2025-07-15 14:03:18 +02:00
0f2b709c7c cli/command/container: diff: remove redundant validation and cleanup
client.ContainerDiff already validates the given container name/ID, and
produces an error when empty, so we don't have to check for this;
abba330bbf/client/container_diff.go (L13-L16)

While updating, also;

- remove the diffOptions type, as there were no other options, and make
  the container name/ID a string argument.
- fix camelCase nameing of dockerCLI

Before this patch:

    docker diff ""
    Container name cannot be empty

With this patch:

    docker diff ""
    invalid container name or ID: value is empty

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-07-15 01:08:29 +02:00
7668b683d2 Merge pull request #6176 from thaJeztah/rm_use_AllowOverwriteDirWithFile
cli/command/container: don't set CopyToContainerOptions.AllowOverwriteDirWithFile
2025-07-14 23:17:34 +02:00
53d02ece89 remove use of github.com/docker/docker/pkg/longpath
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-07-14 22:34:36 +02:00
3600ebca76 remove uses of github.com/docker/docker/pkg/ioutils ReadCloserWrapper
It was the only utility we consumed from the package, and it's trivial
to implement, so let's create local copies of it.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-07-14 22:09:31 +02:00
9b047a501f remove uses of pkg/stringid.GenerateRandomID()
This utility was only used for testing, and to generate a random
suffix for Dockerfiles. As we don't need the same contract as
pkg/stringid.GenerateRandomID() (not allow all-numeric IDs as they
would not be usable for hostnames), we can use a local test-utility,
and local implementation for the random suffix instead.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-07-14 20:11:07 +02:00
e0f4bc699c cli/command/formatter: add TrunateID utility
We were depending on pkg/stringid to truncate IDs for presentation. While
traditionally, we used a fixed length for "truncated" IDs, this is not
a strict requirement (any ID-prefix should work, but conflicts may
happen on shorter IDs).

This patch adds a local `TruncateID()` utility in the formatter package;
it's currently using the same implementation and length as the
`stringid.TruncateID` function, but may diverge in future.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-07-14 20:10:56 +02:00
1264a59779 Dockerfile: bump gotest.tools/gotestsum v1.12.3 (for go1.25)
full diff: https://github.com/gotestyourself/gotestsum/compare/v1.12.0...v1.12.3

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-07-13 17:50:03 +02:00
e6b8cc1c7d Dockerfile: update buildx to v0.25.0
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-07-13 17:45:07 +02:00
50fa436c21 Dockerfile: update compose to v2.38.2
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-07-13 17:43:50 +02:00
0be687acc0 cli/command/container: don't set CopyToContainerOptions.AllowOverwriteDirWithFile
The `AllowOverwriteDirWithFile` option was added when reimplementing the
CLI using the API Client lib in [moby@1b2b91b]. Before that refactor, the
`noOverwriteDirNonDir` query argument [would be set unconditionally][1]
by the CLI, with no options to control the behavior.

It's unclear why the `noOverwriteDirNonDir` was implemented as opt-in (not
opt-out), as overwriting a file with a directory (or vice-versa) would
generally be unexpected behavior.

We're considering making `noOverwriteDirNonDir` unconditional on the daemon
side, and to deprecate the `AllowOverwriteDirWithFile` option. This patch
removes its use, as it was set to the default either way, and there's no
options to configure it from the CLI.

[1]: 8c9ad7b818/api/client/cp.go (L345-L346)
[moby@1b2b91b]: 1b2b91ba43

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-07-13 13:48:59 +02:00
c69d8bde4a Merge pull request #6173 from vvoland/fix-anchor-cdi
docs: fix CDI device configuration anchor
2025-07-11 15:27:46 +02:00
8eac03d5fa docs: fix CDI device configuration anchor
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2025-07-11 10:44:29 +02:00
578ccf607d Merge pull request #6170 from thaJeztah/e2e_newline_check
Some checks failed
build / bin-image (push) Has been cancelled
build / prepare-plugins (push) Has been cancelled
build / plugins (push) Has been cancelled
codeql / codeql (push) Has been cancelled
e2e / tests (alpine, 23, connhelper-ssh) (push) Has been cancelled
e2e / tests (alpine, 23, local) (push) Has been cancelled
e2e / tests (alpine, 26, connhelper-ssh) (push) Has been cancelled
e2e / tests (alpine, 26, local) (push) Has been cancelled
e2e / tests (alpine, 27, connhelper-ssh) (push) Has been cancelled
e2e / tests (alpine, 27, local) (push) Has been cancelled
e2e / tests (alpine, 28, connhelper-ssh) (push) Has been cancelled
e2e / tests (alpine, 28, local) (push) Has been cancelled
e2e / tests (debian, 23, connhelper-ssh) (push) Has been cancelled
e2e / tests (debian, 23, local) (push) Has been cancelled
e2e / tests (debian, 26, connhelper-ssh) (push) Has been cancelled
e2e / tests (debian, 26, local) (push) Has been cancelled
e2e / tests (debian, 27, connhelper-ssh) (push) Has been cancelled
e2e / tests (debian, 27, local) (push) Has been cancelled
e2e / tests (debian, 28, connhelper-ssh) (push) Has been cancelled
e2e / tests (debian, 28, local) (push) Has been cancelled
test / ctn (push) Has been cancelled
test / host (macos-13) (push) Has been cancelled
test / host (macos-14) (push) Has been cancelled
validate / validate (lint) (push) Has been cancelled
validate / validate (shellcheck) (push) Has been cancelled
validate / validate (update-authors) (push) Has been cancelled
validate / validate (validate-vendor) (push) Has been cancelled
validate / validate-md (push) Has been cancelled
validate / validate-make (manpages) (push) Has been cancelled
validate / validate-make (yamldocs) (push) Has been cancelled
e2e/global: TestPromptExitCode: check for trailing newline
2025-07-09 14:04:02 +02:00
0c5e258f8a e2e/global: TestPromptExitCode: check for trailing newline
Make the test slightly more permissive; we're looking for a trailing
newline, not necessarily an empty line.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-07-09 13:35:17 +02:00
30cad385b6 Merge pull request #6167 from vvoland/update-go
Update to go1.24.5
2025-07-09 01:23:29 +02:00
9bcc88611f update to go1.24.5
- https://github.com/golang/go/issues?q=milestone%3AGo1.24.5+label%3ACherryPickApproved
- full diff: https://github.com/golang/go/compare/go1.24.4...go1.24.5

This minor releases include 1 security fixes following the security policy:

- cmd/go: unexpected command execution in untrusted VCS repositories

    Various uses of the Go toolchain in untrusted VCS repositories can result in
    unexpected code execution. When using the Go toolchain in directories fetched
    using various VCS tools (such as directly cloning Git or Mercurial repositories)
    can cause the toolchain to execute unexpected commands, if said directory
    contains multiple VCS configuration metadata (such as a '.hg' directory in a Git
    repository). This is due to how the Go toolchain attempts to resolve which VCS
    is being used in order to embed build information in binaries and determine
    module versions.

    The toolchain will now abort attempting to resolve which VCS is being used if it
    detects multiple VCS configuration metadata in a module directory or nested VCS
    configuration metadata (such as a '.git' directoy in a parent directory and a
    '.hg' directory in a child directory). This will not prevent the toolchain from
    building modules, but will result in binaries omitting VCS related build
    information.

    If this behavior is expected by the user, the old behavior can be re-enabled by
    setting GODEBUG=allowmultiplevcs=1. This should only be done in trusted
    repositories.

    Thanks to RyotaK (https://ryotak.net) of GMO Flatt Security Inc for reporting
    this issue.

    This is CVE-2025-4674 and https://go.dev/issue/74380.

View the release notes for more information:
https://go.dev/doc/devel/release#go1.24.5

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2025-07-08 19:23:57 +02:00
3302212263 Merge pull request #6163 from Benehiko/env-credential-warn
registry: warn of `DOCKER_AUTH_CONFIG` usage in login and logout
2025-07-08 15:33:07 +02:00
ccd5bd8d57 registry: warn of DOCKER_AUTH_CONFIG usage in login and logout
Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
2025-07-08 14:07:32 +02:00
dec07e6fdf tui/note: add warning note type
Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
2025-07-08 14:07:22 +02:00
28f19a9d65 Merge pull request #6162 from ArthurFlag/ENGDOCS-2807-cdi-docs-update
docs: cdi isn't experimental
2025-07-07 17:53:48 +02:00
219e5ca4f2 Merge pull request #6165 from thaJeztah/bump_engine_28.3.1
vendor: github.com/docker/docker v28.3.1
2025-07-07 17:53:31 +02:00
7e040d91ef docs: cdi is not experimental anymore
Signed-off-by: ArthurFlag <arthur.flageul@docker.com>
2025-07-07 16:10:30 +02:00
76524e7d0e vendor: github.com/docker/docker v28.3.1
no changes in vendored code

full diff: https://github.com/docker/docker/compare/v28.3.0...v28.3.1

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-07-07 15:52:29 +02:00
3262107821 cli/config: export const dockerEnvConfig
Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
2025-07-04 14:04:38 +02:00
8403869122 Merge pull request #6158 from thaJeztah/reduce_strslice
cli/command/container: remove redundant uses of strslice.StrSlice
2025-07-02 17:42:43 +02:00
1fc7194554 Merge pull request #6159 from thaJeztah/hide_codecov
rename codecov.yml to .codecov.yml
2025-07-02 17:42:11 +02:00
fa2a7f1536 Merge pull request #6154 from thaJeztah/bump_engine
vendor: github.com/docker/docker v28.3.0
2025-07-02 17:41:49 +02:00
350b3a6e25 Merge pull request #6160 from thaJeztah/fix_otel_debug_logs
cli/debug: fix OTELErrorHandler logging messages if there's no error
2025-07-02 17:41:26 +02:00
4ea6fbf538 cli/debug: fix OTELErrorHandler logging messages if there's no error
I noticed this in a ticket in the compose issue tracker; with debug logging
enabled, the OTEL error-logger may be logging even if there's no error;

    DEBU[0000] Executing bake with args: [bake --file - --progress rawjson --metadata-file /tmp/compose-build-metadataFile-1203980021.json --allow fs.read=/home/user/dev/project --allow fs.read=/home/user/dev/project --allow fs.read=/home/user/dev/project/nginx --allow fs.read=/home/user/dev/project]
    TRAC[0000] Plugin server listening on @docker_cli_d8df486f78df3b7357995be71bf0cef6
    DEBU[0005] otel error                                    error="<nil>"
    ^CTRAC[0055] Closing plugin server
    TRAC[0055] Closing plugin server
    DEBU[0055] otel error                                    error="<nil>"
    DEBU[0055] otel error                                    error="<nil>"

Update the error-handler to not log if there's no error.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-07-02 11:59:05 +02:00
74a896f18c Merge pull request #6157 from ndeloof/use_api_socket
mount /var/run/docker.sock for --use-api-socket
2025-07-01 17:00:24 +02:00
94f097da28 rename codecov.yml to .codecov.yml
Make it a hidden file. From the [CodeCov docs][1]:

> Can I name the file .codecov.yml?
>
> Yes, you can name the file `codecov.yml` or `.codecov.yml`. However, the
> file must still be located in the repository root, `dev/`, or `.github/`
> directories

[1]: https://docs.codecov.com/docs/codecov-yaml#can-i-name-the-file-codecovyml

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-07-01 16:20:54 +02:00
e7e238eb4b cli/command/container: remove redundant uses of strslice.StrSlice
The strslice.StrSlice type is a string-slice with a custom JSON Unmarshal
function to provide backward-compatibility with older API requests (see
[moby@17d6f00] and [moby@ea4a067]).

Given that the type is assigned implicitly through the fields on HostConfig,
we can just use a regular []string instead.

[moby@17d6f00]: 17d6f00ec2
[moby@ea4a067]: ea4a06740b

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-07-01 10:09:54 +02:00
2ba7cb8b44 mount /var/run/docker.sock for --use-api-socket
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
2025-06-30 12:38:15 +02:00
52e1e4fb21 vendor: github.com/docker/docker v28.3.0
no diff; same commit: https://github.com/docker/docker/compare/v28.3.0-rc.2...v28.3.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-06-30 09:35:21 +02:00
7cbee73f19 Merge pull request #6147 from thaJeztah/connhelper_quote
cli/connhelper: quote ssh arguments to prevent shell injection
2025-06-25 17:21:12 +02:00
ae6f8d0021 Merge pull request #6149 from vvoland/gha-tags
gha/bin-image: add major and minor version image tags
2025-06-25 14:35:11 +00:00
70867e7067 gha/bin-image: add major and minor version image tags
Adding image tags that follow the semver major and minor versions (e.g., `28`
and `28.3`) for the moby-bin images.

This makes it easier for users to reference the latest build within a
major or minor version series without having to know the exact
minor/patch version.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2025-06-24 23:35:10 +02:00
38b7060a21 Merge pull request #6148 from thaJeztah/vendor_rc2
Some checks failed
build / bin-image (push) Has been cancelled
build / prepare-plugins (push) Has been cancelled
build / plugins (push) Has been cancelled
codeql / codeql (push) Has been cancelled
e2e / tests (alpine, 23, connhelper-ssh) (push) Has been cancelled
e2e / tests (alpine, 23, local) (push) Has been cancelled
e2e / tests (alpine, 26, connhelper-ssh) (push) Has been cancelled
e2e / tests (alpine, 26, local) (push) Has been cancelled
e2e / tests (alpine, 27, connhelper-ssh) (push) Has been cancelled
e2e / tests (alpine, 27, local) (push) Has been cancelled
e2e / tests (alpine, 28, connhelper-ssh) (push) Has been cancelled
e2e / tests (alpine, 28, local) (push) Has been cancelled
e2e / tests (debian, 23, connhelper-ssh) (push) Has been cancelled
e2e / tests (debian, 23, local) (push) Has been cancelled
e2e / tests (debian, 26, connhelper-ssh) (push) Has been cancelled
e2e / tests (debian, 26, local) (push) Has been cancelled
e2e / tests (debian, 27, connhelper-ssh) (push) Has been cancelled
e2e / tests (debian, 27, local) (push) Has been cancelled
e2e / tests (debian, 28, connhelper-ssh) (push) Has been cancelled
e2e / tests (debian, 28, local) (push) Has been cancelled
test / ctn (push) Has been cancelled
test / host (macos-13) (push) Has been cancelled
test / host (macos-14) (push) Has been cancelled
validate / validate (lint) (push) Has been cancelled
validate / validate (shellcheck) (push) Has been cancelled
validate / validate (update-authors) (push) Has been cancelled
validate / validate (validate-vendor) (push) Has been cancelled
validate / validate-md (push) Has been cancelled
validate / validate-make (manpages) (push) Has been cancelled
validate / validate-make (yamldocs) (push) Has been cancelled
vendor: github.com/docker/docker v28.3.0-rc.2
2025-06-24 15:37:19 +00:00
2d46d162c1 vendor: github.com/docker/docker v28.3.0-rc.2
no diff; same commit, but tagged;
https://github.com/docker/docker/compare/265f70964794...v28.3.0-rc.2

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-06-24 16:35:29 +02:00
88d1133224 cli/connhelper: quote ssh arguments to prevent shell injection
When connecting to a remote daemon through an ssh:// connection,
the CLI connects with the remote host using ssh, executing the
`docker system dial-stdio` command on the remote host to connect
to the daemon API's unix socket.

By default, the `docker system dial-stdio` command connects with the
daemon using the default location (/var/run/docker.sock), or the
location as configured on the remote host.

Commit 25ebf0ec9c (included in docker
CLI v24.0.0-rc.2 and higher) introduced a feature to allow the location
of the socket to be specified through the host connection string, for
example:

     DOCKER_HOST='ssh://example.test/run/custom-docker.sock'

The custom path is included as part of the ssh command executed from
the client machine to connect with the remote host. THe example above
would execute the following command from the client machine;

    ssh -o ConnectTimeout=30 -T -- example.test docker --host unix:///run/custom-docker.sock system dial-stdio

ssh executes remote commands in a shell environment, and no quoting
was in place, which allowed for a connection string to include additional
content, which would be expanded / executed on the remote machine.

For example, the following example would execute `echo hello > /hello.txt`
on the remote machine;

    export DOCKER_HOST='ssh://example.test/var/run/docker.sock $(echo hello > /hello.txt)'
    docker info
    # (output of docker info from the remote machine)

While this doesn't allow the user to do anything they're not already
able to do so (by directly using the same SSH connection), the behavior
is not expected, so this patch adds quoting to prevent such URLs from
resulting in expansion.

This patch updates the cli/connhelper and cli/connhelper/ssh package to
quote parameters used in the ssh command to prevent code execution and
expansion of variables on the remote machine. Quoting is also applied to
other parameters that are obtained from the DOCKER_HOST url, such as username
and hostname.

- The existing `Spec.Args()` method inthe cli/connhelper/ssh package now
  quotes arguments, and returns a nil slice when failing to quote. Users
  of this package should therefore check the returned arguments before
  consuming. This  method did not provide an error-return, and adding
  one would be a breaking change.
- A new `Spec.Command` method is introduced, which (unlike the `Spec.Args()`
  method) provides an error return. Users are recommended to use this new
  method instead of the `Spec.Args()` method.

Some minor additional changes in behavior are included in this patch;

- Connection URLs with a trailing slash (e.g. `ssh://example.test/`)
  would previously result in `unix:///` being used as custom socket
  path. After this patch, the trailing slash is ignored, and no custom
  socket path is used.
- Specifying a remote command is now required. When passing an empty
  remote command, `Spec.Args()` now results in a `nil` value to be
  returned (or an `no remote command specified` error when using
  `Spec.Comnmand()`.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-06-24 16:26:17 +02:00
82eda48066 cli/connhelper/internal/syntax: fix linting issues
cli/connhelper/internal/syntax/parser.go:31:2: Duplicate words (the) found (dupword)
        // Note that it shares some features with Bash, due to the the shared
        ^
    cli/connhelper/internal/syntax/quote.go:48:1: cyclomatic complexity 35 of func `Quote` is high (> 16) (gocyclo)
    func Quote(s string, lang LangVariant) (string, error) {
    ^
    cli/connhelper/internal/syntax/quote.go:103:3: shadow: declaration of "offs" shadows declaration at line 56 (govet)
            offs := 0
            ^

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-06-24 16:24:34 +02:00
52d2a9b5ae cli/connhelper/internal/syntax: remove unused code from fork
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-06-24 16:24:29 +02:00
64a9a6d0c8 cli/connhelper: add fork of mvdan.cc/sh/v3/syntax v3.10.0
This adds a local fork of the mvdan.cc/sh/v3/syntax package to provide the
Quote function without having to introduce additional (indirect) dependencies
of the mvdan.cc/sh module.

This commit does not compile as it references code not forked.

The following files were included:

- https://raw.githubusercontent.com/mvdan/sh/refs/tags/v3.10.0/syntax/quote.go
- https://raw.githubusercontent.com/mvdan/sh/refs/tags/v3.10.0/syntax/parser.go
- https://raw.githubusercontent.com/mvdan/sh/refs/tags/v3.10.0/LICENSE

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2025-06-24 10:02:53 +02:00
78 changed files with 988 additions and 576 deletions

View File

@ -121,6 +121,8 @@ jobs:
type=semver,pattern={{version}}
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}}
-
name: Build and push image
uses: docker/bake-action@v6

View File

@ -63,7 +63,7 @@ jobs:
name: Update Go
uses: actions/setup-go@v5
with:
go-version: "1.24.4"
go-version: "1.24.5"
-
name: Initialize CodeQL
uses: github/codeql-action/init@v3

View File

@ -66,7 +66,7 @@ jobs:
name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.24.4"
go-version: "1.24.5"
-
name: Test
run: |

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.24.4"
go: "1.24.5"
timeout: 5m

View File

@ -1,19 +1,30 @@
# syntax=docker/dockerfile:1
ARG BASE_VARIANT=alpine
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
# It must be a supported tag in the docker.io/library/alpine image repository
# that's also available as alpine image variant for the Golang version used.
ARG ALPINE_VERSION=3.21
ARG BASE_DEBIAN_DISTRO=bookworm
ARG GO_VERSION=1.24.4
ARG GO_VERSION=1.24.5
ARG XX_VERSION=1.6.1
ARG GOVERSIONINFO_VERSION=v1.4.1
ARG GOTESTSUM_VERSION=v1.12.0
# GOTESTSUM_VERSION sets the version of gotestsum to install in the dev container.
# It must be a valid tag in the https://github.com/gotestyourself/gotestsum repository.
ARG GOTESTSUM_VERSION=v1.12.3
# BUILDX_VERSION sets the version of buildx to use for the e2e tests.
# It must be a tag in the docker.io/docker/buildx-bin image repository
# on Docker Hub.
ARG BUILDX_VERSION=0.24.0
ARG COMPOSE_VERSION=v2.36.2
ARG BUILDX_VERSION=0.25.0
# COMPOSE_VERSION is the version of compose to install in the dev container.
# It must be a tag in the docker.io/docker/compose-bin image repository
# on Docker Hub.
ARG COMPOSE_VERSION=v2.38.2
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx

View File

@ -8,7 +8,7 @@ import (
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/command/inspect"
"github.com/docker/docker/api/types/swarm"
units "github.com/docker/go-units"
"github.com/docker/go-units"
)
const (

View File

@ -16,7 +16,7 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/streams"
"github.com/docker/docker/api/types/container"
units "github.com/docker/go-units"
"github.com/docker/go-units"
"github.com/moby/go-archive"
"github.com/morikuni/aec"
"github.com/pkg/errors"
@ -398,8 +398,7 @@ func copyToContainer(ctx context.Context, dockerCLI command.Cli, copyConfig cpCo
}
options := container.CopyToContainerOptions{
AllowOverwriteDirWithFile: false,
CopyUIDGID: copyConfig.copyUIDGID,
CopyUIDGID: copyConfig.copyUIDGID,
}
if copyConfig.quiet {

View File

@ -248,15 +248,14 @@ func createContainer(ctx context.Context, dockerCli command.Cli, containerCfg *c
// 1. Mount the actual docker socket.
// 2. A synthezised ~/.docker/config.json with resolved tokens.
socket := dockerCli.DockerEndpoint().Host
if !strings.HasPrefix(socket, "unix://") {
return "", fmt.Errorf("flag --use-api-socket can only be used with unix sockets: docker endpoint %s incompatible", socket)
if dockerCli.ServerInfo().OSType == "windows" {
return "", errors.New("flag --use-api-socket can't be used with a Windows Docker Engine")
}
socket = strings.TrimPrefix(socket, "unix://") // should we confirm absolute path?
// hard-code engine socket path until https://github.com/moby/moby/pull/43459 gives us a discovery mechanism
containerCfg.HostConfig.Mounts = append(containerCfg.HostConfig.Mounts, mount.Mount{
Type: mount.TypeBind,
Source: socket,
Source: "/var/run/docker.sock",
Target: "/var/run/docker.sock",
BindOptions: &mount.BindOptions{},
})

View File

@ -7,25 +7,17 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/command/formatter"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
type diffOptions struct {
container string
}
// NewDiffCommand creates a new cobra.Command for `docker diff`
func NewDiffCommand(dockerCli command.Cli) *cobra.Command {
var opts diffOptions
return &cobra.Command{
Use: "diff CONTAINER",
Short: "Inspect changes to files or directories on a container's filesystem",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts.container = args[0]
return runDiff(cmd.Context(), dockerCli, &opts)
return runDiff(cmd.Context(), dockerCli, args[0])
},
Annotations: map[string]string{
"aliases": "docker container diff, docker diff",
@ -34,16 +26,13 @@ func NewDiffCommand(dockerCli command.Cli) *cobra.Command {
}
}
func runDiff(ctx context.Context, dockerCli command.Cli, opts *diffOptions) error {
if opts.container == "" {
return errors.New("Container name cannot be empty")
}
changes, err := dockerCli.Client().ContainerDiff(ctx, opts.container)
func runDiff(ctx context.Context, dockerCLI command.Cli, containerID string) error {
changes, err := dockerCLI.Client().ContainerDiff(ctx, containerID)
if err != nil {
return err
}
diffCtx := formatter.Context{
Output: dockerCli.Out(),
Output: dockerCLI.Out(),
Format: NewDiffFormat("{{.Type}} {{.Path}}"),
}
return DiffFormatWrite(diffCtx, changes)

View File

@ -77,17 +77,3 @@ func TestRunDiffClientError(t *testing.T) {
err := cmd.Execute()
assert.ErrorIs(t, err, clientError)
}
func TestRunDiffEmptyContainerError(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cmd := NewDiffCommand(cli)
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
containerID := ""
cmd.SetArgs([]string{containerID})
err := cmd.Execute()
assert.Error(t, err, "Container name cannot be empty")
}

View File

@ -5,8 +5,7 @@ import (
"sync"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/pkg/stringid"
units "github.com/docker/go-units"
"github.com/docker/go-units"
)
const (
@ -176,7 +175,7 @@ func (c *statsContext) Name() string {
func (c *statsContext) ID() string {
if c.trunc {
return stringid.TruncateID(c.s.ID)
return formatter.TruncateID(c.s.ID)
}
return c.s.ID
}

View File

@ -5,13 +5,13 @@ import (
"testing"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/cli/internal/test"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestContainerStatsContext(t *testing.T) {
containerID := stringid.GenerateRandomID()
containerID := test.RandomID()
var ctx statsContext
tt := []struct {

View File

@ -9,7 +9,6 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/ioutils"
"github.com/docker/docker/pkg/stdcopy"
"github.com/moby/term"
"github.com/sirupsen/logrus"
@ -19,6 +18,18 @@ import (
// TODO: This could be moved to `pkg/term`.
var defaultEscapeKeys = []byte{16, 17}
// readCloserWrapper wraps an io.Reader, and implements an io.ReadCloser
// It calls the given callback function when closed.
type readCloserWrapper struct {
io.Reader
closer func() error
}
// Close calls back the passed closer function
func (r *readCloserWrapper) Close() error {
return r.closer()
}
// A hijackedIOStreamer handles copying input to and output from streams to the
// connection.
type hijackedIOStreamer struct {
@ -100,7 +111,10 @@ func (h *hijackedIOStreamer) setupInput() (restore func(), err error) {
}
}
h.inputStream = ioutils.NewReadCloserWrapper(term.NewEscapeProxy(h.inputStream, escapeKeys), h.inputStream.Close)
h.inputStream = &readCloserWrapper{
Reader: term.NewEscapeProxy(h.inputStream, escapeKeys),
closer: h.inputStream.Close,
}
return restore, nil
}

View File

@ -18,7 +18,6 @@ import (
"github.com/docker/docker/api/types/container"
mounttypes "github.com/docker/docker/api/types/mount"
networktypes "github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/strslice"
"github.com/docker/go-connections/nat"
"github.com/pkg/errors"
"github.com/spf13/pflag"
@ -400,17 +399,14 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
tmpfs[k] = v
}
var (
runCmd strslice.StrSlice
entrypoint strslice.StrSlice
)
var runCmd, entrypoint []string
if len(copts.Args) > 0 {
runCmd = copts.Args
}
if copts.entrypoint != "" {
entrypoint = strslice.StrSlice{copts.entrypoint}
entrypoint = []string{copts.entrypoint}
} else if flags.Changed("entrypoint") {
// if `--entrypoint=` is parsed then Entrypoint is reset
entrypoint = []string{""}
@ -551,9 +547,9 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
if haveHealthSettings {
return nil, errors.Errorf("--no-healthcheck conflicts with --health-* options")
}
healthConfig = &container.HealthConfig{Test: strslice.StrSlice{"NONE"}}
healthConfig = &container.HealthConfig{Test: []string{"NONE"}}
} else if haveHealthSettings {
var probe strslice.StrSlice
var probe []string
if copts.healthCmd != "" {
probe = []string{"CMD-SHELL", copts.healthCmd}
}
@ -675,8 +671,8 @@ func parse(flags *pflag.FlagSet, copts *containerOptions, serverOS string) (*con
UTSMode: utsMode,
UsernsMode: usernsMode,
CgroupnsMode: cgroupnsMode,
CapAdd: strslice.StrSlice(copts.capAdd.GetSlice()),
CapDrop: strslice.StrSlice(copts.capDrop.GetSlice()),
CapAdd: copts.capAdd.GetSlice(),
CapDrop: copts.capDrop.GetSlice(),
GroupAdd: copts.groupAdd.GetSlice(),
RestartPolicy: restartPolicy,
SecurityOpt: securityOpts,

View File

@ -9,7 +9,7 @@ import (
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/internal/prompt"
"github.com/docker/cli/opts"
units "github.com/docker/go-units"
"github.com/docker/go-units"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)

View File

@ -7,7 +7,6 @@ import (
"time"
"github.com/docker/docker/api/types/build"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
)
@ -115,7 +114,7 @@ func (c *buildCacheContext) MarshalJSON() ([]byte, error) {
func (c *buildCacheContext) ID() string {
id := c.v.ID
if c.trunc {
id = stringid.TruncateID(c.v.ID)
id = TruncateID(c.v.ID)
}
if c.v.InUse {
return id + "*"
@ -131,7 +130,7 @@ func (c *buildCacheContext) Parent() string {
parent = c.v.Parent //nolint:staticcheck // Ignore SA1019: Field was deprecated in API v1.42, but kept for backward compatibility
}
if c.trunc {
return stringid.TruncateID(parent)
return TruncateID(parent)
}
return parent
}

View File

@ -14,7 +14,6 @@ import (
"github.com/containerd/platforms"
"github.com/distribution/reference"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
@ -135,7 +134,7 @@ func (c *ContainerContext) MarshalJSON() ([]byte, error) {
// option being set, the full or truncated ID is returned.
func (c *ContainerContext) ID() string {
if c.trunc {
return stringid.TruncateID(c.c.ID)
return TruncateID(c.c.ID)
}
return c.c.ID
}
@ -172,7 +171,7 @@ func (c *ContainerContext) Image() string {
return "<no image>"
}
if c.trunc {
if trunc := stringid.TruncateID(c.c.ImageID); trunc == stringid.TruncateID(c.c.Image) {
if trunc := TruncateID(c.c.ImageID); trunc == TruncateID(c.c.Image) {
return trunc
}
// truncate digest if no-trunc option was not selected

View File

@ -13,7 +13,6 @@ import (
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/pkg/stringid"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
@ -21,7 +20,7 @@ import (
)
func TestContainerPsContext(t *testing.T) {
containerID := stringid.GenerateRandomID()
containerID := test.RandomID()
unix := time.Now().Add(-65 * time.Second).Unix()
var ctx ContainerContext
@ -34,7 +33,7 @@ func TestContainerPsContext(t *testing.T) {
{
container: container.Summary{ID: containerID},
trunc: true,
expValue: stringid.TruncateID(containerID),
expValue: TruncateID(containerID),
call: ctx.ID,
},
{

View File

@ -11,7 +11,7 @@ import (
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/volume"
units "github.com/docker/go-units"
"github.com/docker/go-units"
)
const (

View File

@ -27,6 +27,25 @@ func charWidth(r rune) int {
}
}
const shortLen = 12
// TruncateID returns a shorthand version of a string identifier for presentation,
// after trimming digest algorithm prefix (if any).
//
// This function is a copy of [stringid.TruncateID] for presentation / formatting
// purposes.
//
// [stringid.TruncateID]: https://github.com/moby/moby/blob/v28.3.2/pkg/stringid/stringid.go#L19
func TruncateID(id string) string {
if i := strings.IndexRune(id, ':'); i >= 0 {
id = id[i+1:]
}
if len(id) > shortLen {
id = id[:shortLen]
}
return id
}
// Ellipsis truncates a string to fit within maxDisplayWidth, and appends ellipsis (…).
// For maxDisplayWidth of 1 and lower, no ellipsis is appended.
// For maxDisplayWidth of 1, first char of string will return even if its width > 1.

View File

@ -7,6 +7,49 @@ import (
is "gotest.tools/v3/assert/cmp"
)
func TestTruncateID(t *testing.T) {
tests := []struct {
doc, id, expected string
}{
{
doc: "empty ID",
id: "",
expected: "",
},
{
// IDs are expected to be 12 (short) or 64 characters, and not be numeric only,
// but TruncateID should handle these gracefully.
doc: "invalid ID",
id: "1234",
expected: "1234",
},
{
doc: "full ID",
id: "90435eec5c4e124e741ef731e118be2fc799a68aba0466ec17717f24ce2ae6a2",
expected: "90435eec5c4e",
},
{
doc: "digest",
id: "sha256:90435eec5c4e124e741ef731e118be2fc799a68aba0466ec17717f24ce2ae6a2",
expected: "90435eec5c4e",
},
{
doc: "very long ID",
id: "90435eec5c4e124e741ef731e118be2fc799a68aba0466ec17717f24ce2ae6a290435eec5c4e124e741ef731e118be2fc799a68aba0466ec17717f24ce2ae6a2",
expected: "90435eec5c4e",
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
actual := TruncateID(tc.id)
if actual != tc.expected {
t.Errorf("expected: %q, got: %q", tc.expected, actual)
}
})
}
}
func TestEllipsis(t *testing.T) {
testcases := []struct {
source string

View File

@ -6,8 +6,7 @@ import (
"github.com/distribution/reference"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/pkg/stringid"
units "github.com/docker/go-units"
"github.com/docker/go-units"
)
const (
@ -216,7 +215,7 @@ func (c *imageContext) MarshalJSON() ([]byte, error) {
func (c *imageContext) ID() string {
if c.trunc {
return stringid.TruncateID(c.i.ID)
return TruncateID(c.i.ID)
}
return c.i.ID
}

View File

@ -9,13 +9,12 @@ import (
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/pkg/stringid"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestImageContext(t *testing.T) {
imageID := stringid.GenerateRandomID()
imageID := test.RandomID()
unix := time.Now().Unix()
zeroTime := int64(-62135596800)
@ -27,7 +26,7 @@ func TestImageContext(t *testing.T) {
}{
{
imageCtx: imageContext{i: image.Summary{ID: imageID}, trunc: true},
expValue: stringid.TruncateID(imageID),
expValue: TruncateID(imageID),
call: ctx.ID,
},
{

View File

@ -6,7 +6,7 @@ import (
"strings"
"github.com/docker/docker/api/types/volume"
units "github.com/docker/go-units"
"github.com/docker/go-units"
)
const (

View File

@ -12,13 +12,12 @@ import (
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/pkg/stringid"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestVolumeContext(t *testing.T) {
volumeName := stringid.GenerateRandomID()
volumeName := test.RandomID()
var ctx volumeContext
cases := []struct {

View File

@ -4,6 +4,8 @@ import (
"archive/tar"
"bufio"
"bytes"
"crypto/rand"
"encoding/hex"
"fmt"
"io"
"net/http"
@ -15,10 +17,8 @@ import (
"time"
"github.com/docker/docker/builder/remotecontext/git"
"github.com/docker/docker/pkg/ioutils"
"github.com/docker/docker/pkg/progress"
"github.com/docker/docker/pkg/streamformatter"
"github.com/docker/docker/pkg/stringid"
"github.com/moby/go-archive"
"github.com/moby/go-archive/compression"
"github.com/moby/patternmatcher"
@ -108,7 +108,7 @@ func DetectArchiveReader(input io.ReadCloser) (rc io.ReadCloser, isArchive bool,
return nil, false, errors.Errorf("failed to peek context header from STDIN: %v", err)
}
return ioutils.NewReadCloserWrapper(buf, func() error { return input.Close() }), IsArchive(magic), nil
return newReadCloserWrapper(buf, func() error { return input.Close() }), IsArchive(magic), nil
}
// WriteTempDockerfile writes a Dockerfile stream to a temporary file with a
@ -169,7 +169,7 @@ func GetContextFromReader(rc io.ReadCloser, dockerfileName string) (out io.ReadC
return nil, "", err
}
return ioutils.NewReadCloserWrapper(tarArchive, func() error {
return newReadCloserWrapper(tarArchive, func() error {
err := tarArchive.Close()
os.RemoveAll(dockerfileDir)
return err
@ -227,7 +227,7 @@ func GetContextFromURL(out io.Writer, remoteURL, dockerfileName string) (io.Read
// Pass the response body through a progress reader.
progReader := progress.NewProgressReader(response.Body, progressOutput, response.ContentLength, "", "Downloading build context from remote url: "+remoteURL)
return GetContextFromReader(ioutils.NewReadCloserWrapper(progReader, func() error { return response.Body.Close() }), dockerfileName)
return GetContextFromReader(newReadCloserWrapper(progReader, func() error { return response.Body.Close() }), dockerfileName)
}
// getWithStatusError does an http.Get() and returns an error if the
@ -379,7 +379,7 @@ func AddDockerfileToBuildContext(dockerfileCtx io.ReadCloser, buildCtx io.ReadCl
return nil, "", err
}
now := time.Now()
randomName := ".dockerfile." + stringid.GenerateRandomID()[:20]
randomName := ".dockerfile." + randomSuffix()
buildCtx = archive.ReplaceFileTarWrapper(buildCtx, map[string]archive.TarModifierFunc{
// Add the dockerfile with a random filename
@ -422,6 +422,15 @@ func AddDockerfileToBuildContext(dockerfileCtx io.ReadCloser, buildCtx io.ReadCl
return buildCtx, randomName, nil
}
// randomSuffix returns a unique, 20-character ID consisting of a-z, 0-9.
func randomSuffix() string {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
panic(err) // This shouldn't happen
}
return hex.EncodeToString(b)[:20]
}
// Compress the build context for sending to the API
func Compress(buildCtx io.ReadCloser) (io.ReadCloser, error) {
pipeReader, pipeWriter := io.Pipe()
@ -444,3 +453,25 @@ func Compress(buildCtx io.ReadCloser) (io.ReadCloser, error) {
return pipeReader, nil
}
// readCloserWrapper wraps an io.Reader, and implements an io.ReadCloser
// It calls the given callback function when closed. It should be constructed
// with [newReadCloserWrapper].
type readCloserWrapper struct {
io.Reader
closer func() error
}
// Close calls back the passed closer function
func (r *readCloserWrapper) Close() error {
return r.closer()
}
// newReadCloserWrapper wraps an io.Reader, and implements an io.ReadCloser.
// It calls the given callback function when closed.
func newReadCloserWrapper(r io.Reader, closer func() error) io.ReadCloser {
return &readCloserWrapper{
Reader: r,
closer: closer,
}
}

View File

@ -2,8 +2,7 @@ package build
import (
"path/filepath"
"github.com/docker/docker/pkg/longpath"
"strings"
)
func getContextRoot(srcPath string) (string, error) {
@ -11,5 +10,27 @@ func getContextRoot(srcPath string) (string, error) {
if err != nil {
return "", err
}
return longpath.AddPrefix(cr), nil
return addPrefix(cr), nil
}
// longPathPrefix is the longpath prefix for Windows file paths.
const longPathPrefix = `\\?\`
// addPrefix adds the Windows long path prefix to the path provided if
// it does not already have it.
//
// See https://github.com/moby/moby/pull/15898
//
// This is a copy of [longpath.AddPrefix].
//
// [longpath.AddPrefix]:https://pkg.go.dev/github.com/docker/docker@v28.3.2+incompatible/pkg/longpath#AddPrefix
func addPrefix(path string) string {
if strings.HasPrefix(path, longPathPrefix) {
return path
}
if strings.HasPrefix(path, `\\`) {
// This is a UNC path, so we need to add 'UNC' to the path as well.
return longPathPrefix + `UNC` + path[1:]
}
return longPathPrefix + path
}

View File

@ -0,0 +1,22 @@
package build
import (
"strings"
"testing"
)
func TestStandardLongPath(t *testing.T) {
c := `C:\simple\path`
longC := addPrefix(c)
if !strings.EqualFold(longC, `\\?\C:\simple\path`) {
t.Errorf("Wrong long path returned. Original = %s ; Long = %s", c, longC)
}
}
func TestUNCLongPath(t *testing.T) {
c := `\\server\share\path`
longC := addPrefix(c)
if !strings.EqualFold(longC, `\\?\UNC\server\share\path`) {
t.Errorf("Wrong UNC long path returned. Original = %s ; Long = %s", c, longC)
}
}

View File

@ -7,8 +7,7 @@ import (
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/pkg/stringid"
units "github.com/docker/go-units"
"github.com/docker/go-units"
)
const (
@ -72,7 +71,7 @@ func (c *historyContext) MarshalJSON() ([]byte, error) {
func (c *historyContext) ID() string {
if c.trunc {
return stringid.TruncateID(c.h.ID)
return formatter.TruncateID(c.h.ID)
}
return c.h.ID
}

View File

@ -10,7 +10,6 @@ import (
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/pkg/stringid"
"gotest.tools/v3/assert"
)
@ -21,7 +20,7 @@ type historyCase struct {
}
func TestHistoryContext_ID(t *testing.T) {
id := stringid.GenerateRandomID()
id := test.RandomID()
var ctx historyContext
cases := []historyCase{
@ -35,7 +34,7 @@ func TestHistoryContext_ID(t *testing.T) {
historyContext{
h: image.HistoryResponseItem{ID: id},
trunc: true,
}, stringid.TruncateID(id), ctx.ID,
}, formatter.TruncateID(id), ctx.ID,
},
}

View File

@ -12,10 +12,10 @@ import (
"github.com/containerd/platforms"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/internal/tui"
"github.com/docker/docker/api/types/filters"
imagetypes "github.com/docker/docker/api/types/image"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
"github.com/morikuni/aec"
"github.com/opencontainers/go-digest"
@ -222,7 +222,7 @@ func printImageTree(dockerCLI command.Cli, view treeView) error {
Align: alignLeft,
Width: 12,
DetailsValue: func(d *imageDetails) string {
return stringid.TruncateID(d.ID)
return formatter.TruncateID(d.ID)
},
},
{

View File

@ -6,7 +6,6 @@ import (
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/pkg/stringid"
)
const (
@ -73,7 +72,7 @@ func (c *networkContext) MarshalJSON() ([]byte, error) {
func (c *networkContext) ID() string {
if c.trunc {
return stringid.TruncateID(c.n.ID)
return formatter.TruncateID(c.n.ID)
}
return c.n.ID
}

View File

@ -14,13 +14,12 @@ import (
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/pkg/stringid"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestNetworkContext(t *testing.T) {
networkID := stringid.GenerateRandomID()
networkID := test.RandomID()
var ctx networkContext
cases := []struct {
@ -35,7 +34,7 @@ func TestNetworkContext(t *testing.T) {
{networkContext{
n: network.Summary{ID: networkID},
trunc: true,
}, stringid.TruncateID(networkID), ctx.ID},
}, formatter.TruncateID(networkID), ctx.ID},
{networkContext{
n: network.Summary{Name: "network_name"},
}, "network_name", ctx.Name},

View File

@ -10,7 +10,7 @@ import (
"github.com/docker/cli/cli/command/inspect"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/system"
units "github.com/docker/go-units"
"github.com/docker/go-units"
)
const (

View File

@ -14,13 +14,12 @@ import (
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/api/types/system"
"github.com/docker/docker/pkg/stringid"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestNodeContext(t *testing.T) {
nodeID := stringid.GenerateRandomID()
nodeID := test.RandomID()
var ctx nodeContext
cases := []struct {

View File

@ -5,7 +5,6 @@ import (
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stringid"
)
const (
@ -66,7 +65,7 @@ func (c *pluginContext) MarshalJSON() ([]byte, error) {
func (c *pluginContext) ID() string {
if c.trunc {
return stringid.TruncateID(c.p.ID)
return formatter.TruncateID(c.p.ID)
}
return c.p.ID
}

View File

@ -12,13 +12,12 @@ import (
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/stringid"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestPluginContext(t *testing.T) {
pluginID := stringid.GenerateRandomID()
pluginID := test.RandomID()
var ctx pluginContext
cases := []struct {
@ -33,7 +32,7 @@ func TestPluginContext(t *testing.T) {
{pluginContext{
p: types.Plugin{ID: pluginID},
trunc: true,
}, stringid.TruncateID(pluginID), ctx.ID},
}, formatter.TruncateID(pluginID), ctx.ID},
{pluginContext{
p: types.Plugin{Name: "plugin_name"},
}, "plugin_name", ctx.Name},

View File

@ -110,6 +110,9 @@ func runLogin(ctx context.Context, dockerCLI command.Cli, opts loginOptions) err
if err := verifyLoginOptions(dockerCLI, &opts); err != nil {
return err
}
maybePrintEnvAuthWarning(dockerCLI)
var (
serverAddress string
msg string

View File

@ -36,6 +36,8 @@ func NewLogoutCommand(dockerCli command.Cli) *cobra.Command {
}
func runLogout(ctx context.Context, dockerCLI command.Cli, serverAddress string) error {
maybePrintEnvAuthWarning(dockerCLI)
var isDefaultRegistry bool
if serverAddress == "" {

View File

@ -0,0 +1,18 @@
package registry
import (
"os"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/internal/tui"
)
// maybePrintEnvAuthWarning if the `DOCKER_AUTH_CONFIG` environment variable is
// set this function will output a warning to stdErr
func maybePrintEnvAuthWarning(out command.Streams) {
if os.Getenv(configfile.DockerEnvConfigKey) != "" {
tui.NewOutput(out.Err()).
PrintWarning("%[1]s is set and takes precedence.\nUnset %[1]s to restore the CLI auth behaviour.\n", configfile.DockerEnvConfigKey)
}
}

View File

@ -8,7 +8,7 @@ import (
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/command/inspect"
"github.com/docker/docker/api/types/swarm"
units "github.com/docker/go-units"
"github.com/docker/go-units"
)
const (

View File

@ -14,8 +14,7 @@ import (
mounttypes "github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/pkg/stringid"
units "github.com/docker/go-units"
"github.com/docker/go-units"
"github.com/fvbommel/sortorder"
"github.com/pkg/errors"
)
@ -645,7 +644,7 @@ func (c *serviceContext) MarshalJSON() ([]byte, error) {
}
func (c *serviceContext) ID() string {
return stringid.TruncateID(c.service.ID)
return formatter.TruncateID(c.service.ID)
}
func (c *serviceContext) Name() string {

View File

@ -13,13 +13,13 @@ import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/command/idresolver"
"github.com/docker/cli/internal/logdetails"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/stdcopy"
"github.com/docker/docker/pkg/stringid"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@ -220,7 +220,7 @@ func (f *taskFormatter) format(ctx context.Context, logCtx logContext) (string,
if f.opts.noTrunc {
taskName += "." + task.ID
} else {
taskName += "." + stringid.TruncateID(task.ID)
taskName += "." + formatter.TruncateID(task.ID)
}
}

View File

@ -11,12 +11,12 @@ import (
"strings"
"time"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/progress"
"github.com/docker/docker/pkg/streamformatter"
"github.com/docker/docker/pkg/stringid"
)
var (
@ -505,7 +505,7 @@ func (u *globalProgressUpdater) writeTaskProgress(task swarm.Task, nodeCount int
if task.Status.Err != "" {
u.progressOut.WriteProgress(progress.Progress{
ID: stringid.TruncateID(task.NodeID),
ID: formatter.TruncateID(task.NodeID),
Action: truncError(task.Status.Err),
})
return
@ -513,7 +513,7 @@ func (u *globalProgressUpdater) writeTaskProgress(task swarm.Task, nodeCount int
if !terminalState(task.DesiredState) && !terminalState(task.Status.State) {
u.progressOut.WriteProgress(progress.Progress{
ID: stringid.TruncateID(task.NodeID),
ID: formatter.TruncateID(task.NodeID),
Action: fmt.Sprintf("%-[1]*s", longestState, task.Status.State),
Current: numberedStates[task.Status.State],
Total: maxProgress,

View File

@ -8,7 +8,6 @@ import (
"github.com/distribution/reference"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/go-units"
)
@ -79,7 +78,7 @@ func (c *taskContext) MarshalJSON() ([]byte, error) {
func (c *taskContext) ID() string {
if c.trunc {
return stringid.TruncateID(c.task.ID)
return formatter.TruncateID(c.task.ID)
}
return c.task.ID
}

View File

@ -5,7 +5,6 @@ import (
"strings"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/pkg/stringid"
)
const (
@ -119,7 +118,7 @@ func (c *signerInfoContext) Keys() string {
truncatedKeys := []string{}
if c.trunc {
for _, keyID := range c.s.Keys {
truncatedKeys = append(truncatedKeys, stringid.TruncateID(keyID))
truncatedKeys = append(truncatedKeys, formatter.TruncateID(keyID))
}
return strings.Join(truncatedKeys, ", ")
}

View File

@ -5,13 +5,13 @@ import (
"testing"
"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/cli/internal/test"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestTrustTag(t *testing.T) {
digest := stringid.GenerateRandomID()
digest := test.RandomID()
trustedTag := "tag"
var ctx trustTagContext

View File

@ -21,7 +21,7 @@ import (
"github.com/docker/cli/opts/swarmopts"
"github.com/docker/docker/api/types/versions"
"github.com/docker/go-connections/nat"
units "github.com/docker/go-units"
"github.com/docker/go-units"
"github.com/go-viper/mapstructure/v2"
"github.com/google/shlex"
"github.com/pkg/errors"

View File

@ -56,7 +56,7 @@ type configEnv struct {
AuthConfigs map[string]configEnvAuth `json:"auths"`
}
// dockerEnvConfig is an environment variable that contains a JSON encoded
// DockerEnvConfigKey is an environment variable that contains a JSON encoded
// credential config. It only supports storing the credentials as a base64
// encoded string in the format base64("username:pat").
//
@ -71,7 +71,7 @@ type configEnv struct {
// }
// }
// }
const dockerEnvConfig = "DOCKER_AUTH_CONFIG"
const DockerEnvConfigKey = "DOCKER_AUTH_CONFIG"
// ProxyConfig contains proxy configuration settings
type ProxyConfig struct {
@ -296,7 +296,7 @@ func (configFile *ConfigFile) GetCredentialsStore(registryHostname string) crede
store = newNativeStore(configFile, helper)
}
envConfig := os.Getenv(dockerEnvConfig)
envConfig := os.Getenv(DockerEnvConfigKey)
if envConfig == "" {
return store
}

View File

@ -47,14 +47,19 @@ func getConnectionHelper(daemonURL string, sshFlags []string) (*ConnectionHelper
}
sshFlags = addSSHTimeout(sshFlags)
sshFlags = disablePseudoTerminalAllocation(sshFlags)
remoteCommand := []string{"docker", "system", "dial-stdio"}
socketPath := sp.Path
if strings.Trim(sp.Path, "/") != "" {
remoteCommand = []string{"docker", "--host=unix://" + socketPath, "system", "dial-stdio"}
}
sshArgs, err := sp.Command(sshFlags, remoteCommand...)
if err != nil {
return nil, err
}
return &ConnectionHelper{
Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) {
args := []string{"docker"}
if sp.Path != "" {
args = append(args, "--host", "unix://"+sp.Path)
}
args = append(args, "system", "dial-stdio")
return commandconn.New(ctx, "ssh", append(sshFlags, sp.Args(args...)...)...)
return commandconn.New(ctx, "ssh", sshArgs...)
},
Host: "http://docker.example.com",
}, nil

View File

@ -0,0 +1,27 @@
Copyright (c) 2016, Daniel Martí. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,13 @@
// Package syntax is a fork of [mvdan.cc/sh/v3@v3.10.0/syntax].
//
// Copyright (c) 2016, Daniel Martí. All rights reserved.
//
// It is a reduced set of the package to only provide the [Quote] function,
// and contains the [LICENSE], [quote.go] and [parser.go] files at the given
// revision.
//
// [quote.go]: https://raw.githubusercontent.com/mvdan/sh/refs/tags/v3.10.0/syntax/quote.go
// [parser.go]: https://raw.githubusercontent.com/mvdan/sh/refs/tags/v3.10.0/syntax/parser.go
// [LICENSE]: https://raw.githubusercontent.com/mvdan/sh/refs/tags/v3.10.0/LICENSE
// [mvdan.cc/sh/v3@v3.10.0/syntax]: https://pkg.go.dev/mvdan.cc/sh/v3@v3.10.0/syntax
package syntax

View File

@ -0,0 +1,95 @@
// Copyright (c) 2016, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information
package syntax
// LangVariant describes a shell language variant to use when tokenizing and
// parsing shell code. The zero value is [LangBash].
type LangVariant int
const (
// LangBash corresponds to the GNU Bash language, as described in its
// manual at https://www.gnu.org/software/bash/manual/bash.html.
//
// We currently follow Bash version 5.2.
//
// Its string representation is "bash".
LangBash LangVariant = iota
// LangPOSIX corresponds to the POSIX Shell language, as described at
// https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html.
//
// Its string representation is "posix" or "sh".
LangPOSIX
// LangMirBSDKorn corresponds to the MirBSD Korn Shell, also known as
// mksh, as described at http://www.mirbsd.org/htman/i386/man1/mksh.htm.
// Note that it shares some features with Bash, due to the shared
// ancestry that is ksh.
//
// We currently follow mksh version 59.
//
// Its string representation is "mksh".
LangMirBSDKorn
// LangBats corresponds to the Bash Automated Testing System language,
// as described at https://github.com/bats-core/bats-core. Note that
// it's just a small extension of the Bash language.
//
// Its string representation is "bats".
LangBats
// LangAuto corresponds to automatic language detection,
// commonly used by end-user applications like shfmt,
// which can guess a file's language variant given its filename or shebang.
//
// At this time, [Variant] does not support LangAuto.
LangAuto
)
func (l LangVariant) String() string {
switch l {
case LangBash:
return "bash"
case LangPOSIX:
return "posix"
case LangMirBSDKorn:
return "mksh"
case LangBats:
return "bats"
case LangAuto:
return "auto"
}
return "unknown shell language variant"
}
// IsKeyword returns true if the given word is part of the language keywords.
func IsKeyword(word string) bool {
// This list has been copied from the bash 5.1 source code, file y.tab.c +4460
switch word {
case
"!",
"[[", // only if COND_COMMAND is defined
"]]", // only if COND_COMMAND is defined
"case",
"coproc", // only if COPROCESS_SUPPORT is defined
"do",
"done",
"else",
"esac",
"fi",
"for",
"function",
"if",
"in",
"select", // only if SELECT_COMMAND is defined
"then",
"time", // only if COMMAND_TIMING is defined
"until",
"while",
"{",
"}":
return true
}
return false
}

View File

@ -0,0 +1,187 @@
// Copyright (c) 2021, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information
package syntax
import (
"fmt"
"strings"
"unicode"
"unicode/utf8"
)
type QuoteError struct {
ByteOffset int
Message string
}
func (e QuoteError) Error() string {
return fmt.Sprintf("cannot quote character at byte %d: %s", e.ByteOffset, e.Message)
}
const (
quoteErrNull = "shell strings cannot contain null bytes"
quoteErrPOSIX = "POSIX shell lacks escape sequences"
quoteErrRange = "rune out of range"
quoteErrMksh = "mksh cannot escape codepoints above 16 bits"
)
// Quote returns a quoted version of the input string,
// so that the quoted version is expanded or interpreted
// as the original string in the given language variant.
//
// Quoting is necessary when using arbitrary literal strings
// as words in a shell script or command.
// Without quoting, one can run into syntax errors,
// as well as the possibility of running unintended code.
//
// An error is returned when a string cannot be quoted for a variant.
// For instance, POSIX lacks escape sequences for non-printable characters,
// and no language variant can represent a string containing null bytes.
// In such cases, the returned error type will be *QuoteError.
//
// The quoting strategy is chosen on a best-effort basis,
// to minimize the amount of extra bytes necessary.
//
// Some strings do not require any quoting and are returned unchanged.
// Those strings can be directly surrounded in single quotes as well.
//
//nolint:gocyclo // ignore "cyclomatic complexity 35 of func `Quote` is high (> 16) (gocyclo)"
func Quote(s string, lang LangVariant) (string, error) {
if s == "" {
// Special case; an empty string must always be quoted,
// as otherwise it expands to zero fields.
return "''", nil
}
shellChars := false
nonPrintable := false
offs := 0
for rem := s; len(rem) > 0; {
r, size := utf8.DecodeRuneInString(rem)
switch r {
// Like regOps; token characters.
case ';', '"', '\'', '(', ')', '$', '|', '&', '>', '<', '`',
// Whitespace; might result in multiple fields.
' ', '\t', '\r', '\n',
// Escape sequences would be expanded.
'\\',
// Would start a comment unless quoted.
'#',
// Might result in brace expansion.
'{',
// Might result in tilde expansion.
'~',
// Might result in globbing.
'*', '?', '[',
// Might result in an assignment.
'=':
shellChars = true
case '\x00':
return "", &QuoteError{ByteOffset: offs, Message: quoteErrNull}
}
if r == utf8.RuneError || !unicode.IsPrint(r) {
if lang == LangPOSIX {
return "", &QuoteError{ByteOffset: offs, Message: quoteErrPOSIX}
}
nonPrintable = true
}
rem = rem[size:]
offs += size
}
if !shellChars && !nonPrintable && !IsKeyword(s) {
// Nothing to quote; avoid allocating.
return s, nil
}
// Single quotes are usually best,
// as they don't require any escaping of characters.
// If we have any invalid utf8 or non-printable runes,
// use $'' so that we can escape them.
// Note that we can't use double quotes for those.
var b strings.Builder
if nonPrintable {
b.WriteString("$'")
lastRequoteIfHex := false
offs = 0
for rem := s; len(rem) > 0; {
nextRequoteIfHex := false
r, size := utf8.DecodeRuneInString(rem)
switch {
case r == '\'', r == '\\':
b.WriteByte('\\')
b.WriteRune(r)
case unicode.IsPrint(r) && r != utf8.RuneError:
if lastRequoteIfHex && isHex(r) {
b.WriteString("'$'")
}
b.WriteRune(r)
case r == '\a':
b.WriteString(`\a`)
case r == '\b':
b.WriteString(`\b`)
case r == '\f':
b.WriteString(`\f`)
case r == '\n':
b.WriteString(`\n`)
case r == '\r':
b.WriteString(`\r`)
case r == '\t':
b.WriteString(`\t`)
case r == '\v':
b.WriteString(`\v`)
case r < utf8.RuneSelf, r == utf8.RuneError && size == 1:
// \xXX, fixed at two hexadecimal characters.
fmt.Fprintf(&b, "\\x%02x", rem[0])
// Unfortunately, mksh allows \x to consume more hex characters.
// Ensure that we don't allow it to read more than two.
if lang == LangMirBSDKorn {
nextRequoteIfHex = true
}
case r > utf8.MaxRune:
// Not a valid Unicode code point?
return "", &QuoteError{ByteOffset: offs, Message: quoteErrRange}
case lang == LangMirBSDKorn && r > 0xFFFD:
// From the CAVEATS section in R59's man page:
//
// mksh currently uses OPTU-16 internally, which is the same as
// UTF-8 and CESU-8 with 0000..FFFD being valid codepoints.
return "", &QuoteError{ByteOffset: offs, Message: quoteErrMksh}
case r < 0x10000:
// \uXXXX, fixed at four hexadecimal characters.
fmt.Fprintf(&b, "\\u%04x", r)
default:
// \UXXXXXXXX, fixed at eight hexadecimal characters.
fmt.Fprintf(&b, "\\U%08x", r)
}
rem = rem[size:]
lastRequoteIfHex = nextRequoteIfHex
offs += size
}
b.WriteString("'")
return b.String(), nil
}
// Single quotes without any need for escaping.
if !strings.Contains(s, "'") {
return "'" + s + "'", nil
}
// The string contains single quotes,
// so fall back to double quotes.
b.WriteByte('"')
for _, r := range s {
switch r {
case '"', '\\', '`', '$':
b.WriteByte('\\')
}
b.WriteRune(r)
}
b.WriteByte('"')
return b.String(), nil
}
func isHex(r rune) bool {
return (r >= '0' && r <= '9') ||
(r >= 'a' && r <= 'f') ||
(r >= 'A' && r <= 'F')
}

View File

@ -5,6 +5,8 @@ import (
"errors"
"fmt"
"net/url"
"github.com/docker/cli/cli/connhelper/internal/syntax"
)
// ParseURL creates a [Spec] from the given ssh URL. It returns an error if
@ -76,16 +78,106 @@ type Spec struct {
Path string
}
// Args returns args except "ssh" itself combined with optional additional command args
func (sp *Spec) Args(add ...string) []string {
// Args returns args except "ssh" itself combined with optional additional
// command and args to be executed on the remote host. It attempts to quote
// the given arguments to account for ssh executing the remote command in a
// shell. It returns nil when unable to quote the remote command.
func (sp *Spec) Args(remoteCommandAndArgs ...string) []string {
// Format the remote command to run using the ssh connection, quoting
// values where needed because ssh executes these in a POSIX shell.
remoteCommand, err := quoteCommand(remoteCommandAndArgs...)
if err != nil {
return nil
}
sshArgs, err := sp.args()
if err != nil {
return nil
}
if remoteCommand != "" {
sshArgs = append(sshArgs, remoteCommand)
}
return sshArgs
}
func (sp *Spec) args(sshFlags ...string) ([]string, error) {
var args []string
if sp.Host == "" {
return nil, errors.New("no host specified")
}
if sp.User != "" {
args = append(args, "-l", sp.User)
// Quote user, as it's obtained from the URL.
usr, err := syntax.Quote(sp.User, syntax.LangPOSIX)
if err != nil {
return nil, fmt.Errorf("invalid user: %w", err)
}
args = append(args, "-l", usr)
}
if sp.Port != "" {
args = append(args, "-p", sp.Port)
// Quote port, as it's obtained from the URL.
port, err := syntax.Quote(sp.Port, syntax.LangPOSIX)
if err != nil {
return nil, fmt.Errorf("invalid port: %w", err)
}
args = append(args, "-p", port)
}
args = append(args, "--", sp.Host)
args = append(args, add...)
return args
// We consider "sshFlags" to be "trusted", and set from code only,
// as they are not parsed from the DOCKER_HOST URL.
args = append(args, sshFlags...)
host, err := syntax.Quote(sp.Host, syntax.LangPOSIX)
if err != nil {
return nil, fmt.Errorf("invalid host: %w", err)
}
return append(args, "--", host), nil
}
// Command returns the ssh flags and arguments to execute a command
// (remoteCommandAndArgs) on the remote host. Where needed, it quotes
// values passed in remoteCommandAndArgs to account for ssh executing
// the remote command in a shell. It returns an error if no remote command
// is passed, or when unable to quote the remote command.
//
// Important: to preserve backward-compatibility, Command does not currently
// perform sanitization or quoting on the sshFlags and callers are expected
// to sanitize this argument.
func (sp *Spec) Command(sshFlags []string, remoteCommandAndArgs ...string) ([]string, error) {
if len(remoteCommandAndArgs) == 0 {
return nil, errors.New("no remote command specified")
}
sshArgs, err := sp.args(sshFlags...)
if err != nil {
return nil, err
}
remoteCommand, err := quoteCommand(remoteCommandAndArgs...)
if err != nil {
return nil, err
}
if remoteCommand != "" {
sshArgs = append(sshArgs, remoteCommand)
}
return sshArgs, nil
}
// quoteCommand returns the remote command to run using the ssh connection
// as a single string, quoting values where needed because ssh executes
// these in a POSIX shell.
func quoteCommand(commandAndArgs ...string) (string, error) {
var quotedCmd string
for i, arg := range commandAndArgs {
a, err := syntax.Quote(arg, syntax.LangPOSIX)
if err != nil {
return "", fmt.Errorf("invalid argument: %w", err)
}
if i == 0 {
quotedCmd = a
continue
}
quotedCmd += " " + a
}
// each part is quoted appropriately, so now we'll have a full
// shell command to pass off to "ssh"
return quotedCmd, nil
}

View File

@ -1,6 +1,7 @@
package ssh
import (
"strings"
"testing"
"gotest.tools/v3/assert"
@ -26,6 +27,28 @@ func TestParseURL(t *testing.T) {
Host: "example.com",
},
},
{
doc: "bare ssh URL with trailing slash",
url: "ssh://example.com/",
expectedArgs: []string{
"--", "example.com",
},
expectedSpec: Spec{
Host: "example.com",
Path: "/",
},
},
{
doc: "bare ssh URL with trailing slashes",
url: "ssh://example.com//",
expectedArgs: []string{
"--", "example.com",
},
expectedSpec: Spec{
Host: "example.com",
Path: "//",
},
},
{
doc: "bare ssh URL and remote command",
url: "ssh://example.com",
@ -34,7 +57,7 @@ func TestParseURL(t *testing.T) {
},
expectedArgs: []string{
"--", "example.com",
"docker", "system", "dial-stdio",
`docker system dial-stdio`,
},
expectedSpec: Spec{
Host: "example.com",
@ -48,7 +71,7 @@ func TestParseURL(t *testing.T) {
},
expectedArgs: []string{
"--", "example.com",
"docker", "--host", "unix:///var/run/docker.sock", "system", "dial-stdio",
`docker --host unix:///var/run/docker.sock system dial-stdio`,
},
expectedSpec: Spec{
Host: "example.com",
@ -84,6 +107,25 @@ func TestParseURL(t *testing.T) {
Path: "/var/run/docker.sock",
},
},
{
// This test is only to verify the behavior of ParseURL to
// pass through the Path as-is. Neither Spec.Args, nor
// Spec.Command use the Path field directly, and it should
// likely be deprecated.
doc: "bad path",
url: `ssh://example.com/var/run/docker.sock '$(echo hello > /hello.txt)'`,
remoteCommand: []string{
"docker", "--host", `unix:///var/run/docker.sock '$(echo hello > /hello.txt)'`, "system", "dial-stdio",
},
expectedArgs: []string{
"--", "example.com",
`docker --host "unix:///var/run/docker.sock '\$(echo hello > /hello.txt)'" system dial-stdio`,
},
expectedSpec: Spec{
Host: "example.com",
Path: `/var/run/docker.sock '$(echo hello > /hello.txt)'`,
},
},
{
doc: "malformed URL",
url: "malformed %%url",
@ -123,6 +165,21 @@ func TestParseURL(t *testing.T) {
url: "https://example.com",
expectedError: `invalid SSH URL: incorrect scheme: https`,
},
{
doc: "invalid URL with NUL character",
url: "ssh://example.com/var/run/\x00docker.sock",
expectedError: `invalid SSH URL: net/url: invalid control character in URL`,
},
{
doc: "invalid URL with newline character",
url: "ssh://example.com/var/run/docker.sock\n",
expectedError: `invalid SSH URL: net/url: invalid control character in URL`,
},
{
doc: "invalid URL with control character",
url: "ssh://example.com/var/run/\x1bdocker.sock",
expectedError: `invalid SSH URL: net/url: invalid control character in URL`,
},
}
for _, tc := range testCases {
t.Run(tc.doc, func(t *testing.T) {
@ -139,3 +196,122 @@ func TestParseURL(t *testing.T) {
})
}
}
func TestCommand(t *testing.T) {
testCases := []struct {
doc string
url string
sshFlags []string
customCmd []string
expectedCmd []string
expectedError string
}{
{
doc: "bare ssh URL",
url: "ssh://example.com",
expectedCmd: []string{
"--", "example.com",
"docker system dial-stdio",
},
},
{
doc: "bare ssh URL with trailing slash",
url: "ssh://example.com/",
expectedCmd: []string{
"--", "example.com",
"docker system dial-stdio",
},
},
{
doc: "bare ssh URL with custom ssh flags",
url: "ssh://example.com",
sshFlags: []string{"-T", "-o", "ConnectTimeout=30", "-oStrictHostKeyChecking=no"},
expectedCmd: []string{
"-T",
"-o", "ConnectTimeout=30",
"-oStrictHostKeyChecking=no",
"--", "example.com",
"docker system dial-stdio",
},
},
{
doc: "ssh URL with all options",
url: "ssh://me@example.com:10022/var/run/docker.sock",
sshFlags: []string{"-T", "-o ConnectTimeout=30"},
expectedCmd: []string{
"-l", "me",
"-p", "10022",
"-T",
"-o ConnectTimeout=30",
"--", "example.com",
"docker '--host=unix:///var/run/docker.sock' system dial-stdio",
},
},
{
doc: "bad ssh flags",
url: "ssh://example.com",
sshFlags: []string{"-T", "-o", `ConnectTimeout=30 $(echo hi > /hi.txt)`},
expectedCmd: []string{
"-T",
"-o", `ConnectTimeout=30 $(echo hi > /hi.txt)`,
"--", "example.com",
"docker system dial-stdio",
},
},
{
doc: "bad username",
url: `ssh://$(shutdown)me@example.com`,
expectedCmd: []string{
"-l", `'$(shutdown)me'`,
"--", "example.com",
"docker system dial-stdio",
},
},
{
doc: "bad hostname",
url: `ssh://$(shutdown)example.com`,
expectedCmd: []string{
"--", `'$(shutdown)example.com'`,
"docker system dial-stdio",
},
},
{
doc: "bad path",
url: `ssh://example.com/var/run/docker.sock '$(echo hello > /hello.txt)'`,
expectedCmd: []string{
"--", "example.com",
`docker "--host=unix:///var/run/docker.sock '\$(echo hello > /hello.txt)'" system dial-stdio`,
},
},
{
doc: "missing command",
url: "ssh://example.com",
customCmd: []string{},
expectedError: "no remote command specified",
},
}
for _, tc := range testCases {
t.Run(tc.doc, func(t *testing.T) {
sp, err := ParseURL(tc.url)
assert.NilError(t, err)
var commandAndArgs []string
if tc.customCmd == nil {
socketPath := sp.Path
commandAndArgs = []string{"docker", "system", "dial-stdio"}
if strings.Trim(socketPath, "/") != "" {
commandAndArgs = []string{"docker", "--host=unix://" + socketPath, "system", "dial-stdio"}
}
}
actualCmd, err := sp.Command(tc.sshFlags, commandAndArgs...)
if tc.expectedError == "" {
assert.NilError(t, err)
assert.Check(t, is.DeepEqual(actualCmd, tc.expectedCmd), "%+#v", actualCmd)
} else {
assert.Check(t, is.Error(err, tc.expectedError))
assert.Check(t, is.Nil(actualCmd))
}
})
}
}

View File

@ -33,5 +33,8 @@ func IsEnabled() bool {
// The default is to log to the debug level which is only
// enabled when debugging is enabled.
var OTELErrorHandler otel.ErrorHandler = otel.ErrorHandlerFunc(func(err error) {
if err == nil {
return
}
logrus.WithError(err).Debug("otel error")
})

View File

@ -1,5 +1,5 @@
variable "GO_VERSION" {
default = "1.24.4"
default = "1.24.5"
}
variable "VERSION" {
default = ""

View File

@ -1,5 +1,7 @@
# syntax=docker/dockerfile:1
# ALPINE_VERSION sets the version of the alpine base image to use.
# It must be a supported tag in the docker.io/library/alpine image repository.
ARG ALPINE_VERSION=3.21
FROM alpine:${ALPINE_VERSION} AS gen

View File

@ -1,12 +1,16 @@
# syntax=docker/dockerfile:1
ARG GO_VERSION=1.24.4
ARG GO_VERSION=1.24.5
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
# It must be a supported tag in the docker.io/library/alpine image repository
# that's also available as alpine image variant for the Golang version used.
ARG ALPINE_VERSION=3.21
# BUILDX_VERSION sets the version of buildx to install in the dev container.
# It must be a valid tag in the docker.io/docker/buildx-bin image repository
# on Docker Hub.
ARG BUILDX_VERSION=0.24.0
ARG BUILDX_VERSION=0.25.0
FROM docker/buildx-bin:${BUILDX_VERSION} AS buildx
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS golang
@ -22,7 +26,9 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
&& gofumpt --version
FROM golang AS gotestsum
ARG GOTESTSUM_VERSION=v1.12.0
# GOTESTSUM_VERSION sets the version of gotestsum to install in the dev container.
# It must be a valid tag in the https://github.com/gotestyourself/gotestsum repository.
ARG GOTESTSUM_VERSION=v1.12.3
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg/mod \
--mount=type=tmpfs,target=/go/src/ \

View File

@ -1,6 +1,10 @@
# syntax=docker/dockerfile:1
ARG GO_VERSION=1.24.4
ARG GO_VERSION=1.24.5
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
# It must be a supported tag in the docker.io/library/alpine image repository
# that's also available as alpine image variant for the Golang version used.
ARG ALPINE_VERSION=3.21
ARG GOLANGCI_LINT_VERSION=v2.1.5

View File

@ -1,6 +1,10 @@
# syntax=docker/dockerfile:1
ARG GO_VERSION=1.24.4
ARG GO_VERSION=1.24.5
# ALPINE_VERSION sets the version of the alpine base image to use, including for the golang image.
# It must be a supported tag in the docker.io/library/alpine image repository
# that's also available as alpine image variant for the Golang version used.
ARG ALPINE_VERSION=3.21
ARG MODOUTDATED_VERSION=v0.8.0

View File

@ -937,15 +937,14 @@ PS C:\> docker run --device=class/86E0D1E0-8089-11D0-9CE4-08003E301F73 mcr.micro
#### CDI devices
> [!NOTE]
> The CDI feature is experimental, and potentially subject to change.
> CDI is currently only supported for Linux containers.
[Container Device Interface
(CDI)](https://github.com/cncf-tags/container-device-interface/blob/main/SPEC.md)
is a standardized mechanism for container runtimes to create containers which
are able to interact with third party devices.
CDI is currently only supported for Linux containers and is enabled by default
since Docker Engine 28.3.0.
With CDI, device configurations are declaratively defined using a JSON or YAML
file. In addition to enabling the container to interact with the device node,
it also lets you specify additional configuration for the device, such as
@ -966,7 +965,7 @@ This starts an `ubuntu` container with access to the specified CDI device,
available on the system running the daemon, in one of the configured CDI
specification directories.
- The CDI feature has been enabled in the daemon; see [Enable CDI
devices](https://docs.docker.com/reference/cli/dockerd/#enable-cdi-devices).
devices](https://docs.docker.com/reference/cli/dockerd/#configure-cdi-devices).
### <a name="attach"></a> Attach to STDIN/STDOUT/STDERR (-a, --attach)

View File

@ -840,42 +840,49 @@ $ docker run -it --add-host host.docker.internal:host-gateway \
PING host.docker.internal (2001:db8::1111): 56 data bytes
```
### Enable CDI devices
> [!NOTE]
> This is experimental feature and as such doesn't represent a stable API.
>
> This feature isn't enabled by default. To this feature, set `features.cdi` to
> `true` in the `daemon.json` configuration file.
### Configure CDI devices
Container Device Interface (CDI) is a
[standardized](https://github.com/cncf-tags/container-device-interface/blob/main/SPEC.md)
mechanism for container runtimes to create containers which are able to
interact with third party devices.
CDI is currently only supported for Linux containers and is enabled by default
since Docker Engine 28.3.0.
The Docker daemon supports running containers with CDI devices if the requested
device specifications are available on the filesystem of the daemon.
The default specification directors are:
The default specification directories are:
- `/etc/cdi/` for static CDI Specs
- `/var/run/cdi` for generated CDI Specs
Alternatively, you can set custom locations for CDI specifications using the
#### Set custom locations
To set custom locations for CDI specifications, use the
`cdi-spec-dirs` option in the `daemon.json` configuration file, or the
`--cdi-spec-dir` flag for the `dockerd` CLI.
`--cdi-spec-dir` flag for the `dockerd` CLI:
```json
{
"features": {
"cdi": true
},
"cdi-spec-dirs": ["/etc/cdi/", "/var/run/cdi"]
}
```
When CDI is enabled for a daemon, you can view the configured CDI specification
directories using the `docker info` command.
You can view the configured CDI specification directories using the `docker info` command.
#### Disable CDI devices
The feature in enabled by default. To disable it, use the `cdi` options in the `deamon.json` file:
```json
"features": {
"cdi": false
},
```
To check the status of the CDI devices, run `docker info`.
#### Daemon logging format {#log-format}

View File

@ -240,7 +240,7 @@ func TestPromptExitCode(t *testing.T) {
case <-writeDone:
buf.Reset()
assert.NilError(t, bufioWriter.Flush())
assert.Equal(t, buf.String(), "\n", "expected a new line after the process exits from SIGINT")
assert.Assert(t, strings.HasSuffix(buf.String(), "\n"), "expected a new line after the process exits from SIGINT")
}
})
}

View File

@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
ARG GO_VERSION=1.24.4
ARG GO_VERSION=1.24.5
FROM golang:${GO_VERSION}-alpine AS generated
ENV GOTOOLCHAIN=local

15
internal/test/randomid.go Normal file
View File

@ -0,0 +1,15 @@
package test
import (
"crypto/rand"
"encoding/hex"
)
// RandomID returns a unique, 64-character ID consisting of a-z, 0-9.
func RandomID() string {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
panic(err) // This shouldn't happen
}
return hex.EncodeToString(b)
}

View File

@ -15,19 +15,39 @@ var InfoHeader = Str{
Fancy: aec.Bold.Apply(aec.LightCyanB.Apply(aec.BlackF.Apply("i")) + " " + aec.LightCyanF.Apply("Info → ")),
}
func (o Output) PrintNote(format string, args ...any) {
type options struct {
header Str
}
type noteOptions func(o *options)
func withHeader(header Str) noteOptions {
return func(o *options) {
o.header = header
}
}
func (o Output) printNoteWithOptions(format string, args []any, opts ...noteOptions) {
if o.isTerminal {
// TODO: Handle all flags
format = strings.ReplaceAll(format, "--platform", ColorFlag.Apply("--platform"))
}
header := o.Sprint(InfoHeader)
opt := &options{
header: InfoHeader,
}
_, _ = fmt.Fprint(o, "\n", header)
for _, override := range opts {
override(opt)
}
h := o.Sprint(opt.header)
_, _ = fmt.Fprint(o, "\n", h)
s := fmt.Sprintf(format, args...)
for idx, line := range strings.Split(s, "\n") {
if idx > 0 {
_, _ = fmt.Fprint(o, strings.Repeat(" ", Width(header)))
_, _ = fmt.Fprint(o, strings.Repeat(" ", Width(h)))
}
l := line
@ -37,3 +57,16 @@ func (o Output) PrintNote(format string, args ...any) {
_, _ = fmt.Fprintln(o, l)
}
}
func (o Output) PrintNote(format string, args ...any) {
o.printNoteWithOptions(format, args, withHeader(InfoHeader))
}
var warningHeader = Str{
Plain: " Warn -> ",
Fancy: aec.Bold.Apply(aec.LightYellowB.Apply(aec.BlackF.Apply("w")) + " " + ColorWarning.Apply("Warn → ")),
}
func (o Output) PrintWarning(format string, args ...any) {
o.printNoteWithOptions(format, args, withHeader(warningHeader))
}

View File

@ -15,7 +15,7 @@ require (
github.com/distribution/reference v0.6.0
github.com/docker/cli-docs-tool v0.10.0
github.com/docker/distribution v2.8.3+incompatible
github.com/docker/docker v28.3.0-rc.1.0.20250620162235-265f70964794+incompatible // v28.x branch / v28.3.0-rc.2
github.com/docker/docker v28.3.1+incompatible
github.com/docker/docker-credential-helpers v0.9.3
github.com/docker/go-connections v0.5.0
github.com/docker/go-units v0.5.0

View File

@ -57,8 +57,8 @@ github.com/docker/cli-docs-tool v0.10.0/go.mod h1:5EM5zPnT2E7yCLERZmrDA234Vwn09f
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v28.3.0-rc.1.0.20250620162235-265f70964794+incompatible h1:aIFvT3snnLS3DAg38wPrGS9WEcV86fTeONAO/W6N+Y8=
github.com/docker/docker v28.3.0-rc.1.0.20250620162235-265f70964794+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.3.1+incompatible h1:20+BmuA9FXlCX4ByQ0vYJcUEnOmRM6XljDnFWR+jCyY=
github.com/docker/docker v28.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=

View File

@ -1,44 +0,0 @@
package ioutils
import (
"io"
"os"
"github.com/moby/sys/atomicwriter"
)
// NewAtomicFileWriter returns WriteCloser so that writing to it writes to a
// temporary file and closing it atomically changes the temporary file to
// destination path. Writing and closing concurrently is not allowed.
// NOTE: umask is not considered for the file's permissions.
//
// Deprecated: use [atomicwriter.New] instead.
func NewAtomicFileWriter(filename string, perm os.FileMode) (io.WriteCloser, error) {
return atomicwriter.New(filename, perm)
}
// AtomicWriteFile atomically writes data to a file named by filename and with the specified permission bits.
// NOTE: umask is not considered for the file's permissions.
//
// Deprecated: use [atomicwriter.WriteFile] instead.
func AtomicWriteFile(filename string, data []byte, perm os.FileMode) error {
return atomicwriter.WriteFile(filename, data, perm)
}
// AtomicWriteSet is used to atomically write a set
// of files and ensure they are visible at the same time.
// Must be committed to a new directory.
//
// Deprecated: use [atomicwriter.WriteSet] instead.
type AtomicWriteSet = atomicwriter.WriteSet
// NewAtomicWriteSet creates a new atomic write set to
// atomically create a set of files. The given directory
// is used as the base directory for storing files before
// commit. If no temporary directory is given the system
// default is used.
//
// Deprecated: use [atomicwriter.NewWriteSet] instead.
func NewAtomicWriteSet(tmpDir string) (*atomicwriter.WriteSet, error) {
return atomicwriter.NewWriteSet(tmpDir)
}

View File

@ -1,118 +0,0 @@
package ioutils
import (
"context"
"io"
"runtime/debug"
"sync/atomic"
"github.com/containerd/log"
)
// readCloserWrapper wraps an io.Reader, and implements an io.ReadCloser
// It calls the given callback function when closed. It should be constructed
// with NewReadCloserWrapper
type readCloserWrapper struct {
io.Reader
closer func() error
closed atomic.Bool
}
// Close calls back the passed closer function
func (r *readCloserWrapper) Close() error {
if !r.closed.CompareAndSwap(false, true) {
subsequentCloseWarn("ReadCloserWrapper")
return nil
}
return r.closer()
}
// NewReadCloserWrapper wraps an io.Reader, and implements an io.ReadCloser.
// It calls the given callback function when closed.
func NewReadCloserWrapper(r io.Reader, closer func() error) io.ReadCloser {
return &readCloserWrapper{
Reader: r,
closer: closer,
}
}
// cancelReadCloser wraps an io.ReadCloser with a context for cancelling read
// operations.
type cancelReadCloser struct {
cancel func()
pR *io.PipeReader // Stream to read from
pW *io.PipeWriter
closed atomic.Bool
}
// NewCancelReadCloser creates a wrapper that closes the ReadCloser when the
// context is cancelled. The returned io.ReadCloser must be closed when it is
// no longer needed.
func NewCancelReadCloser(ctx context.Context, in io.ReadCloser) io.ReadCloser {
pR, pW := io.Pipe()
// Create a context used to signal when the pipe is closed
doneCtx, cancel := context.WithCancel(context.Background())
p := &cancelReadCloser{
cancel: cancel,
pR: pR,
pW: pW,
}
go func() {
_, err := io.Copy(pW, in)
select {
case <-ctx.Done():
// If the context was closed, p.closeWithError
// was already called. Calling it again would
// change the error that Read returns.
default:
p.closeWithError(err)
}
in.Close()
}()
go func() {
for {
select {
case <-ctx.Done():
p.closeWithError(ctx.Err())
case <-doneCtx.Done():
return
}
}
}()
return p
}
// Read wraps the Read method of the pipe that provides data from the wrapped
// ReadCloser.
func (p *cancelReadCloser) Read(buf []byte) (int, error) {
return p.pR.Read(buf)
}
// closeWithError closes the wrapper and its underlying reader. It will
// cause future calls to Read to return err.
func (p *cancelReadCloser) closeWithError(err error) {
_ = p.pW.CloseWithError(err)
p.cancel()
}
// Close closes the wrapper its underlying reader. It will cause
// future calls to Read to return io.EOF.
func (p *cancelReadCloser) Close() error {
if !p.closed.CompareAndSwap(false, true) {
subsequentCloseWarn("cancelReadCloser")
return nil
}
p.closeWithError(io.EOF)
return nil
}
func subsequentCloseWarn(name string) {
log.G(context.TODO()).Error("subsequent attempt to close " + name)
if log.GetLevel() >= log.DebugLevel {
log.G(context.TODO()).Errorf("stack trace: %s", string(debug.Stack()))
}
}

View File

@ -1,96 +0,0 @@
package ioutils
import (
"io"
"sync"
)
// WriteFlusher wraps the Write and Flush operation ensuring that every write
// is a flush. In addition, the Close method can be called to intercept
// Read/Write calls if the targets lifecycle has already ended.
type WriteFlusher struct {
w io.Writer
flusher flusher
flushed chan struct{}
flushedOnce sync.Once
closed chan struct{}
closeLock sync.Mutex
}
type flusher interface {
Flush()
}
func (wf *WriteFlusher) Write(b []byte) (int, error) {
select {
case <-wf.closed:
return 0, io.EOF
default:
}
n, err := wf.w.Write(b)
wf.Flush() // every write is a flush.
return n, err
}
// Flush the stream immediately.
func (wf *WriteFlusher) Flush() {
select {
case <-wf.closed:
return
default:
}
wf.flushedOnce.Do(func() {
close(wf.flushed)
})
wf.flusher.Flush()
}
// Flushed returns the state of flushed.
// If it's flushed, return true, or else it return false.
func (wf *WriteFlusher) Flushed() bool {
// BUG(stevvooe): Remove this method. Its use is inherently racy. Seems to
// be used to detect whether or a response code has been issued or not.
// Another hook should be used instead.
var flushed bool
select {
case <-wf.flushed:
flushed = true
default:
}
return flushed
}
// Close closes the write flusher, disallowing any further writes to the
// target. After the flusher is closed, all calls to write or flush will
// result in an error.
func (wf *WriteFlusher) Close() error {
wf.closeLock.Lock()
defer wf.closeLock.Unlock()
select {
case <-wf.closed:
return io.EOF
default:
close(wf.closed)
}
return nil
}
// nopFlusher represents a type which flush operation is nop.
type nopFlusher struct{}
// Flush is a nop operation.
func (f *nopFlusher) Flush() {}
// NewWriteFlusher returns a new WriteFlusher.
func NewWriteFlusher(w io.Writer) *WriteFlusher {
var fl flusher
if f, ok := w.(flusher); ok {
fl = f
} else {
fl = &nopFlusher{}
}
return &WriteFlusher{w: w, flusher: fl, closed: make(chan struct{}), flushed: make(chan struct{})}
}

View File

@ -1,28 +0,0 @@
package ioutils
import (
"io"
"sync/atomic"
)
type writeCloserWrapper struct {
io.Writer
closer func() error
closed atomic.Bool
}
func (r *writeCloserWrapper) Close() error {
if !r.closed.CompareAndSwap(false, true) {
subsequentCloseWarn("WriteCloserWrapper")
return nil
}
return r.closer()
}
// NewWriteCloserWrapper returns a new io.WriteCloser.
func NewWriteCloserWrapper(r io.Writer, closer func() error) io.WriteCloser {
return &writeCloserWrapper{
Writer: r,
closer: closer,
}
}

View File

@ -1,42 +0,0 @@
// Package longpath introduces some constants and helper functions for handling
// long paths in Windows.
//
// Long paths are expected to be prepended with "\\?\" and followed by either a
// drive letter, a UNC server\share, or a volume identifier.
package longpath
import (
"os"
"runtime"
"strings"
)
// longPathPrefix is the longpath prefix for Windows file paths.
const longPathPrefix = `\\?\`
// AddPrefix adds the Windows long path prefix to the path provided if
// it does not already have it.
func AddPrefix(path string) string {
if strings.HasPrefix(path, longPathPrefix) {
return path
}
if strings.HasPrefix(path, `\\`) {
// This is a UNC path, so we need to add 'UNC' to the path as well.
return longPathPrefix + `UNC` + path[1:]
}
return longPathPrefix + path
}
// MkdirTemp is the equivalent of [os.MkdirTemp], except that on Windows
// the result is in Windows longpath format. On Unix systems it is
// equivalent to [os.MkdirTemp].
func MkdirTemp(dir, prefix string) (string, error) {
tempDir, err := os.MkdirTemp(dir, prefix)
if err != nil {
return "", err
}
if runtime.GOOS != "windows" {
return tempDir, nil
}
return AddPrefix(tempDir), nil
}

View File

@ -1,63 +0,0 @@
// Package stringid provides helper functions for dealing with string identifiers
package stringid
import (
"crypto/rand"
"encoding/hex"
"strings"
)
const (
shortLen = 12
fullLen = 64
)
// TruncateID returns a shorthand version of a string identifier for convenience.
// A collision with other shorthands is very unlikely, but possible.
// In case of a collision a lookup with TruncIndex.Get() will fail, and the caller
// will need to use a longer prefix, or the full-length Id.
func TruncateID(id string) string {
if i := strings.IndexRune(id, ':'); i >= 0 {
id = id[i+1:]
}
if len(id) > shortLen {
id = id[:shortLen]
}
return id
}
// GenerateRandomID returns a unique, 64-character ID consisting of a-z, 0-9.
// It guarantees that the ID, when truncated ([TruncateID]) does not consist
// of numbers only, so that the truncated ID can be used as hostname for
// containers.
func GenerateRandomID() string {
b := make([]byte, 32)
for {
if _, err := rand.Read(b); err != nil {
panic(err) // This shouldn't happen
}
id := hex.EncodeToString(b)
// make sure that the truncated ID does not consist of only numeric
// characters, as it's used as default hostname for containers.
//
// See:
// - https://github.com/moby/moby/issues/3869
// - https://bugzilla.redhat.com/show_bug.cgi?id=1059122
if allNum(id[:shortLen]) {
// all numbers; try again
continue
}
return id
}
}
// allNum checks whether id consists of only numbers (0-9).
func allNum(id string) bool {
for _, c := range []byte(id) {
if c > '9' || c < '0' {
return false
}
}
return true
}

5
vendor/modules.txt vendored
View File

@ -65,7 +65,7 @@ github.com/docker/distribution/registry/client/transport
github.com/docker/distribution/registry/storage/cache
github.com/docker/distribution/registry/storage/cache/memory
github.com/docker/distribution/uuid
# github.com/docker/docker v28.3.0-rc.1.0.20250620162235-265f70964794+incompatible
# github.com/docker/docker v28.3.1+incompatible
## explicit
github.com/docker/docker/api
github.com/docker/docker/api/types
@ -95,14 +95,11 @@ github.com/docker/docker/client
github.com/docker/docker/internal/lazyregexp
github.com/docker/docker/internal/multierror
github.com/docker/docker/pkg/homedir
github.com/docker/docker/pkg/ioutils
github.com/docker/docker/pkg/jsonmessage
github.com/docker/docker/pkg/longpath
github.com/docker/docker/pkg/process
github.com/docker/docker/pkg/progress
github.com/docker/docker/pkg/stdcopy
github.com/docker/docker/pkg/streamformatter
github.com/docker/docker/pkg/stringid
github.com/docker/docker/registry
# github.com/docker/docker-credential-helpers v0.9.3
## explicit; go 1.21