Compare commits

..
111 Commits
Author SHA1 Message Date
Paweł Gronowski 43987fca48 Merge pull request #5687 from thaJeztah/25.0_backport_bump_xx
[25.0 backport] update xx to v1.6.1 for compatibility with alpine 3.21
2024-12-18 10:25:17 +00:00
Sebastiaan van Stijn cdbfdc6025 update xx to v1.6.1 for compatibility with alpine 3.21
This fixes compatibility with alpine 3.21

- Fix additional possible `xx-cc`/`xx-cargo` compatibility issue with Alpine 3.21
- Support for Alpine 3.21
- Fix `xx-verify` with `file` 5.46+
- Fix possible error taking lock in `xx-apk` in latest Alpine without `coreutils`

full diff: https://github.com/tonistiigi/xx/compare/v1.5.0...v1.6.1

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 1e51ae7af2)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-12-13 01:06:21 +01:00
Sebastiaan van Stijn 4b0e7ba9db Dockerfile: update xx to v1.5.0
full diff: https://github.com/tonistiigi/xx/compare/v1.4.0...v1.5.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 1e6cbbc3f1)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-12-13 01:05:57 +01:00
CrazyMax d661d0449f Dockerfile: update to xx 1.4.0
Signed-off-by: CrazyMax <crazy-max@users.noreply.github.com>
(cherry picked from commit adb018084c)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-12-13 01:05:33 +01:00
Sebastiaan van Stijn 57ec2c7d72 Merge pull request #5680 from austinvazquez/cherry-pick-3284a80b05ea0e25b33cfd8ed383ee105d18a5bd-to-25.0
[25.0] update to go1.22.10
2024-12-08 17:40:33 +01:00
Sebastiaan van Stijn 190ebb5036 update to go1.22.10
go1.22.10 (released 2024-12-03) includes fixes to the runtime and the syscall
package. See the Go 1.22.10 milestone on our issue tracker for details.

- https://github.com/golang/go/issues?q=milestone%3AGo1.22.10+label%3ACherryPickApproved
- full diff: https://github.com/golang/go/compare/go1.22.9...go1.22.10

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 3284a80b05)
Signed-off-by: Austin Vazquez <macedonv@amazon.com>
2024-12-07 09:26:18 -07:00
Sebastiaan van Stijn ecccbb4e63 Merge pull request #5677 from austinvazquez/cherry-pick-9617e8d0ce8ef7d38bd2291eb7ba0da1a05fabc5-to-25.0
[25.0] gha: update to macOS 13, add macOS 14 arm64 (Apple Silicon M1)
2024-12-07 10:15:42 +01:00
Sebastiaan van Stijn 0a98cba34b gha: update to macOS 13, add macOS 14 arm64 (Apple Silicon M1)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 9617e8d0ce)
Signed-off-by: Austin Vazquez <macedonv@amazon.com>
2024-12-06 23:59:34 -07:00
Paweł Gronowski 64a322d6e1 Merge pull request #5642 from austinvazquez/cherry-pick-35c87e326c4f5c39cfd7473473cc4a5c6647c366-25.0
[25.0 backport] update to go1.22.9
2024-11-22 11:04:59 +01:00
Paweł Gronowski 8b446aa5d0 update to go1.22.9
- https://github.com/golang/go/issues?q=milestone%3AGo1.22.9+label%3ACherryPickApproved
- full diff: https://github.com/golang/go/compare/go1.22.8...go1.22.9

go1.22.9 (released 2024-11-06) includes fixes to the linker. See the
[Go 1.22.9 milestone](https://github.com/golang/go/issues?q=milestone%3AGo1.22.9+label%3ACherryPickApproved)
for details.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit 35c87e326c)
Signed-off-by: Austin Vazquez <macedonv@amazon.com>
2024-11-22 01:23:36 +00:00
Sebastiaan van Stijn 0bab007417 Merge pull request #5566 from austinvazquez/cherry-pick-baceb4b1586f38b9d6c031ab0a696f32c52852f3-to-25.0
[25.0 backport] docs, man: dockerd: add documentation for "--log-format" option
2024-10-21 22:02:31 +02:00
Sebastiaan van Stijn a0f4097740 docs: dockerd: add documentation for --log-format option
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit baceb4b158)
Signed-off-by: Austin Vazquez <macedonv@amazon.com>
2024-10-21 16:06:39 +00:00
Sebastiaan van Stijn 11634426e8 man: dockerd: add description for --log-format option
This option was added in a08abec9f8d59eaa44c375900e254384a68c5a31,
as part of Docker v25.0, but did not update the docs and manpage.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 9ae514fdc7)
Signed-off-by: Austin Vazquez <macedonv@amazon.com>
2024-10-21 16:06:30 +00:00
Sebastiaan van Stijn 315009cd65 Merge pull request #5511 from austinvazquez/cherry-pick-daea277ee839742be94e1f41d5c477f114a81273-to-25.0
[25.0 backport] volume/update: require 1 argument/fix panic
2024-10-08 10:49:37 +02:00
Sebastiaan van Stijn 45475c3ead Merge pull request #5514 from austinvazquez/cherry-pick-a6ab65948e6ecaf4167e2ef91b24c30929296256-to-25.0
[25.0 backport] ci: update to go1.22.8
2024-10-08 10:39:38 +02:00
Laura Brehm 024b3c1e9b volume/update: require 1 argument/fix panic
This command was declaring that it requires at least 1 argument, when it
needs exactly 1 argument. This was causing the CLI to panic when the
command was invoked with no argument:

`docker volume update`

Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit daea277ee8)
Signed-off-by: Austin Vazquez <macedonv@amazon.com>
2024-10-08 04:56:38 +00:00
Austin Vazquez 718cd79a8a ci: update to go1.22.8
Signed-off-by: Austin Vazquez <macedonv@amazon.com>
(cherry picked from commit a6ab65948e)
Signed-off-by: Austin Vazquez <macedonv@amazon.com>
2024-10-08 04:15:39 +00:00
Sebastiaan van Stijn 24c47bad80 gha: update codeql workflow to go1.22.7
commit d7d56599ca updated this
repository to go1.22, but the codeql action didn't specify a
patch version, and was missed.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit e1213edcc6)
Signed-off-by: Austin Vazquez <macedonv@amazon.com>
2024-10-08 04:14:48 +00:00
Paweł Gronowski 52037f602b update to go1.22.7
- https://github.com/golang/go/issues?q=milestone%3AGo1.22.7+label%3ACherryPickApproved
- full diff: https://github.com/golang/go/compare/go1.22.6...go1.22.7

These minor releases include 3 security fixes following the security policy:

- go/parser: stack exhaustion in all Parse* functions

    Calling any of the Parse functions on Go source code which contains deeply nested literals can cause a panic due to stack exhaustion.

    This is CVE-2024-34155 and Go issue https://go.dev/issue/69138.

- encoding/gob: stack exhaustion in Decoder.Decode

    Calling Decoder.Decode on a message which contains deeply nested structures can cause a panic due to stack exhaustion.

    This is a follow-up to CVE-2022-30635.

    Thanks to Md Sakib Anwar of The Ohio State University (anwar.40@osu.edu) for reporting this issue.

    This is CVE-2024-34156 and Go issue https://go.dev/issue/69139.

- go/build/constraint: stack exhaustion in Parse

    Calling Parse on a "// +build" build tag line with deeply nested expressions can cause a panic due to stack exhaustion.

    This is CVE-2024-34158 and Go issue https://go.dev/issue/69141.

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

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit 3bf39d25a0)
Signed-off-by: Austin Vazquez <macedonv@amazon.com>
2024-10-08 04:13:46 +00:00
Sebastiaan van Stijn 25adbb56af Merge pull request #5406 from austinvazquez/cherry-pick-go1.22.6-to-25.0
[25.0 backport] update to go1.22.6
2024-09-10 16:59:25 +02:00
Sebastiaan van Stijn b26009a92b update to go1.22.6
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit d7d56599ca)
Signed-off-by: Austin Vazquez <macedonv@amazon.com>
2024-09-10 14:44:50 +00:00
Paweł Gronowski 8a604b18a3 update to go1.21.13
- https://github.com/golang/go/issues?q=milestone%3AGo1.21.13+label%3ACherryPickApproved
- full diff: https://github.com/golang/go/compare/go1.21.12...go1.21.13

go1.21.13 (released 2024-08-06) includes fixes to the go command, the
covdata command, and the bytes package. See the [Go 1.21.13 milestone](https://github.com/golang/go/issues?q=milestone%3AGo1.21.13+label%3ACherryPickApproved)
on our issue tracker for details.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit 434d8b75e8)
Signed-off-by: Austin Vazquez <macedonv@amazon.com>
2024-09-10 14:44:47 +00:00
Sebastiaan van Stijn 1c5ff8a32d Merge pull request #5397 from thaJeztah/25.0_backport_fix_plugins_CGO_ENABLED
[25.0 backport] scripts/build/plugins: don't override CGO_ENABLED set by .variables
2024-09-10 16:37:58 +02:00
Sebastiaan van Stijn 06e1305fd7 scripts/build/plugins: don't override CGO_ENABLED set by .variables
The `.variables` sets `CGO_ENABLED=1` on arm; https://github.com/docker/cli/blob/b0c41b78d8c80c5f8faa736cf3c74dc31092e443/scripts/build/.variables#L57-L68
And if enabled, it sets `-buildmode=pie`; https://github.com/docker/cli/blob/b0c41b78d8c80c5f8faa736cf3c74dc31092e443/scripts/build/.variables#L79-L88

But that looks to be conflicting with the hardcoded `CGO_ENABLED=0` in
this script, which causes the build to fail on go1.22;

    > [build-plugins 1/1] RUN --mount=ro --mount=type=cache,target=/root/.cache     xx-go --wrap &&     TARGET=/out ./scripts/build/plugins e2e/cli-plugins/plugins/*:
    0.127 Building static docker-helloworld
    0.127 + CGO_ENABLED=0
    0.127 + GO111MODULE=auto
    0.127 + go build -o /out/plugins-linux-arm/docker-helloworld -tags ' osusergo' -ldflags ' -X "github.com/docker/cli/cli/version.GitCommit=5c123b1" -X "github.com/docker/cli/cli/version.BuildTime=2024-09-02T13:52:17Z" -X "github.com/docker/cli/cli/version.Version=pr-5387" -extldflags -static' -buildmode=pie github.com/docker/cli/cli-plugins/examples/helloworld
    0.135 -buildmode=pie requires external (cgo) linking, but cgo is not enabled

This patch sets the CGO_ENABLED variable before sourcing `.variables`,
so that other variables which are conditionally set are handled correctly.

Before this PR:

    #18 [build-plugins 1/1] RUN --mount=ro --mount=type=cache,target=/root/.cache     xx-go --wrap &&     TARGET=/out ./scripts/build/plugins e2e/cli-plugins/plugins/*
    #18 0.123 Building static docker-helloworld
    #18 0.124 + CGO_ENABLED=0
    #18 0.124 + GO111MODULE=auto
    #18 0.124 + go build -o /out/plugins-linux-arm/docker-helloworld -tags ' osusergo' -ldflags ' -X "github.com/docker/cli/cli/version.GitCommit=c8c402e" -X "github.com/docker/cli/cli/version.BuildTime=2024-09-03T08:28:25Z" -X "github.com/docker/cli/cli/version.Version=pr-5381" -extldflags -static' -buildmode=pie github.com/docker/cli/cli-plugins/examples/helloworld
    ....

With this PR:

    #18 [build-plugins 1/1] RUN --mount=ro --mount=type=cache,target=/root/.cache     xx-go --wrap &&     TARGET=/out ./scripts/build/plugins e2e/cli-plugins/plugins/*
    #18 0.110 Building static docker-helloworld
    #18 0.110 + GO111MODULE=auto
    #18 0.110 + go build -o /out/plugins-linux-arm/docker-helloworld -tags '' -ldflags ' -X "github.com/docker/cli/cli/version.GitCommit=050d9d6" -X "github.com/docker/cli/cli/version.BuildTime=2024-09-03T09:19:05Z" -X "github.com/docker/cli/cli/version.Version=pr-5387"' github.com/docker/cli/cli-plugins/examples/helloworld
    ....

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 9e29967960)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-09-03 12:57:48 +02:00
Paweł Gronowski 32ac720d7a Merge pull request #5341 from thaJeztah/25.0_backport_fix_bps_limit
[25.0 backport] run: fix GetList return empty issue for throttledevice
2024-08-12 11:58:10 +02:00
Sebastiaan van Stijn ac68396abb Merge pull request #5336 from vvoland/5310-25.0
[25.0 backport] gha: set permissions to read-only by default
2024-08-09 20:02:39 +02:00
Jianyong Wu a73610dc4f run: fix GetList return empty issue for throttledevice
Test "--device-read-bps" "--device-write-bps" will fail. The root
cause is that GetList helper return empty as its local variable
initialized to zero size.

This patch fix it by setting the related slice size to non-zero.

Signed-off-by: Jianyong Wu <wujianyong@hygon.cn>
Fixes: #5321
(cherry picked from commit 73e78a5822)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-08-09 19:48:04 +02:00
Sebastiaan van Stijn 1924acea45 gha: set permissions to read-only by default
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit e4d99b4b60)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-08-09 10:45:58 +02:00
Laura Brehm 32b99dd6a3 Merge pull request #5292 from laurazard/25-backport-flaky-tests
[25.0 backport] fix flaky `connhelper` tests
2024-07-24 12:26:41 +01:00
Laura Brehm 6fb9a5b264 tests: fix other flaky connhelper tests
Follow up to cc68c66c95 (there were more
tests with incorrect syntax).

Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit 4a7388f0dd)
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-07-24 12:15:11 +01:00
Laura Brehm 956c112f16 tests: fix flaxy TestCloseRunningCommand test
Looks like this test was failing due to bad syntax on the `while` loop,
which caused it to die after 1 second. If the test took a bit longer,
the process would be dead before the following assertions run, causing
the test to fail/be flaky.

Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit cc68c66c95)
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-07-24 12:12:16 +01:00
Sebastiaan van Stijn 0e8f5236d1 Merge pull request #5279 from austinvazquez/vendor-golang.org-updates-to-25.0
[25.0 backport] vendor: golang.org/x/sys v0.18.0, golang.org/x/term v0.18.0, golang.org/x/crypto v0.21.0, golang.org/x/net v0.23.0
2024-07-23 13:28:37 +02:00
Sebastiaan van Stijn 02b482013c vendor: golang.org/x/net v0.23.0
full diff: https://github.com/golang/net/compare/v0.22.0...v0.23.0

Includes a fix for CVE-2023-45288, which is also addressed in go1.22.2
and go1.21.9;

> http2: close connections when receiving too many headers
>
> Maintaining HPACK state requires that we parse and process
> all HEADERS and CONTINUATION frames on a connection.
> When a request's headers exceed MaxHeaderBytes, we don't
> allocate memory to store the excess headers but we do
> parse them. This permits an attacker to cause an HTTP/2
> endpoint to read arbitrary amounts of data, all associated
> with a request which is going to be rejected.
>
> Set a limit on the amount of excess header frames we
> will process before closing a connection.
>
> Thanks to Bartek Nowotarski for reporting this issue.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 5fcbbde4b9)
Signed-off-by: Austin Vazquez <macedonv@amazon.com>
2024-07-22 17:01:43 +00:00
Sebastiaan van Stijn e2dad1bd3f vendor: golang.org/x/net v0.22.0, golang.org/x/crypto v0.21.0
full diffs changes relevant to vendored code:

- https://github.com/golang/net/compare/v0.19.0...v0.22.0
    - http2: remove suspicious uint32->v conversion in frame code
    - http2: send an error of FLOW_CONTROL_ERROR when exceed the maximum octets
- https://github.com/golang/crypto/compare/v0.17.0...v0.21.0
    - (no changes in vendored code)

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 4745b957d2)
Signed-off-by: Austin Vazquez <macedonv@amazon.com>
2024-07-22 17:01:43 +00:00
Sebastiaan van Stijn df5d652d99 vendor: golang.org/x/term v0.18.0
no changes in vendored code

full diff: https://github.com/golang/term/compare/v0.15.0...v0.18.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit c7a50ebb9f)
Signed-off-by: Austin Vazquez <macedonv@amazon.com>
2024-07-22 17:01:43 +00:00
Sebastiaan van Stijn 82a04c86b3 vendor: golang.org/x/sys v0.18.0
full diff: https://github.com/golang/sys/compare/v0.16.0...v0.18.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 9a2133f2d4)
Signed-off-by: Austin Vazquez <macedonv@amazon.com>
2024-07-22 17:01:40 +00:00
Sebastiaan van Stijn 4c0e910f61 Merge pull request #5278 from austinvazquez/require-changelog-in-25.0
[25.0 backport] Require changelog in 25.0
2024-07-22 18:04:35 +02:00
Sebastiaan van Stijn 5b9ca94a89 Merge pull request #5277 from austinvazquez/fix-codeql-2.16-in-25.0
[25.0 backport] Fix codeql 2.16 in 25.0
2024-07-22 17:49:10 +02:00
Sebastiaan van Stijn a9126aac28 Merge pull request #5276 from austinvazquez/update-to-go-1.21.12-in-25.0
[25.0 backport] Update to go 1.21.12 in 25.0
2024-07-22 17:19:40 +02:00
Paweł Gronowski 26850c6a89 ci/validate-pr: Use ::error:: command to print errors
This will make Github render the log line as an error.

(copied from moby/moby fb92caf2aa6cf3664e11dc06ee10d114af300826)

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit c3243a8cc3)
Signed-off-by: Austin Vazquez <macedonv@amazon.com>
2024-07-22 15:10:19 +00:00
Paweł Gronowski 73b9f1c0fb github/ci: Check if backport is opened against the expected branch
(copied from moby/moby 61269e718fbdbbad397b0089105ec910fc0e62ca)

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit f92fcdef1b)
Signed-off-by: Austin Vazquez <macedonv@amazon.com>
2024-07-22 15:10:11 +00:00
Paweł Gronowski de7a473c43 ci: Require changelog description
Any PR that is labeled with any `impact/*` label should have a
description for the changelog and an `area/*` label.

(copied from moby/moby 1d473549e865ef6b90ee936c280f4bda677de39b)

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit 745704d7b4)
Signed-off-by: Austin Vazquez <macedonv@amazon.com>
2024-07-22 15:10:02 +00:00
Sebastiaan van Stijn ef3b190da3 ci: set DISABLE_WARN_OUTSIDE_CONTAINER=1 for CodeQL action
CodeQL autobuild uses the makefile, but outside of a container, so let's
set this variable to prevent it having to wait 10 seconds;

    Use "make dev" to start an interactive development container,
    use "make -f docker.Makefile " to execute this target
    in a container, or set DISABLE_WARN_OUTSIDE_CONTAINER=1 to
    disable this warning.

    Press Ctrl+C now to abort, or wait for the script to continue..

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit b120b96ac7)
Signed-off-by: Austin Vazquez <macedonv@amazon.com>
2024-07-22 15:03:53 +00:00
Sebastiaan van Stijn 7e4a7b5477 ci: fix CodeQL 2.16.4 autobuild
CodeQL 2.16.4's auto-build added support for multi-module repositories,
and is trying to be smart by searching for modules in every directory,
including vendor directories. If no module is found, it's creating one
which is ... not what we want, so let's give it a "go.mod".

Here's from a run in CI;

    /opt/hostedtoolcache/CodeQL/2.16.4/x64/codeql/codeql version --format=json
    {
      "productName" : "CodeQL",
      "vendor" : "GitHub",
      "version" : "2.16.4",
      "sha" : "9727ba3cd3d5a26f8b9347bf3c3eb4f565ac077b",
      "branches" : [
        "codeql-cli-2.16.4"
      ],
      "copyright" : "Copyright (C) 2019-2024 GitHub, Inc.",
      "unpackedLocation" : "/opt/hostedtoolcache/CodeQL/2.16.4/x64/codeql",
      "configFileLocation" : "/home/runner/.config/codeql/config",
      "configFileFound" : false,
      "features" : {
        "analysisSummaryV2Option" : true,
        "buildModeOption" : true,
        "bundleSupportsIncludeDiagnostics" : true,
        "featuresInVersionResult" : true,
        "indirectTracingSupportsStaticBinaries" : false,
        "informsAboutUnsupportedPathFilters" : true,
        "supportsPython312" : true,
        "mrvaPackCreate" : true,
        "threatModelOption" : true,
        "traceCommandUseBuildMode" : true,
        "v2ramSizing" : true,
        "mrvaPackCreateMultipleQueries" : true,
        "setsCodeqlRunnerEnvVar" : true
      }
    }

With 2.16.4, first it is unable to correlate files with the project, considering
them "stray" files;

    Attempting to automatically build go code
    /opt/hostedtoolcache/CodeQL/2.16.4/x64/codeql/go/tools/autobuild.sh
    2024/03/16 15:54:34 Autobuilder was built with go1.22.0, environment has go1.21.8
    2024/03/16 15:54:34 LGTM_SRC is /home/runner/work/cli/cli
    2024/03/16 15:54:34 Found no go.work files in the workspace; looking for go.mod files...
    2024/03/16 15:54:34 Found stray Go source file in cli/cobra.go.
    2024/03/16 15:54:34 Found stray Go source file in cli/cobra_test.go.
    2024/03/16 15:54:34 Found stray Go source file in cli/command/builder/client_test.go.
    2024/03/16 15:54:34 Found stray Go source file in cli/command/builder/cmd.go.
    ...

It then tries to build the binary, but in go modules mode, which fails (it also
seems to be doing this for each and every directory);

    Use "make dev" to start an interactive development container,
    use "make -f docker.Makefile " to execute this target
    in a container, or set DISABLE_WARN_OUTSIDE_CONTAINER=1 to
    disable this warning.

    Press Ctrl+C now to abort, or wait for the script to continue..

    ./scripts/build/binary
    Building static docker-linux-amd64
    + go build -o build/docker-linux-amd64 -tags  osusergo pkcs11 -ldflags  -X "github.com/docker/cli/cli/version.GitCommit=38c3ff6" -X "github.com/docker/cli/cli/version.BuildTime=2024-03-16T17:20:38Z" -X "github.com/docker/cli/cli/version.Version=38c3ff6.m" -extldflags -static -buildmode=pie github.com/docker/cli/cmd/docker
    cannot find package "github.com/docker/cli/cmd/docker" in any of:
        /opt/hostedtoolcache/go/1.21.8/x64/src/github.com/docker/cli/cmd/docker (from $GOROOT)
        /home/runner/go/src/github.com/docker/cli/cmd/docker (from $GOPATH)
    make: *** [Makefile:62: binary] Error 1
    2024/03/16 17:20:38 Running /usr/bin/make [make] failed, continuing anyway: exit status 2
    2024/03/16 17:20:38 Build failed, continuing to install dependencies.
    2024/03/16 17:20:38 The code in vendor/gotest.tools/v3/skip seems to be missing a go.mod file. Attempting to initialize one...
    2024/03/16 17:20:38 Import path is 'github.com/docker/cli'

If also seems to be doing this for ... every package?

    cat 0_codeql.log | grep 'you are not in a container' | wc -l
    497

After which it starts to create modules out of every directory;

    The code in internal/test/network seems to be missing a go.mod file. Attempting to initialize one...
    The code in internal/test/notary seems to be missing a go.mod file. Attempting to initialize one...
    The code in internal/test/output seems to be missing a go.mod file. Attempting to initialize one...
    The code in opts seems to be missing a go.mod file. Attempting to initialize one...
    The code in service seems to be missing a go.mod file. Attempting to initialize one...
    The code in service/logs seems to be missing a go.mod file. Attempting to initialize one...
    The code in templates seems to be missing a go.mod file. Attempting to initialize one...
    The code in vendor seems to be missing a go.mod file. Attempting to initialize one...
    The code in vendor/dario.cat seems to be missing a go.mod file. Attempting to initialize one...
    The code in vendor/dario.cat/mergo seems to be missing a go.mod file. Attempting to initialize one...
    ...
    Skipping dependency package regexp.
    Skipping dependency package github.com/opencontainers/go-digest.
    Skipping dependency package github.com/distribution/reference.
    Extracting /home/runner/work/cli/cli/cli/command/go.mod
    Done extracting /home/runner/work/cli/cli/cli/command/go.mod (1ms)
    Extracting /home/runner/work/cli/cli/cli/command/go.mod
    Done extracting /home/runner/work/cli/cli/cli/command/go.mod (0ms)
    Extracting /home/runner/work/cli/cli/cli/command/go.mod
    Done extracting /home/runner/work/cli/cli/cli/command/go.mod (0ms)

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 24186d8008)
Signed-off-by: Austin Vazquez <macedonv@amazon.com>
2024-07-22 15:03:39 +00:00
Paweł Gronowski 0dd60b064f update to go1.21.12
- https://github.com/golang/go/issues?q=milestone%3AGo1.21.12+label%3ACherryPickApproved
- full diff: https://github.com/golang/go/compare/go1.21.11...go1.21.12

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

net/http: denial of service due to improper 100-continue handling

The net/http HTTP/1.1 client mishandled the case where a server responds to a request with an "Expect: 100-continue" header with a non-informational (200 or higher) status. This mishandling could leave a client connection in an invalid state, where the next request sent on the connection will fail.

An attacker sending a request to a net/http/httputil.ReverseProxy proxy can exploit this mishandling to cause a denial of service by sending "Expect: 100-continue" requests which elicit a non-informational response from the backend. Each such request leaves the proxy with an invalid connection, and causes one subsequent request using that connection to fail.

Thanks to Geoff Franks for reporting this issue.

This is CVE-2024-24791 and Go issue https://go.dev/issue/67555.
View the release notes for more information:
https://go.dev/doc/devel/release#go1.21.12

**- Description for the changelog**

```markdown changelog
Update Go runtime to 1.21.12
```

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit d73d7d4ed3)
Signed-off-by: Austin Vazquez <macedonv@amazon.com>
2024-07-22 14:53:01 +00:00
Sebastiaan van Stijn a90d08534b Dockerfile: update ALPINE_VERSION to 3.20
Update to the current version of Alpine, which is also the default for
the golang:alpine image

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit e70f68595d)
Signed-off-by: Austin Vazquez <macedonv@amazon.com>
2024-07-22 14:52:15 +00:00
Sebastiaan van Stijn 1fbc90faf7 update to go1.21.11
go1.21.11 (released 2024-06-04) includes security fixes to the archive/zip
and net/netip packages, as well as bug fixes to the compiler, the go command,
the runtime, and the os package. See the Go 1.21.11 milestone on our issue
tracker for details;

- https://github.com/golang/go/issues?q=milestone%3AGo1.21.11+label%3ACherryPickApproved
- full diff: https://github.com/golang/go/compare/go1.21.10...go1.21.11

From the security announcement;

We have just released Go versions 1.22.4 and 1.21.11, minor point releases.
These minor releases include 2 security fixes following the security policy:

- archive/zip: mishandling of corrupt central directory record

  The archive/zip package's handling of certain types of invalid zip files
  differed from the behavior of most zip implementations. This misalignment
  could be exploited to create an zip file with contents that vary depending
  on the implementation reading the file. The archive/zip package now rejects
  files containing these errors.

  Thanks to Yufan You for reporting this issue.

  This is CVE-2024-24789 and Go issue https://go.dev/issue/66869.

- net/netip: unexpected behavior from Is methods for IPv4-mapped IPv6 addresses

  The various Is methods (IsPrivate, IsLoopback, etc) did not work as expected
  for IPv4-mapped IPv6 addresses, returning false for addresses which would
  return true in their traditional IPv4 forms.

  Thanks to Enze Wang of Alioth and Jianjun Chen of Zhongguancun Lab
  for reporting this issue.

  This is CVE-2024-24790 and Go issue https://go.dev/issue/67680.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 630e1d3e95)
Signed-off-by: Austin Vazquez <macedonv@amazon.com>
2024-07-22 14:44:01 +00:00
Paweł Gronowski c5aee98be7 update to go1.21.10
These minor releases include 2 security fixes following the security policy:

- cmd/go: arbitrary code execution during build on darwin
On Darwin, building a Go module which contains CGO can trigger arbitrary code execution when using the Apple version of ld, due to
usage of the -lto_library flag in a "#cgo LDFLAGS" directive.
Thanks to Juho Forsén of Mattermost for reporting this issue.
This is CVE-2024-24787 and Go issue https://go.dev/issue/67119.

- net: malformed DNS message can cause infinite loop
A malformed DNS message in response to a query can cause the Lookup functions to get stuck in an infinite loop.
Thanks to long-name-let-people-remember-you on GitHub for reporting this issue, and to Mateusz Poliwczak for bringing the issue to
our attention.
This is CVE-2024-24788 and Go issue https://go.dev/issue/66754.

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

- https://github.com/golang/go/issues?q=milestone%3AGo1.21.10+label%3ACherryPickApproved
- full diff: https://github.com/golang/go/compare/go1.21.9...go1.21.10

**- Description for the changelog**

```markdown changelog
Update Go runtime to 1.21.10
```

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit eb99994c75)
Signed-off-by: Austin Vazquez <macedonv@amazon.com>
2024-07-22 14:41:44 +00:00
Sebastiaan van Stijn bc7a15921e Merge pull request #5106 from laurazard/update-actions-25.0
[25.0 backport] gha: update to actions/upload-artifact@v4
2024-06-03 16:47:47 +02:00
Sebastiaan van Stijn d379797cec gha: update to actions/upload-artifact@v4
v3 is using Node.js 16 which are being deprecated:

    Node.js 16 actions are deprecated. Please update the following actions to use Node.js 20: actions/upload-artifact@v3. For more information see: https://github.blog/changelog/2023-09-22-github-actions-transitioning-from-node-16-to-node-20/.

ci: incl. platform pair in artifact name

This fixes an the issue w/ `upload-artifact@v4`.
See: https://github.blog/2024-02-12-get-started-with-v4-of-github-actions-artifacts/#compatibility

Co-authored-by: Laura Brehm <laurabrehm@hey.com>
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit b9cd722595)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-06-03 16:43:15 +02:00
Laura Brehm 7e7b0ee8e6 Merge pull request #4988 from vvoland/v25.0-4986
[25.0 backport] update to go1.21.9
2024-04-05 15:48:30 +01:00
Paweł Gronowski f2918727a6 update to go1.21.9
go1.21.9 (released 2024-04-03) includes a security fix to the net/http
package, as well as bug fixes to the linker, and the go/types and
net/http packages. See the Go 1.21.9 milestone on our issue tracker for
details.

- https://github.com/golang/go/issues?q=milestone%3AGo1.21.9+label%3ACherryPickApproved
- full diff: https://github.com/golang/go/compare/go1.21.8...go1.21.9

**- Description for the changelog**

```markdown changelog
Update Go runtime to 1.21.9
```

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit 0a5bd6c75b)
2024-04-05 12:45:15 +02:00
Paweł Gronowski 5dc9bcc5b7 Merge pull request #4951 from vvoland/vendor-docker-25.0.5-dev
vendor: github.com/docker/docker e63daec8672d (v25.0.5-dev)
2024-03-19 15:51:10 +01:00
Paweł Gronowski c2be159764 vendor: github.com/docker/docker e63daec8672d (v25.0.5-dev)
full diff: https://github.com/docker/docker/compare/061aa95809be...e63daec8672d

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-03-19 15:43:45 +01:00
Bjorn Neergaard 1a576c50a9 Merge pull request #4924 from vvoland/v25.0-4923
[25.0 backport] bake: Add `windows/arm64` target to bin-image-cross
2024-03-06 09:08:42 -07:00
Paweł Gronowski 690b1565fb bake: Add windows/arm64 target to bin-image-cross
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit ab9d560570)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-03-06 16:48:25 +01:00
Sebastiaan van Stijn 03114ec2ca Merge pull request #4921 from vvoland/vendor-docker
vendor: github.com/docker/docker 061aa95809be396a6
2024-03-06 15:33:35 +01:00
Paweł Gronowski 833128bce5 vendor: github.com/docker/docker 061aa95809be396a6
no change in vendored files

full diff: https://github.com/docker/docker/compare/9e526bc3943c...061aa95809be396a6

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-03-06 15:08:04 +01:00
Sebastiaan van Stijn fd4d39aa88 Merge pull request #4920 from vvoland/vendor-docker
[25.0] vendor: github.com/docker/docker 9e526bc3943c
2024-03-05 22:25:25 +01:00
Sebastiaan van Stijn b4b35dedc6 Merge pull request #4919 from vvoland/v25.0-4918
[25.0 backport] update to go1.21.8
2024-03-05 22:24:46 +01:00
Paweł Gronowski ce113a74af vendor: github.com/docker/docker 9e526bc3943c
no change in vendored files

full diff: https://github.com/docker/docker/compare/51e876cd964c...9e526bc3943c

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-03-05 22:15:37 +01:00
Paweł Gronowski a3b6c9ea7e update to go1.21.8
go1.21.8 (released 2024-03-05) includes 5 security fixes:

- crypto/x509: Verify panics on certificates with an unknown public key algorithm (CVE-2024-24783, https://go.dev/issue/65390)
- net/http: memory exhaustion in Request.ParseMultipartForm (CVE-2023-45290, https://go.dev/issue/65383)
- net/http, net/http/cookiejar: incorrect forwarding of sensitive headers and cookies on HTTP redirect (CVE-2023-45289, https://go.dev/issue/65065)
- html/template: errors returned from MarshalJSON methods may break template escaping (CVE-2024-24785, https://go.dev/issue/65697)
- net/mail: comments in display names are incorrectly handled (CVE-2024-24784, https://go.dev/issue/65083)

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

- https://github.com/golang/go/issues?q=milestone%3AGo1.21.8+label%3ACherryPickApproved
- full diff: https://github.com/golang/go/compare/go1.21.6...go1.21.8

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit 3b77477943)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-03-05 22:09:26 +01:00
Sebastiaan van Stijn 2bf4225ad2 Merge pull request #4908 from vvoland/vendor-docker
[25.0] vendor: github.com/docker/docker 25.0.4-51e876cd964c4bb1f0a7c1bc24ecab9321b3ff1c
2024-03-05 17:13:10 +01:00
Sebastiaan van Stijn f783e8d58a Merge pull request #4915 from vvoland/v25.0-4839
[25.0 backport] update CI
2024-03-05 13:01:56 +01:00
Christopher Petito 956d15c723 Cleanup of dockerfiles, compose files and env vars
Signed-off-by: Christopher Petito <chrisjpetito@gmail.com>
(cherry picked from commit 69ed6588a8)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-03-05 09:56:21 +01:00
Christopher Petito 5a942fadcf Update gha runners and engines used in e2e tests
- gha runners updated to ubuntu 22.04
- e2e now runs against moby 23.0, 24.0 and 25.0
- temporarily skip broken test for moby < 25

Signed-off-by: Christopher Petito <chrisjpetito@gmail.com>
(cherry picked from commit 6b67b95493)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-03-05 09:56:19 +01:00
Paweł Gronowski 592c146cca testenv: Add DaemonAPIVersion helper
Allow tests to check the negotiated API version used by the client.

Can be used to skip tests based on API versions, for example:
```go
    skip.If(t, versions.LessThan(environment.DaemonAPIVersion(t), "1.44"))
```

will skip the test if the API version is older than 1.44

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
(cherry picked from commit 9831fea4db)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-03-05 09:56:16 +01:00
Paweł Gronowski 0735e78cc9 vendor: github.com/docker/docker 25.0.4-51e876cd96
full diff: https://github.com/docker/docker/compare/v25.0.3...51e876cd964c4bb1f0a7c1bc24ecab9321b3ff1c

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-03-04 12:58:21 +01:00
Sebastiaan van Stijn 63a3db4b31 Merge pull request #4914 from vvoland/v25.0-4831
[25.0 backport] Dockerfile: update docker compose to v2.24.3
2024-03-04 12:50:24 +01:00
Sebastiaan van Stijn 0b9bf6a6f4 Merge pull request #4913 from vvoland/v25-4867
[25.0 backport] Test fixes needed for upgrading ci runners and engine
2024-03-04 12:49:50 +01:00
Sebastiaan van Stijn e0dab5ce1e Dockerfile: update docker compose to v2.24.3
Update the version of compose used in CI to the latest version.

- full diff: https://github.com/docker/compose/compare/v2.24.2...v2.24.3
- release notes: https://github.com/docker/compose/releases/tag/v2.24.2

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 53e2e54c29)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-03-04 12:32:16 +01:00
Sebastiaan van Stijn b59204cc43 Merge pull request #4912 from vvoland/v25-4881
[25.0 backport] update to go1.21.7
2024-03-04 12:20:28 +01:00
Sebastiaan van Stijn b8459ce351 Merge pull request #4911 from vvoland/v25-4876
[25.0 backport] Fixed typo in bash completion functions
2024-03-04 12:19:52 +01:00
Christopher Petito a25a9100f3 Minor test fixes necessary for eventually upgrading ci runners and engine version
Signed-off-by: Christopher Petito <chrisjpetito@gmail.com>
(cherry picked from commit 30dd7c1319)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-03-04 12:19:36 +01:00
Sebastiaan van Stijn eb223e7eaf Merge pull request #4910 from vvoland/v25-4860
[25.0 backport] docker stack: allow '=' separator in extra_hosts
2024-03-04 12:19:04 +01:00
Sebastiaan van Stijn c87c4c96ec update to go1.21.7
go1.21.7 (released 2024-02-06) includes fixes to the compiler, the go command,
the runtime, and the crypto/x509 package. See the Go 1.21.7 milestone on our
issue tracker for details:

- https://github.com/golang/go/issues?q=milestone%3AGo1.21.7+label%3ACherryPickApproved
- full diff: https://github.com/golang/go/compare/go1.21.6...go1.21.7

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 20b9d489e0)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-03-04 12:15:36 +01:00
David le Blanc c270556d44 Fixed typo in bash completion functions
Signed-off-by: David le Blanc <david-le-blanc@users.noreply.github.com>
(cherry picked from commit c514003e69)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-03-04 12:12:55 +01:00
Sebastiaan van Stijn 98f603bdd1 Merge pull request #4909 from vvoland/v25-4862
[25.0 backport] Avoid keeping @docker_cli_[UUID] files
2024-03-04 11:55:42 +01:00
Rob Murray 1cddb2b03d docker stack: allow '=' separator in extra_hosts
extra_hosts in the compose file format allows '=' as a separator, and brackets
around IP addresses, the engine API doesn't.

So, transform the values when reading a compose file for 'docker stack'.

Signed-off-by: Rob Murray <rob.murray@docker.com>
(cherry picked from commit c986d09bca)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-03-04 11:55:30 +01:00
Kirill A. Korinsky 8715d9a33a Avoid keeping @docker_cli_[UUID] files
Seems that OpenBSD behaves like darwin and requires to unlink all
socket, after it was used.

Tested on OpenBSD 7.4

Signed-off-by: Kirill A. Korinsky <kirill@korins.ky>
(cherry picked from commit 2c214241fa)
Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
2024-03-04 11:34:38 +01:00
Sebastiaan van Stijn a5937c6043 Merge pull request #4885 from thaJeztah/25.0_backport_regenerate_mdddocs
[25.0 backport] docs: regenerate markdown
2024-02-21 11:58:13 +01:00
Sebastiaan van Stijn 9142b58351 docs: regenerate markdown
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit f2e98f9a93)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-02-21 11:34:20 +01:00
Sebastiaan van Stijn f67e569a8f Merge pull request #4883 from dvdksn/backport_docs_cli_reference_urlscheme
[25.0 backport] docs: update link targets
2024-02-21 09:32:16 +01:00
David Karlsson 08eba2246c docs: update url scheme for reference docs
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
(cherry picked from commit caf72655fb)
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-02-21 09:22:08 +01:00
David Karlsson 4fd2cf5f2d deps: update cli-docs-tool version (v0.7.0)
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
(cherry picked from commit e244044944)
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-02-21 09:22:04 +01:00
Sebastiaan van Stijn bdfe1645f5 Merge pull request #4858 from thaJeztah/25.0_vendor_docker_25.0.3
[25.0] vendor: github.com/docker/docker v25.0.3
2024-02-07 10:47:40 +01:00
Sebastiaan van Stijn e456704864 vendor: github.com/docker/docker v25.0.3
full diff: https://github.com/docker/docker/compare/v25.0.2...v25.0.3

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-02-07 02:20:23 +01:00
Sebastiaan van Stijn 4debf411d1 Merge pull request #4857 from thaJeztah/25.0_backport_codecov-action-4
[25.0 backport] build(deps): Bump codecov/codecov-action from 3 to 4
2024-02-06 21:42:40 +01:00
Sebastiaan van Stijn 5e6ce1bde1 Merge pull request #4856 from thaJeztah/25.0_backport_plugin-socket-tests
[25.0 backport] Add tests for CLI/plugin communication
2024-02-06 21:42:20 +01:00
dependabot[bot] 5428301e3f build(deps): Bump codecov/codecov-action from 3 to 4
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3 to 4.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
(cherry picked from commit b123ce6526)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-02-06 21:35:08 +01:00
Laura Brehm 1cbc218c05 tests: add plugin-socket-compatibility tests
Adds a new plugin to the e2e plugins that simulates an older
plugin binary and a test suite to ensure older plugin binaries
keep behaving the same with newer CLI versions.

Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit cfa9fef77d)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-02-06 21:31:55 +01:00
Laura Brehm 2f6b5ada71 scripts: don't hardcode architecture in e2e script
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit 1c4d6d85dd)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-02-06 21:31:55 +01:00
Laura Brehm d8e07c9c47 tests: add tests for cli-plugins/socket
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
(cherry picked from commit 469bfc05ed)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-02-06 21:31:52 +01:00
Sebastiaan van Stijn 5f1b610fc3 Merge pull request #4841 from thaJeztah/25.0_vendor_docker_25.0.2
[25.0] vendor: github.com/docker/docker v25.0.2
2024-02-01 16:32:18 +01:00
Sebastiaan van Stijn c105cd3ac2 Merge pull request #4837 from dvdksn/25.0_docs_backport_linode_volume_plugin
[25.0 Backport] docs: Add Linode docker volume plugin #4396
2024-02-01 04:15:42 +01:00
Sebastiaan van Stijn 62b2963b80 vendor: github.com/docker/docker v25.0.2
no changes in vendored code

full diff: https://github.com/docker/docker/compare/v25.0.1...v25.0.2

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-02-01 04:12:31 +01:00
Sebastiaan van Stijn 71f2b0d109 vendor: github.com/docker/docker v25.0.1
relevant changes:

- Fix isGitURL regular expression
- pkg/system: return even richer xattr errors

full diff: https://github.com/moby/moby/compare/v25.0.0...v25.0.1

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 4b1ed1f442)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-02-01 04:11:47 +01:00
Zhiwei Liang 617bc98c8d Add Linode docker volume plugin
Signed-off-by: Zhiwei Liang <zliang@akamai.com>
(cherry picked from commit 1f9573bb05)
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
2024-01-31 13:06:53 +01:00
Sebastiaan van Stijn 29cf629222 Merge pull request #4819 from dvdksn/25.0_backport_docs_host-gateway-ip_daemonjson
[25.0 backport] docs: update host-gateway-ip to use daemon.json instead of cli flag
2024-01-23 16:11:10 +01:00
David Karlsson 4caf4de039 docs: update host-gateway-ip to use daemon.json instead of cli flag
Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
(cherry picked from commit ec0a62436e)
2024-01-23 16:04:58 +01:00
Sebastiaan van Stijn 950ecd42fd Merge pull request #4815 from thaJeztah/25.0_backport_update_compose
[25.0 backport] Dockerfile: update docker compose to v2.24.2
2024-01-23 11:04:43 +01:00
Sebastiaan van Stijn 6ab4781bd0 Dockerfile: update docker compose to v2.24.2
Update the version of compose used in CI to the latest version.

- full diff: docker/compose@v2.24.0...v2.24.2
- release notes: https://github.com/docker/compose/releases/tag/v2.24.1
- release notes: https://github.com/docker/compose/releases/tag/v2.24.2

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 091af560ca)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-01-23 10:34:14 +01:00
Sebastiaan van Stijn e8852e8ed2 Merge pull request #4806 from thaJeztah/25.0_backport_socket-eof-return
[25.0 backport] socket: return from loop after EOF
2024-01-22 14:10:02 +01:00
Tonis Tiigi 4e097c643d socket: return from loop after EOF
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
(cherry picked from commit 8cd3b00420)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-01-20 13:45:39 +01:00
Sebastiaan van Stijn 01f9332618 Merge pull request #4803 from thaJeztah/25.0_backport_update_engine
[25.0 backport] vendor: github.com/docker/docker v25.0.0
2024-01-19 15:31:35 +01:00
Sebastiaan van Stijn 4cd8d5cf47 Merge pull request #4804 from dvdksn/backport_25.0_docs_fix_cli_broken_alias_links
[25.0 backport] docs: fix broken links to alias pages
2024-01-19 15:18:55 +01:00
David Karlsson 21c12847bf docs: move base command to docker.md
CLI reference for the base command was generated to cli.md

Changed it to docker.md to handle broken links.

Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
(cherry picked from commit d633890f91)
2024-01-19 15:09:15 +01:00
David Karlsson 22e1f2cbfa docs: fix broken links to alias pages
Alias pages redirect to the canonical names, but these pages still
linked to the aliases, causing broken links when building the docs site.

Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
(cherry picked from commit aff4649cb7)
2024-01-19 15:09:07 +01:00
Sebastiaan van Stijn 68abf14c15 vendor: github.com/docker/docker v25.0.0
full diff: https://github.com/docker/docker/compare/v25.0.0-rc.3...v25.0.0

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 337dd82d8b)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-01-19 15:07:25 +01:00
Sebastiaan van Stijn 85a5ee4cb0 Merge pull request #4801 from dvdksn/backport_25.0_docs_cdi
[25.0 backport] docs: add documentation for CDI
2024-01-19 15:05:59 +01:00
David Karlsson 9e1e07657a docs: add documentation for CDI
- Add section about cdi-spec-dirs daemon configuration
- Add subsection about cdi in --device section for docker run
- Update `docker info` output example

Signed-off-by: David Karlsson <35727626+dvdksn@users.noreply.github.com>
(cherry picked from commit 852d198bb5)
2024-01-19 14:58:18 +01:00
3504 changed files with 226510 additions and 322988 deletions
+19
View File
@@ -0,0 +1,19 @@
# This is a dummy CircleCI config file to avoid GitHub status failures reported
# on branches that don't use CircleCI. This file should be deleted when all
# branches are no longer dependent on CircleCI.
version: 2
jobs:
dummy:
docker:
- image: busybox
steps:
- run:
name: "dummy"
command: echo "dummy job"
workflows:
version: 2
ci:
jobs:
- dummy
+2 -2
View File
@@ -1,6 +1,6 @@
/build/ /build/
/cmd/docker/winresources/versioninfo.json /cli/winresources/versioninfo.json
/cmd/docker/winresources/*.syso /cli/winresources/*.syso
/man/man*/ /man/man*/
/man/vendor/ /man/vendor/
/man/go.sum /man/go.sum
-11
View File
@@ -1,14 +1,3 @@
* text=auto
Dockerfile* linguist-language=Dockerfile Dockerfile* linguist-language=Dockerfile
vendor.mod linguist-language=Go-Module vendor.mod linguist-language=Go-Module
vendor.sum linguist-language=Go-Checksums vendor.sum linguist-language=Go-Checksums
*.go -text diff=golang
# scripts directory contains shell scripts
# without extensions, so we need to force
scripts/** text=auto eol=lf
# shell scripts should always have LF
*.sh text eol=lf
+4 -3
View File
@@ -1,6 +1,7 @@
# GitHub code owners # GitHub code owners
# See https://github.com/blog/2392-introducing-code-owners # See https://github.com/blog/2392-introducing-code-owners
cli/command/stack/** @silvin-lubecki @docker/runtime-owners cli/command/stack/** @silvin-lubecki
contrib/completion/bash/** @albers @docker/runtime-owners contrib/completion/bash/** @albers
docs/** @thaJeztah @docker/runtime-owners contrib/completion/zsh/** @sdurrheimer
docs/** @thaJeztah
+2 -2
View File
@@ -8,12 +8,12 @@ body:
attributes: attributes:
value: | value: |
Thank you for taking the time to report a bug! Thank you for taking the time to report a bug!
If this is a security issue report it to the [Docker Security team](mailto:security@docker.com). If this is a security issue please report it to the [Docker Security team](mailto:security@docker.com).
- type: textarea - type: textarea
id: description id: description
attributes: attributes:
label: Description label: Description
description: Give a clear and concise description of the bug description: Please give a clear and concise description of the bug
validations: validations:
required: true required: true
- type: textarea - type: textarea
+4 -10
View File
@@ -1,17 +1,11 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: "🗃️ Docker Sandboxes: report an issue" - name: "Contributing to Docker"
about: "Issues with Docker Sandboxes should be filed in the Docker Desktop feedback tracker (not docker/cli)."
url: "https://github.com/docker/desktop-feedback/issues"
- name: "🖥️ Docker Desktop: report an issue"
about: "General Docker Desktop issues (installation, upgrades, UI, networking on macOS/Windows, WSL2, etc.) should be filed in the Docker Desktop feedback tracker (not docker/cli)."
url: "https://github.com/docker/desktop-feedback/issues"
- name: "🧑‍💻 Contributing to Docker"
about: "Read guidelines and tips about contributing to Docker." about: "Read guidelines and tips about contributing to Docker."
url: "https://github.com/docker/cli/blob/master/CONTRIBUTING.md" url: "https://github.com/docker/cli/blob/master/CONTRIBUTING.md"
- name: "🔒 Security and Vulnerabilities" - name: "Security and Vulnerabilities"
about: "Report any security issues or vulnerabilities responsibly to the Docker security team. Do not use the public issue tracker." about: "Please report any security issues or vulnerabilities responsibly to the Docker security team. Please do not use the public issue tracker."
url: "https://github.com/moby/moby/security/policy" url: "https://github.com/moby/moby/security/policy"
- name: "💬 General Support" - name: "General Support"
about: "Get the help you need to build, share, and run your Docker applications" about: "Get the help you need to build, share, and run your Docker applications"
url: "https://www.docker.com/support/" url: "https://www.docker.com/support/"
+18 -21
View File
@@ -2,36 +2,33 @@
Please make sure you've read and understood our contributing guidelines; Please make sure you've read and understood our contributing guidelines;
https://github.com/docker/cli/blob/master/CONTRIBUTING.md https://github.com/docker/cli/blob/master/CONTRIBUTING.md
Make sure commits are signed off (`git commit -s`) with your real name. ** Make sure all your commits include a signature generated with `git commit -s` **
If the PR relates to an existing issue or PR, mention it at the top: For additional information on our contributing process, read our contributing
guide https://docs.docker.com/opensource/code/
- Fixes: https://github.com/docker/cli/issues/12345678 If this is a bug fix, make sure your description includes "fixes #xxxx", or
- Replaces: https://github.com/docker/cli/pull/12345678 "closes #xxxx"
- Follow up to: https://github.com/docker/cli/pull/12345678
- Related to: https://github.com/docker/cli/issues/12345678 Please provide the following information:
--> -->
## Summary **- What I did**
## Release notes (optional) **- How I did it**
**- How to verify it**
**- Description for the changelog**
<!-- <!--
One-sentence user-facing release note. Write a short (one line) summary that describes the changes in this
Leave empty if the change is not changelog-worthy. pull request for inclusion in the changelog.
Use present tense and imperative tone. It must be placed inside the below triple backticks section:
Good:
- Fix a panic when running `docker top` on a non-running Windows container.
- Don't print warnings in `docker info` for broken symlinks in CLI plugin directories.
Bad:
- Refactor test TestFooWithBar
- Fixed a panic when running docker top
--> -->
```markdown changelog ```markdown changelog
``` ```
## A picture of a cute animal (not mandatory but encouraged) **- A picture of a cute animal (not mandatory but encouraged)**
+1 -10
View File
@@ -5,14 +5,5 @@ updates:
schedule: schedule:
interval: "daily" interval: "daily"
labels: labels:
- "area/ci" - "area/testing"
- "status/2-code-review" - "status/2-code-review"
cooldown:
default-days: 7
groups:
codeql-actions:
patterns:
- "github/codeql-action/*"
docker-actions:
patterns:
- "docker/*"
-59
View File
@@ -1,59 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
// https://docs.renovatebot.com/modules/manager/github-actions/
// https://docs.renovatebot.com/presets-helpers/
"helpers:pinGitHubActionDigestsToSemver",
":gitSignOff"
],
"gomod": {
"managerFilePatterns": ["/(^|/)vendor\\.mod$/"]
},
"postUpdateOptions": ["gomodTidy"],
"minimumReleaseAge": "3 days",
"internalChecksFilter": "strict",
"packageRules": [
{
"matchManagers": ["gomod"],
"matchFileNames": ["vendor.mod"],
"semanticCommits": "disabled",
"commitMessagePrefix": "vendor:"
},
{
"matchManagers": ["gomod"],
"matchFileNames": ["cmd/docker-trust/go.mod"],
// The trust module does not vendor dependencies; vendor/ belongs to the root module.
"postUpdateOptions": ["gomodSkipVendor"],
"semanticCommits": "disabled",
"commitMessagePrefix": "cmd/docker-trust:"
},
{
"matchManagers": ["gomod"],
"matchFileNames": ["vendor.mod"],
"groupName": "golang.org/x packages",
"groupSlug": "golang-x",
"matchDepNames": ["/^golang\\.org\\/x\\//"]
},
{
"matchManagers": ["github-actions"],
"minimumReleaseAge": "7 days",
"addLabels": ["area/ci", "status/2-code-review"]
},
{
"matchManagers": ["github-actions"],
"groupName": "docker GitHub Actions",
"groupSlug": "docker-actions",
"matchDepNames": ["/^docker\\//"]
},
{
// Keep all the google.golang.org/genproto modules at the same version.
"matchManagers": ["gomod"],
"matchFileNames": ["vendor.mod"],
"groupName": "google.golang.org/genproto packages",
"groupSlug": "google-genproto",
"matchDepNames": ["/^google\\.golang\\.org\\/genproto(\\/|$)/"],
"schedule": ["every month"]
}
]
}
+62 -44
View File
@@ -22,22 +22,19 @@ on:
branches: branches:
- 'master' - 'master'
- '[0-9]+.[0-9]+' - '[0-9]+.[0-9]+'
- '[0-9]+.x'
tags: tags:
- 'v*' - 'v*'
pull_request: pull_request:
jobs: jobs:
prepare: prepare:
runs-on: ubuntu-26.04 runs-on: ubuntu-22.04
outputs: outputs:
matrix: ${{ steps.platforms.outputs.matrix }} matrix: ${{ steps.platforms.outputs.matrix }}
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@v4
with:
persist-credentials: false
- -
name: Create matrix name: Create matrix
id: platforms id: platforms
@@ -49,7 +46,7 @@ jobs:
echo ${{ steps.platforms.outputs.matrix }} echo ${{ steps.platforms.outputs.matrix }}
build: build:
runs-on: ubuntu-26.04 runs-on: ubuntu-22.04
needs: needs:
- prepare - prepare
strategy: strategy:
@@ -63,12 +60,17 @@ jobs:
- "" - ""
- glibc - glibc
steps: steps:
-
name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- -
name: Set up Docker Buildx name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 uses: docker/setup-buildx-action@v3
- -
name: Build name: Build
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0 uses: docker/bake-action@v4
with: with:
targets: ${{ matrix.target }} targets: ${{ matrix.target }}
set: | set: |
@@ -80,61 +82,74 @@ jobs:
working-directory: ./build working-directory: ./build
run: | run: |
mkdir /tmp/out mkdir /tmp/out
platformPair=${PLATFORM//\//-} platform=${{ matrix.platform }}
platformPair=${platform//\//-}
tar -cvzf "/tmp/out/docker-${platformPair}.tar.gz" . tar -cvzf "/tmp/out/docker-${platformPair}.tar.gz" .
if [ -z "${{ matrix.use_glibc }}" ]; then if [ -z "${{ matrix.use_glibc }}" ]; then
echo "ARTIFACT_NAME=${{ matrix.target }}-${platformPair}" >> $GITHUB_ENV echo "ARTIFACT_NAME=${{ matrix.target }}-${platformPair}" >> $GITHUB_ENV
else else
echo "ARTIFACT_NAME=${{ matrix.target }}-${platformPair}-glibc" >> $GITHUB_ENV echo "ARTIFACT_NAME=${{ matrix.target }}-${platformPair}-glibc" >> $GITHUB_ENV
fi fi
env:
PLATFORM: ${{ matrix.platform }}
- -
name: Upload artifacts name: Upload artifacts
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 uses: actions/upload-artifact@v4
with: with:
name: ${{ env.ARTIFACT_NAME }} name: ${{ env.ARTIFACT_NAME }}
path: /tmp/out/* path: /tmp/out/*
if-no-files-found: error if-no-files-found: error
bin-image: bin-image:
runs-on: ubuntu-22.04
if: ${{ github.event_name != 'pull_request' && github.repository == 'docker/cli' }} if: ${{ github.event_name != 'pull_request' && github.repository == 'docker/cli' }}
uses: docker/github-builder/.github/workflows/bake.yml@58cb9f5b71b1836d6f690c1e95effdeb9b98cb8a # v1.17.0 steps:
permissions: -
contents: read # same as global permission name: Checkout
id-token: write # for signing attestation(s) and authenticating to Docker Hub with GitHub OIDC Token uses: actions/checkout@v4
with: -
setup-qemu: true name: Set up QEMU
target: bin-image-cross uses: docker/setup-qemu-action@v3
cache: true -
cache-scope: bin-image name: Set up Docker Buildx
output: image uses: docker/setup-buildx-action@v3
push: true -
vars: | name: Docker meta
VERSION=${{ github.ref }} id: meta
meta-images: | uses: docker/metadata-action@v5
dockereng/cli-bin with:
meta-tags: | images: dockereng/cli-bin
type=semver,pattern={{version}} tags: |
type=ref,event=branch type=semver,pattern={{version}}
type=ref,event=pr type=ref,event=branch
type=semver,pattern={{major}} type=ref,event=pr
type=semver,pattern={{major}}.{{minor}} type=sha
registry-identities: | -
- type: dockerhub name: Login to DockerHub
username: dockereng if: github.event_name != 'pull_request'
connection_id: ${{ vars.DOCKERHUB_OIDC_CONNECTIONID }} uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_CLIBIN_USERNAME }}
password: ${{ secrets.DOCKERHUB_CLIBIN_TOKEN }}
-
name: Build and push image
uses: docker/bake-action@v4
with:
files: |
./docker-bake.hcl
${{ steps.meta.outputs.bake-file }}
targets: bin-image-cross
push: ${{ github.event_name != 'pull_request' }}
set: |
*.cache-from=type=gha,scope=bin-image
*.cache-to=type=gha,scope=bin-image,mode=max
prepare-plugins: prepare-plugins:
runs-on: ubuntu-26.04 runs-on: ubuntu-22.04
outputs: outputs:
matrix: ${{ steps.platforms.outputs.matrix }} matrix: ${{ steps.platforms.outputs.matrix }}
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@v4
with:
persist-credentials: false
- -
name: Create matrix name: Create matrix
id: platforms id: platforms
@@ -146,7 +161,7 @@ jobs:
echo ${{ steps.platforms.outputs.matrix }} echo ${{ steps.platforms.outputs.matrix }}
plugins: plugins:
runs-on: ubuntu-26.04 runs-on: ubuntu-22.04
needs: needs:
- prepare-plugins - prepare-plugins
strategy: strategy:
@@ -154,12 +169,15 @@ jobs:
matrix: matrix:
platform: ${{ fromJson(needs.prepare-plugins.outputs.matrix) }} platform: ${{ fromJson(needs.prepare-plugins.outputs.matrix) }}
steps: steps:
-
name: Checkout
uses: actions/checkout@v4
- -
name: Set up Docker Buildx name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 uses: docker/setup-buildx-action@v3
- -
name: Build name: Build
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0 uses: docker/bake-action@v4
with: with:
targets: plugins-cross targets: plugins-cross
set: | set: |
+27 -25
View File
@@ -11,15 +11,14 @@ permissions:
on: on:
push: push:
branches: branches:
- 'master' - 'master'
- '[0-9]+.[0-9]+' - '[0-9]+.[0-9]+'
- '[0-9]+.x' tags:
tags: - 'v*'
- 'v*'
pull_request: pull_request:
# The branches below must be a subset of the branches above # The branches below must be a subset of the branches above
branches: ["master"] branches: [ "master" ]
schedule: schedule:
# ┌───────────── minute (0 - 59) # ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23) # │ ┌───────────── hour (0 - 23)
@@ -34,22 +33,36 @@ on:
jobs: jobs:
codeql: codeql:
runs-on: ubuntu-26.04 runs-on: 'ubuntu-latest'
timeout-minutes: 10 timeout-minutes: 360
env: env:
DISABLE_WARN_OUTSIDE_CONTAINER: '1' DISABLE_WARN_OUTSIDE_CONTAINER: '1'
permissions: permissions:
actions: read actions: read
contents: read contents: read
security-events: write security-events: write
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@v4
with: with:
fetch-depth: 2 fetch-depth: 2
persist-credentials: false -
name: Checkout HEAD on PR
if: ${{ github.event_name == 'pull_request' }}
run: |
git checkout HEAD^2
-
name: Update Go
uses: actions/setup-go@v5
with:
go-version: "1.22.10"
-
name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: go
# CodeQL 2.16.4's auto-build added support for multi-module repositories, # CodeQL 2.16.4's auto-build added support for multi-module repositories,
# and is trying to be smart by searching for modules in every directory, # and is trying to be smart by searching for modules in every directory,
# including vendor directories. If no module is found, it's creating one # including vendor directories. If no module is found, it's creating one
@@ -60,22 +73,11 @@ jobs:
run: | run: |
ln -s vendor.mod go.mod ln -s vendor.mod go.mod
ln -s vendor.sum go.sum ln -s vendor.sum go.sum
-
name: Update Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: "1.26.10"
cache: false
-
name: Initialize CodeQL
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
with:
languages: go
- -
name: Autobuild name: Autobuild
uses: github/codeql-action/autobuild@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 uses: github/codeql-action/autobuild@v3
- -
name: Perform CodeQL Analysis name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 uses: github/codeql-action/analyze@v3
with: with:
category: "/language:go" category: "/language:go"
+15 -25
View File
@@ -19,54 +19,45 @@ on:
branches: branches:
- 'master' - 'master'
- '[0-9]+.[0-9]+' - '[0-9]+.[0-9]+'
- '[0-9]+.x'
tags: tags:
- 'v*' - 'v*'
pull_request: pull_request:
jobs: jobs:
tests: e2e:
runs-on: ubuntu-26.04 runs-on: ubuntu-22.04
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
target: target:
- local - non-experimental
- experimental
- connhelper-ssh - connhelper-ssh
base: base:
- alpine - alpine
- debian - debian
engine-version: engine-version:
- master # moby/moby-bin:master - 25.0 # latest
- rc # latest rc - 24.0 # latest - 1
- 29 # latest - 23.0 # mirantis lts
- 28 # latest - 1 # TODO(krissetto) 19.03 needs a look, doesn't work ubuntu 22.04 (cgroup errors).
- 25 # mirantis lts # we could have a separate job that tests it against ubuntu 20.04
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@v4
with:
persist-credentials: false
- -
name: Update daemon.json name: Update daemon.json
run: | run: |
if [ ! -f /etc/docker/daemon.json ]; then sudo jq '.experimental = true' < /etc/docker/daemon.json > /tmp/docker.json
# ubuntu runners no longer have a default daemon.json present sudo mv /tmp/docker.json /etc/docker/daemon.json
sudo mkdir -p /etc/docker/
echo '{"experimental": true}' | sudo tee /etc/docker/daemon.json
else
# but if there is one; let's patch it to keep other options that may be set.
sudo jq '.experimental = true' < /etc/docker/daemon.json > /tmp/docker.json
sudo mv /tmp/docker.json /etc/docker/daemon.json
fi
sudo cat /etc/docker/daemon.json sudo cat /etc/docker/daemon.json
sudo service docker restart sudo service docker restart
docker version docker version
docker info docker info
- -
name: Set up Docker Buildx name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 uses: docker/setup-buildx-action@v3
- -
name: Run ${{ matrix.target }} name: Run ${{ matrix.target }}
run: | run: |
@@ -77,7 +68,6 @@ jobs:
TESTFLAGS: -coverprofile=/tmp/coverage/coverage.txt TESTFLAGS: -coverprofile=/tmp/coverage/coverage.txt
- -
name: Send to Codecov name: Send to Codecov
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 uses: codecov/codecov-action@v4
with: with:
files: ./build/coverage/coverage.txt file: ./build/coverage/coverage.txt
token: ${{ secrets.CODECOV_TOKEN }}
-42
View File
@@ -1,42 +0,0 @@
name: PR Review - Trigger
on:
pull_request:
types: [ready_for_review, opened, review_requested]
pull_request_review_comment:
types: [created]
permissions: {}
# Deduplicate simultaneous pull_request events for the same fork PR.
# When reviewers are requested at the same time, GitHub fires multiple
# review_requested events. Without this group each event triggers a
# separate review via workflow_run, producing duplicate reviews.
concurrency:
group: pr-review-trigger-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
save-context:
if: github.event.pull_request.head.repo.fork
runs-on: ubuntu-latest
steps:
- name: Save event context
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
COMMENT_JSON: ${{ toJSON(github.event.comment) }}
run: |
mkdir -p context
printf '%s' "${{ github.event_name }}" > context/event_name.txt
printf '%s' "$PR_NUMBER" > context/pr_number.txt
printf '%s' "$PR_HEAD_SHA" > context/pr_head_sha.txt
if [ "${{ github.event_name }}" = "pull_request_review_comment" ]; then
printf '%s' "$COMMENT_JSON" > context/comment.json
fi
- name: Upload context
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: pr-review-context
path: context/
retention-days: 1
-23
View File
@@ -1,23 +0,0 @@
name: PR Review
on:
issue_comment:
types: [created]
workflow_run:
workflows: ["PR Review - Trigger"]
types: [completed]
permissions:
contents: read
jobs:
review:
uses: docker/docker-agent-action/.github/workflows/review-pr.yml@3654ec391b070e247de2e2e3c1ff2519b6378b3a # v2.0.9
permissions:
contents: read # Read repository files and PR diffs
pull-requests: write # Post review comments
issues: write # Create security incident issues if secrets detected
checks: write # (Optional) Show review progress as a check run
id-token: write # Required for OIDC authentication to AWS Secrets Manager
actions: write # Required since v2.0.3 — review-lock cache cleanup and feedback artifacts
with:
trigger-run-id: ${{ github.event_name == 'workflow_run' && format('{0}', github.event.workflow_run.id) || '' }}
-157
View File
@@ -1,157 +0,0 @@
name: Sync Docker release branch
concurrency:
group: ${{ github.workflow }}-${{ inputs.release_branch }}
cancel-in-progress: false
permissions:
contents: read
on:
workflow_dispatch:
inputs:
release_branch:
description: Release branch to sync, for example 29.x
required: true
type: string
tag:
description: Tag to sync from, for example v29.6.0
required: true
type: string
dry_run:
description: Merge but don't push
required: true
default: false
type: boolean
jobs:
sync-release-branch:
runs-on: ubuntu-26.04
permissions:
contents: write
actions: write
outputs:
base_sha: ${{ steps.sync.outputs.base_sha }}
has_changes: ${{ steps.sync.outputs.has_changes }}
temporary_branch: ${{ steps.sync.outputs.temporary_branch }}
temporary_sha: ${{ steps.sync.outputs.temporary_sha }}
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Validate
env:
BRANCH: ${{ github.ref_name }}
RELEASE_BRANCH: ${{ inputs.release_branch }}
run: |
if [ "$BRANCH" != "master" ]; then
echo "::error::This workflow is expected to be run on master, not $BRANCH"
exit 1
fi
if [ "$RELEASE_BRANCH" = "master" ]; then
echo "::error::The release branch must not be master"
exit 1
fi
if ! [[ "$RELEASE_BRANCH" =~ ^[0-9]+\.[x0-9]+$ ]]; then
echo "::error::Invalid release branch name: '$RELEASE_BRANCH'. Expected format: 29.x"
exit 1
fi
- name: Configure git author
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Sync release branch to tag range
id: sync
env:
DRY_RUN: ${{ inputs.dry_run }}
RELEASE_BRANCH: ${{ inputs.release_branch }}
RUN_ATTEMPT: ${{ github.run_attempt }}
RUN_ID: ${{ github.run_id }}
TAG: ${{ inputs.tag }}
run: |
set -o pipefail
base_sha=$(git rev-parse "origin/$RELEASE_BRANCH")
temporary_branch="process/sync-release-branch/$RUN_ID-$RUN_ATTEMPT"
echo "base_sha=$base_sha" >> "$GITHUB_OUTPUT"
echo "temporary_branch=$temporary_branch" >> "$GITHUB_OUTPUT"
# Keep the master checkout unchanged so scripts run from the dispatched revision.
release_worktree="$RUNNER_TEMP/release-branch"
git worktree add --detach "$release_worktree" "$base_sha"
cd "$release_worktree"
tags_file=$(mktemp)
"$GITHUB_WORKSPACE/scripts/unmerged-tags" \
"origin/$RELEASE_BRANCH" \
"$TAG" \
> "$tags_file"
echo >> "$GITHUB_STEP_SUMMARY"
echo "## Tags to sync" >> "$GITHUB_STEP_SUMMARY"
echo >> "$GITHUB_STEP_SUMMARY"
sed 's/^/- /' "$tags_file" >> "$GITHUB_STEP_SUMMARY"
xargs -r "$GITHUB_WORKSPACE/scripts/sync-branch" < "$tags_file" | tee -a "$GITHUB_STEP_SUMMARY"
if [[ "$DRY_RUN" == "true" ]]; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if [[ $(git rev-parse HEAD) == $(git rev-parse "origin/$RELEASE_BRANCH") ]]; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
echo "No changes to push"
exit 0
fi
echo "has_changes=true" >> "$GITHUB_OUTPUT"
git push origin "HEAD:refs/heads/$temporary_branch"
echo "temporary_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
push-release-branch:
needs: sync-release-branch
if: ${{ !inputs.dry_run && needs.sync-release-branch.outputs.has_changes == 'true' }}
runs-on: ubuntu-26.04
environment: docker-releases
permissions:
contents: write
actions: write
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Push release branch
env:
BASE_SHA: ${{ needs.sync-release-branch.outputs.base_sha }}
RELEASE_BRANCH: ${{ inputs.release_branch }}
TEMPORARY_BRANCH: ${{ needs.sync-release-branch.outputs.temporary_branch }}
TEMPORARY_SHA: ${{ needs.sync-release-branch.outputs.temporary_sha }}
run: |
git fetch origin "$RELEASE_BRANCH"
current_sha=$(git rev-parse "origin/$RELEASE_BRANCH")
if [[ "$current_sha" != "$BASE_SHA" ]]; then
echo "$RELEASE_BRANCH changed from $BASE_SHA to $current_sha"
exit 1
fi
git fetch origin "$TEMPORARY_BRANCH"
current_temporary_sha=$(git rev-parse FETCH_HEAD)
if [[ "$current_temporary_sha" != "$TEMPORARY_SHA" ]]; then
echo "$TEMPORARY_BRANCH changed from $TEMPORARY_SHA to $current_temporary_sha"
exit 1
fi
git push origin "FETCH_HEAD:$RELEASE_BRANCH"
- name: Delete temporary branch
env:
TEMPORARY_BRANCH: ${{ needs.sync-release-branch.outputs.temporary_branch }}
run: git push origin --delete "$TEMPORARY_BRANCH"
+22 -22
View File
@@ -19,29 +19,30 @@ on:
branches: branches:
- 'master' - 'master'
- '[0-9]+.[0-9]+' - '[0-9]+.[0-9]+'
- '[0-9]+.x'
tags: tags:
- 'v*' - 'v*'
pull_request: pull_request:
jobs: jobs:
ctn: ctn:
runs-on: ubuntu-26.04 runs-on: ubuntu-22.04
steps: steps:
-
name: Checkout
uses: actions/checkout@v4
- -
name: Set up Docker Buildx name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 uses: docker/setup-buildx-action@v3
- -
name: Test name: Test
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0 uses: docker/bake-action@v4
with: with:
targets: test-coverage targets: test-coverage
- -
name: Send to Codecov name: Send to Codecov
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 uses: codecov/codecov-action@v4
with: with:
files: ./build/coverage/coverage.txt file: ./build/coverage/coverage.txt
token: ${{ secrets.CODECOV_TOKEN }}
host: host:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
@@ -53,37 +54,36 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: os:
- macos-14 # macOS 14 on arm64 (Apple Silicon M1) - macos-13 # macOS 13 on Intel
- macos-15-intel # macOS 15 on Intel - macos-14 # macOS 14 on arm64 (Apple Silicon M1)
- macos-15 # macOS 15 on arm64 (Apple Silicon M1)
# - windows-2022 # FIXME: some tests are failing on the Windows runner, as well as on Appveyor since June 24, 2018: https://ci.appveyor.com/project/docker/cli/history # - windows-2022 # FIXME: some tests are failing on the Windows runner, as well as on Appveyor since June 24, 2018: https://ci.appveyor.com/project/docker/cli/history
steps: steps:
-
name: Prepare git
if: matrix.os == 'windows-latest'
run: |
git config --system core.autocrlf false
git config --system core.eol lf
- -
name: Checkout name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@v4
with: with:
path: ${{ env.GOPATH }}/src/github.com/docker/cli path: ${{ env.GOPATH }}/src/github.com/docker/cli
persist-credentials: false
- -
name: Set up Go name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 uses: actions/setup-go@v5
with: with:
go-version: "1.26.10" go-version: "1.22.10"
cache: false
- -
name: Test name: Test
run: | run: |
# run in go modules mode to prevent traversing to nested modules go test -coverprofile=/tmp/coverage.txt $(go list ./... | grep -vE '/vendor/|/e2e/')
ln -s vendor.mod go.mod
ln -s vendor.sum go.sum
go test -coverprofile=/tmp/coverage.txt $(go list ./... | grep -vE '^github.com/docker/cli/e2e/')
go tool cover -func=/tmp/coverage.txt go tool cover -func=/tmp/coverage.txt
working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli
shell: bash shell: bash
- -
name: Send to Codecov name: Send to Codecov
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 uses: codecov/codecov-action@v4
with: with:
files: /tmp/coverage.txt file: /tmp/coverage.txt
working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli working-directory: ${{ env.GOPATH }}/src/github.com/docker/cli
token: ${{ secrets.CODECOV_TOKEN }}
-58
View File
@@ -1,58 +0,0 @@
name: validate-milestone
permissions:
contents: read
pull-requests: read
on:
pull_request:
types: [opened, synchronize, milestoned, demilestoned, edited]
jobs:
validate-milestone:
runs-on: ubuntu-26.04
timeout-minutes: 5
steps:
- name: Validate milestone matches VERSION
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
MILESTONE: ${{ github.event.pull_request.milestone.title }}
with:
script: |
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
});
core.info(`Modified files: ${files.map(f => f.filename).join(', ')}`);
const touchesVersion = files.some(f => f.filename === 'VERSION');
core.info(`Touches VERSION: ${touchesVersion}`);
// Use the PR's version when it bumps the file, base branch otherwise.
// It's fine to trust the author in this case, it's not meant to be
// a security gate, just a helpful check for maintainers.
const ref = touchesVersion
? context.payload.pull_request.head.sha
: context.payload.pull_request.base.ref;
core.info(`Base ref: ${ref}`);
const resp = await github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: 'VERSION',
ref,
});
const expected = Buffer.from(resp.data.content, resp.data.encoding).toString('utf8').trim();
const milestone = process.env.MILESTONE;
if (!milestone) {
core.setFailed(`PR must have a milestone set (expected: ${expected})`);
return;
}
if (milestone !== expected) {
core.setFailed(`Milestone '${milestone}' does not match VERSION '${expected}'`);
return;
}
core.info(`Milestone: ${milestone} ✓`);
+20 -42
View File
@@ -11,31 +11,24 @@ permissions:
on: on:
pull_request: pull_request:
types: [opened, edited, labeled, unlabeled, synchronize] types: [opened, edited, labeled, unlabeled]
jobs: jobs:
check-labels: check-area-label:
runs-on: ubuntu-26.04 runs-on: ubuntu-20.04
timeout-minutes: 120 # guardrails timeout for the whole job
steps: steps:
- name: Missing `area/` label - name: Missing `area/` label
if: always() && contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'area/') if: contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'area/')
run: | run: |
echo "::error::Every PR with an 'impact/*' label should also have an 'area/*' label" echo "::error::Every PR with an 'impact/*' label should also have an 'area/*' label"
exit 1 exit 1
- name: Missing `kind/` label
if: always() && contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') && !contains(join(github.event.pull_request.labels.*.name, ','), 'kind/')
run: |
echo "::error::Every PR with an 'impact/*' label should also have a 'kind/*' label"
exit 1
- name: OK - name: OK
run: exit 0 run: exit 0
check-changelog: check-changelog:
runs-on: ubuntu-26.04 if: contains(join(github.event.pull_request.labels.*.name, ','), 'impact/')
timeout-minutes: 120 # guardrails timeout for the whole job runs-on: ubuntu-20.04
env: env:
HAS_IMPACT_LABEL: ${{ contains(join(github.event.pull_request.labels.*.name, ','), 'impact/') }}
PR_BODY: | PR_BODY: |
${{ github.event.pull_request.body }} ${{ github.event.pull_request.body }}
steps: steps:
@@ -47,47 +40,32 @@ jobs:
# Strip empty lines # Strip empty lines
desc=$(echo "$block" | awk NF) desc=$(echo "$block" | awk NF)
if [ "$HAS_IMPACT_LABEL" = "true" ]; then if [ -z "$desc" ]; then
if [ -z "$desc" ]; then echo "::error::Changelog section is empty. Please provide a description for the changelog."
echo "::error::Changelog section is empty. Please provide a description for the changelog." exit 1
exit 1 fi
fi
len=$(echo -n "$desc" | wc -c) len=$(echo -n "$desc" | wc -c)
if [[ $len -le 6 ]]; then if [[ $len -le 6 ]]; then
echo "::error::Description looks too short: $desc" echo "::error::Description looks too short: $desc"
exit 1 exit 1
fi
else
if [ -n "$desc" ]; then
echo "::error::PR has a changelog description, but no changelog label"
echo "::error::Please add the relevant 'impact/' label to the PR or remove the changelog description"
exit 1
fi
fi fi
echo "This PR will be included in the release notes with the following note:" echo "This PR will be included in the release notes with the following note:"
echo "$desc" echo "$desc"
check-pr-branch: check-pr-branch:
runs-on: ubuntu-26.04 runs-on: ubuntu-20.04
timeout-minutes: 120 # guardrails timeout for the whole job
env: env:
PR_TITLE: ${{ github.event.pull_request.title }} PR_TITLE: ${{ github.event.pull_request.title }}
steps: steps:
# Backports or PR that target a release branch directly should mention the target branch in the title, for example: # Backports or PR that target a release branch directly should mention the target branch in the title, for example:
# [X.Y backport] Some change that needs backporting to X.Y # [X.Y backport] Some change that needs backporting to X.Y
# [X.Y] Change directly targeting the X.Y branch # [X.Y] Change directly targeting the X.Y branch
- name: Check release branch - name: Get branch from PR title
id: title_branch id: title_branch
run: | run: echo "$PR_TITLE" | sed -n 's/^\[\([0-9]*\.[0-9]*\)[^]]*\].*/branch=\1/p' >> $GITHUB_OUTPUT
# get the intended major version prefix ("[27.1 backport]" -> "27.") from the PR title.
[[ "$PR_TITLE" =~ ^\[([0-9]*\.)[^]]*\] ]] && branch="${BASH_REMATCH[1]}"
# get major version prefix from the release branch ("27.x -> "27.") - name: Check release branch
[[ "$GITHUB_BASE_REF" =~ ^([0-9]*\.) ]] && target_branch="${BASH_REMATCH[1]}" || target_branch="$GITHUB_BASE_REF" if: github.event.pull_request.base.ref != steps.title_branch.outputs.branch && !(github.event.pull_request.base.ref == 'master' && steps.title_branch.outputs.branch == '')
run: echo "::error::PR title suggests targetting the ${{ steps.title_branch.outputs.branch }} branch, but is opened against ${{ github.event.pull_request.base.ref }}" && exit 1
if [[ "$target_branch" != "$branch" ]] && ! [[ "$GITHUB_BASE_REF" == "master" && "$branch" == "" ]]; then
echo "::error::PR is opened against the $GITHUB_BASE_REF branch, but its title suggests otherwise."
exit 1
fi
+9 -36
View File
@@ -19,14 +19,13 @@ on:
branches: branches:
- 'master' - 'master'
- '[0-9]+.[0-9]+' - '[0-9]+.[0-9]+'
- '[0-9]+.x'
tags: tags:
- 'v*' - 'v*'
pull_request: pull_request:
jobs: jobs:
validate: validate:
runs-on: ubuntu-26.04 runs-on: ubuntu-22.04
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -36,21 +35,22 @@ jobs:
- validate-vendor - validate-vendor
- update-authors # ensure authors update target runs fine - update-authors # ensure authors update target runs fine
steps: steps:
-
name: Checkout
uses: actions/checkout@v4
- -
name: Run name: Run
uses: docker/bake-action@018cb6412ab401ebaa809aa5f85966b74628600f # v7.4.0 uses: docker/bake-action@v4
with: with:
targets: ${{ matrix.target }} targets: ${{ matrix.target }}
# check that the generated Markdown and the checked-in files match # check that the generated Markdown and the checked-in files match
validate-md: validate-md:
runs-on: ubuntu-26.04 runs-on: ubuntu-22.04
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@v4
with:
persist-credentials: false
- -
name: Generate name: Generate
shell: 'script --return --quiet --command "bash {0}"' shell: 'script --return --quiet --command "bash {0}"'
@@ -66,7 +66,7 @@ jobs:
fi fi
validate-make: validate-make:
runs-on: ubuntu-26.04 runs-on: ubuntu-22.04
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -76,36 +76,9 @@ jobs:
steps: steps:
- -
name: Checkout name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 uses: actions/checkout@v4
with:
persist-credentials: false
- -
name: Run name: Run
shell: 'script --return --quiet --command "bash {0}"' shell: 'script --return --quiet --command "bash {0}"'
run: | run: |
make -f docker.Makefile ${{ matrix.target }} make -f docker.Makefile ${{ matrix.target }}
validate-gocompat:
runs-on: ubuntu-26.04
env:
GOPATH: ${{ github.workspace }}
GO111MODULE: off
steps:
-
name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
path: src/github.com/docker/cli
persist-credentials: false
-
name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: "1.26.10"
cache: false
-
name: Run gocompat check
shell: 'script --return --quiet --command "bash {0}"'
working-directory: ${{ github.workspace }}/src/github.com/docker/cli
run: |
make -C ./internal/gocompat verify
-31
View File
@@ -1,31 +0,0 @@
name: zizmor
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
on:
workflow_dispatch:
push:
branches:
- 'main'
- 'master'
- '[0-9]+.[0-9]+'
- '[0-9]+.x'
tags:
- 'v*'
pull_request:
jobs:
run:
uses: crazy-max/.github/.github/workflows/zizmor.yml@f91220cba053bd54299ba645a599ee8ed436f222 # v1.11.0
permissions:
contents: read
security-events: write
with:
min-severity: medium
min-confidence: medium
persona: pedantic
+3 -3
View File
@@ -1,5 +1,5 @@
# if you want to ignore files created by your editor/tools, # if you want to ignore files created by your editor/tools,
# consider a global .gitignore https://help.github.com/articles/ignoring-files # please consider a global .gitignore https://help.github.com/articles/ignoring-files
*.exe *.exe
*.exe~ *.exe~
*.orig *.orig
@@ -8,8 +8,8 @@
Thumbs.db Thumbs.db
.editorconfig .editorconfig
/build/ /build/
/cmd/docker/winresources/versioninfo.json /cli/winresources/versioninfo.json
/cmd/docker/winresources/*.syso /cli/winresources/*.syso
profile.out profile.out
# top-level go.mod is not meant to be checked in # top-level go.mod is not meant to be checked in
+153 -205
View File
@@ -1,237 +1,185 @@
version: "2"
run:
# prevent golangci-lint from deducting the go version to lint for through go.mod,
# 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.26.10"
timeout: 5m
issues:
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
max-issues-per-linter: 0
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
max-same-issues: 0
formatters:
enable:
- gofumpt # Detects whether code was gofumpt-ed.
- goimports
exclusions:
generated: strict
linters: linters:
enable: enable:
- asasalint # Detects "[]any" used as argument for variadic "func(...any)".
- bodyclose - bodyclose
- copyloopvar # Detects places where loop variables are copied.
- depguard - depguard
- dogsled # Detects assignments with too many blank identifiers. - dogsled
- dupword # Detects duplicate words. - dupword # Detects duplicate words.
- durationcheck # Detect cases where two time.Duration values are being multiplied in possibly erroneous ways. - durationcheck
- errcheck - errchkjson
- errchkjson # Detects unsupported types passed to json encoding functions and reports if checks for the returned error can be omitted. - exportloopref # Detects pointers to enclosing loop variables.
- exhaustive # Detects missing options in enum switch statements. - gocritic # Metalinter; detects bugs, performance, and styling issues.
- exptostd # Detects functions from golang.org/x/exp/ that can be replaced by std functions.
- fatcontext # Detects nested contexts in loops and function literals.
- forbidigo
- gocheckcompilerdirectives # Detects invalid go compiler directive comments (//go:).
- gocritic # Metalinter; detects bugs, performance, and styling issues.
- gocyclo - gocyclo
- gosec # Detects security problems. - gofumpt # Detects whether code was gofumpt-ed.
- goimports
- gosec # Detects security problems.
- gosimple
- govet - govet
- iface # Detects incorrect use of interfaces. Currently only used for "identical" interfaces in the same package.
- importas # Enforces consistent import aliases.
- ineffassign - ineffassign
- makezero # Finds slice declarations with non-zero initial length. - lll
- mirror # Detects wrong mirror patterns of bytes/strings usage. - megacheck
- misspell # Detects commonly misspelled English words in comments. - misspell # Detects commonly misspelled English words in comments.
- nakedret # Detects uses of naked returns. - nakedret
- nilnesserr # Detects returning nil errors. It combines the features of nilness and nilerr, - nilerr # Detects code that returns nil even if it checks that the error is not nil.
- nosprintfhostport # Detects misuse of Sprintf to construct a host with port in a URL. - nolintlint # Detects ill-formed or insufficient nolint directives.
- nolintlint # Detects ill-formed or insufficient nolint directives. - perfsprint # Detects fmt.Sprintf uses that can be replaced with a faster alternative.
- perfsprint # Detects fmt.Sprintf uses that can be replaced with a faster alternative. - prealloc # Detects slice declarations that could potentially be pre-allocated.
- prealloc # Detects slice declarations that could potentially be pre-allocated. - predeclared # Detects code that shadows one of Go's predeclared identifiers
- predeclared # Detects code that shadows one of Go's predeclared identifiers - reassign
- reassign # Detects reassigning a top-level variable in another package. - revive # Metalinter; drop-in replacement for golint.
- revive # Metalinter; drop-in replacement for golint.
- spancheck # Detects mistakes with OpenTelemetry/Census spans.
- staticcheck - staticcheck
- thelper # Detects test helpers without t.Helper(). - stylecheck # Replacement for golint
- tparallel # Detects inappropriate usage of t.Parallel(). - tenv # Detects using os.Setenv instead of t.Setenv.
- unconvert # Detects unnecessary type conversions. - thelper # Detects test helpers without t.Helper().
- tparallel # Detects inappropriate usage of t.Parallel().
- typecheck
- unconvert # Detects unnecessary type conversions.
- unparam - unparam
- unused - unused
- usestdlibvars # Detects the possibility to use variables/constants from the Go standard library. - usestdlibvars
- usetesting # Reports uses of functions with replacement inside the testing package. - vet
- wastedassign # Detects wasted assignment statements. - wastedassign
disable: disable:
- errcheck - errcheck
settings: run:
depguard: timeout: 5m
rules: skip-files:
main: - cli/compose/schema/bindata.go
deny: - .*generated.*
- pkg: "github.com/containerd/containerd/errdefs"
desc: The containerd errdefs package was migrated to a separate module. Use github.com/containerd/errdefs instead.
- pkg: "github.com/containerd/containerd/log"
desc: The containerd log package was migrated to a separate module. Use github.com/containerd/log instead.
- pkg: "github.com/containerd/containerd/pkg/userns"
desc: Use github.com/moby/sys/userns instead.
- pkg: "github.com/containerd/containerd/platforms"
desc: The containerd platforms package was migrated to a separate module. Use github.com/containerd/platforms instead.
- pkg: "github.com/docker/docker/errdefs"
desc: Use github.com/containerd/errdefs instead.
- pkg: "github.com/docker/docker/pkg/system"
desc: This package should not be used unless strictly necessary.
- pkg: "github.com/docker/distribution/uuid"
desc: Use github.com/google/uuid instead.
- pkg: "io/ioutil"
desc: The io/ioutil package has been deprecated, see https://go.dev/doc/go1.16#ioutil
- pkg: "gopkg.in/yaml.v3"
desc: Use go.yaml.in/yaml/v3 instead.
forbidigo: linters-settings:
forbid: depguard:
- pkg: ^regexp$ rules:
pattern: ^regexp\.MustCompile main:
msg: Use internal/lazyregexp.New instead. deny:
- pkg: io/ioutil
desc: The io/ioutil package has been deprecated, see https://go.dev/doc/go1.16#ioutil
gocyclo:
min-complexity: 16
govet:
check-shadowing: true
settings:
shadow:
strict: true
lll:
line-length: 200
nakedret:
command: nakedret
pattern: ^(?P<path>.*?\\.go):(?P<line>\\d+)\\s*(?P<message>.*)$
gocyclo: revive:
min-complexity: 16 rules:
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#import-shadowing
- name: import-shadowing
severity: warning
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-block
- name: empty-block
severity: warning
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-lines
- name: empty-lines
severity: warning
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#use-any
- name: use-any
severity: warning
disabled: false
gosec: issues:
excludes: # The default exclusion rules are a bit too permissive, so copying the relevant ones below
- G104 # G104: Errors unhandled; (TODO: reduce unhandled errors, or explicitly ignore) exclude-use-default: false
- G115 # G115: integer overflow conversion; (TODO: verify these: https://github.com/docker/cli/issues/5584)
- G117 # G117: Exported struct field matches secret pattern (false positives for legitimate field names)
- G118 # G118: Goroutine uses context.Background/TODO while request-scoped context is available (TODO: evaluate these)
- G122 # G122: Filesystem operation in filepath.Walk/WalkDir callback uses race-prone path (TODO: evaluate these)
- G306 # G306: Expect WriteFile permissions to be 0600 or less (too restrictive; also flags "0o644" permissions)
- G307 # G307: Deferring unsafe method "*os.File" on type "Close" (also EXC0008); (TODO: evaluate these and fix where needed: G307: Deferring unsafe method "*os.File" on type "Close")
- G702 # G702: Command injection via taint analysis (TODO: evaluate these)
- G703 # G703: Path traversal via taint analysis (TODO: evaluate these)
- G704 # G704: SSRF via taint analysis (TODO: evaluate these)
- G705 # G705: XSS via taint analysis (TODO: evaluate these)
govet: exclude:
enable: - parameter .* always receives
- shadow
settings:
shadow:
strict: true
lll: exclude-rules:
line-length: 200 # We prefer to use an "exclude-list" so that new "default" exclusions are not
importas:
# Do not allow unaliased imports of aliased packages.
no-unaliased: true
alias:
# Should no longer be aliased, because we no longer allow moby/docker errdefs.
- pkg: "github.com/docker/docker/errdefs"
alias: ""
- pkg: github.com/opencontainers/image-spec/specs-go/v1
alias: ocispec
# Enforce that gotest.tools/v3/assert/cmp is always aliased as "is"
- pkg: gotest.tools/v3/assert/cmp
alias: is
nakedret:
# Disallow naked returns if func has more lines of code than this setting.
# Default: 30
max-func-lines: 0
staticcheck:
checks:
- all
- -QF1008 # Omit embedded fields from selector expression; https://staticcheck.dev/docs/checks/#QF1008
revive:
rules:
- name: empty-block # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-block
- name: empty-lines # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-lines
- name: import-shadowing # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#import-shadowing
- name: line-length-limit # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#line-length-limit
arguments: [200]
- name: unused-receiver # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-receiver
- name: use-any # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#use-any
- name: use-errors-new # https://github.com/mgechev/revive/blob/HEAD/RULES_DESCRIPTIONS.md#use-errors-new
exclusions:
# We prefer to use an "linters.exclusions.rules" so that new "default" exclusions are not
# automatically inherited. We can decide whether or not to follow upstream # automatically inherited. We can decide whether or not to follow upstream
# defaults when updating golang-ci-lint versions. # defaults when updating golang-ci-lint versions.
# Unfortunately, this means we have to copy the whole exclusion pattern, as # Unfortunately, this means we have to copy the whole exclusion pattern, as
# (unlike the "include" option), the "exclude" option does not take exclusion # (unlike the "include" option), the "exclude" option does not take exclusion
# ID's. # ID's.
# #
# These exclusion patterns are copied from the default excludes at: # These exclusion patterns are copied from the default excluses at:
# https://github.com/golangci/golangci-lint/blob/v1.61.0/pkg/config/issues.go#L11-L104 # https://github.com/golangci/golangci-lint/blob/v1.44.0/pkg/config/issues.go#L10-L104
#
# The default list of exclusions can be found at:
# https://golangci-lint.run/usage/false-positives/#default-exclusions
generated: strict
rules: # EXC0001
# EXC0003 - text: "Error return value of .((os\\.)?std(out|err)\\..*|.*Close|.*Flush|os\\.Remove(All)?|.*print(f|ln)?|os\\.(Un)?Setenv). is not checked"
- text: "func name will be used as test\\.Test.* by other packages, and that stutters; consider calling this" linters:
linters: - errcheck
- revive # EXC0003
- text: "func name will be used as test\\.Test.* by other packages, and that stutters; consider calling this"
linters:
- revive
# EXC0006
- text: "Use of unsafe calls should be audited"
linters:
- gosec
# EXC0007
- text: "Subprocess launch(ed with variable|ing should be audited)"
linters:
- gosec
# EXC0008
# TODO: evaluate these and fix where needed: G307: Deferring unsafe method "*os.File" on type "Close" (gosec)
- text: "G307"
linters:
- gosec
# EXC0009
- text: "(Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less)"
linters:
- gosec
# EXC0010
- text: "Potential file inclusion via variable"
linters:
- gosec
# EXC0007 # G113 Potential uncontrolled memory consumption in Rat.SetString (CVE-2022-23772)
- text: "Subprocess launch(ed with variable|ing should be audited)" # only affects gp < 1.16.14. and go < 1.17.7
linters: - text: "G113"
- gosec linters:
- gosec
# TODO: G104: Errors unhandled. (gosec)
- text: "G104"
linters:
- gosec
# Looks like the match in "EXC0007" above doesn't catch this one
# TODO: consider upstreaming this to golangci-lint's default exclusion rules
- text: "G204: Subprocess launched with a potential tainted input or cmd arguments"
linters:
- gosec
# Looks like the match in "EXC0009" above doesn't catch this one
# TODO: consider upstreaming this to golangci-lint's default exclusion rules
- text: "G306: Expect WriteFile permissions to be 0600 or less"
linters:
- gosec
# EXC0009 # TODO: make sure all packages have a description. Currently, there's 67 packages without.
- text: "(Expect directory permissions to be 0750 or less|Expect file permissions to be 0600 or less)" - text: "package-comments: should have a package comment"
linters: linters:
- gosec - revive
# FIXME temporarily suppress these (see https://github.com/gotestyourself/gotest.tools/issues/272)
- text: "SA1019: (assert|cmp|is)\\.ErrorType is deprecated"
linters:
- staticcheck
# Exclude some linters from running on tests files.
- path: _test\.go
linters:
- errcheck
- gosec
- text: "ST1000: at least one file in a package should have a package comment"
linters:
- stylecheck
# EXC0010 # Allow "err" and "ok" vars to shadow existing declarations, otherwise we get too many false positives.
- text: "Potential file inclusion via variable" - text: '^shadow: declaration of "(err|ok)" shadows declaration'
linters: linters:
- gosec - govet
# TODO: make sure all packages have a description. Currently, there's 67 packages without.
- text: "package-comments: should have a package comment"
linters:
- revive
# Exclude some linters from running on tests files. # Maximum issues count per one linter. Set to 0 to disable. Default is 50.
- path: _test\.go max-issues-per-linter: 0
linters:
- errcheck
- gosec
- text: "ST1000: at least one file in a package should have a package comment" # Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
linters: max-same-issues: 0
- staticcheck
# Allow "err" and "ok" vars to shadow existing declarations, otherwise we get too many false positives.
- text: '^shadow: declaration of "(err|ok)" shadows declaration'
linters:
- govet
# Ignore for cli/command/formatter/tabwriter, which is forked from go stdlib, so we want to align with it.
- text: '^(ST1020|ST1022): comment on exported'
path: "cli/command/formatter/tabwriter"
linters:
- staticcheck
# TODO(thaJeztah): remove once https://github.com/leighmcculloch/gocheckcompilerdirectives/issues/7 is fixed.
- text: "compiler directive unrecognized: //go:fix"
linters:
- gocheckcompilerdirectives
# Log a warning if an exclusion rule is unused.
# Default: false
warn-unused: true
-34
View File
@@ -22,10 +22,7 @@ Akihiro Matsushima <amatsusbit@gmail.com> <amatsus@users.noreply.github.com>
Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp> Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp> <suda.akihiro@lab.ntt.co.jp> Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp> <suda.akihiro@lab.ntt.co.jp>
Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp> <suda.kyoto@gmail.com> Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp> <suda.kyoto@gmail.com>
Alano Terblanche <alano.terblanche@docker.com>
Alano Terblanche <alano.terblanche@docker.com> <18033717+Benehiko@users.noreply.github.com>
Albin Kerouanton <albinker@gmail.com> Albin Kerouanton <albinker@gmail.com>
Albin Kerouanton <albinker@gmail.com> <557933+akerouanton@users.noreply.github.com>
Albin Kerouanton <albinker@gmail.com> <albin@akerouanton.name> Albin Kerouanton <albinker@gmail.com> <albin@akerouanton.name>
Aleksa Sarai <asarai@suse.de> Aleksa Sarai <asarai@suse.de>
Aleksa Sarai <asarai@suse.de> <asarai@suse.com> Aleksa Sarai <asarai@suse.de> <asarai@suse.com>
@@ -39,12 +36,10 @@ Alexander Larsson <alexl@redhat.com> <alexander.larsson@gmail.com>
Alexander Morozov <lk4d4math@gmail.com> Alexander Morozov <lk4d4math@gmail.com>
Alexander Morozov <lk4d4math@gmail.com> <lk4d4@docker.com> Alexander Morozov <lk4d4math@gmail.com> <lk4d4@docker.com>
Alexandre Beslic <alexandre.beslic@gmail.com> <abronan@docker.com> Alexandre Beslic <alexandre.beslic@gmail.com> <abronan@docker.com>
Alexandre Levavasseur <alexandre+oss@13x.fr>
Alexis Couvreur <alexiscouvreur.pro@gmail.com> Alexis Couvreur <alexiscouvreur.pro@gmail.com>
Alicia Lauerman <alicia@eta.im> <allydevour@me.com> Alicia Lauerman <alicia@eta.im> <allydevour@me.com>
Allen Sun <allensun.shl@alibaba-inc.com> <allen.sun@daocloud.io> Allen Sun <allensun.shl@alibaba-inc.com> <allen.sun@daocloud.io>
Allen Sun <allensun.shl@alibaba-inc.com> <shlallen1990@gmail.com> Allen Sun <allensun.shl@alibaba-inc.com> <shlallen1990@gmail.com>
Allie Sadler <allie.sadler@docker.com>
Andrew Weiss <andrew.weiss@docker.com> <andrew.weiss@microsoft.com> Andrew Weiss <andrew.weiss@docker.com> <andrew.weiss@microsoft.com>
Andrew Weiss <andrew.weiss@docker.com> <andrew.weiss@outlook.com> Andrew Weiss <andrew.weiss@docker.com> <andrew.weiss@outlook.com>
André Martins <aanm90@gmail.com> <martins@noironetworks.com> André Martins <aanm90@gmail.com> <martins@noironetworks.com>
@@ -65,14 +60,8 @@ Arko Dasgupta <arko@tetrate.io> <arko.dasgupta@docker.com>
Arko Dasgupta <arko@tetrate.io> <arkodg@users.noreply.github.com> Arko Dasgupta <arko@tetrate.io> <arkodg@users.noreply.github.com>
Arnaud Porterie <icecrime@gmail.com> Arnaud Porterie <icecrime@gmail.com>
Arnaud Porterie <icecrime@gmail.com> <arnaud.porterie@docker.com> Arnaud Porterie <icecrime@gmail.com> <arnaud.porterie@docker.com>
Arthur Flageul <arthur.flageul@gmail.com>
Arthur Flageul <arthur.flageul@gmail.com> <arthur.flageul@docker.com>
Arthur Gautier <baloo@gandi.net> <superbaloo+registrations.github@superbaloo.net> Arthur Gautier <baloo@gandi.net> <superbaloo+registrations.github@superbaloo.net>
Arthur Peka <arthur.peka@outlook.com> <arthrp@users.noreply.github.com> Arthur Peka <arthur.peka@outlook.com> <arthrp@users.noreply.github.com>
Austin Vazquez <austin.vazquez@docker.com>
Austin Vazquez <austin.vazquez@docker.com> <55906459+austinvazquez@users.noreply.github.com>
Austin Vazquez <austin.vazquez@docker.com> <austin.vazquez.dev@gmail.com>
Austin Vazquez <austin.vazquez@docker.com> <macedonv@amazon.com>
Avi Miller <avi.miller@oracle.com> <avi.miller@gmail.com> Avi Miller <avi.miller@oracle.com> <avi.miller@gmail.com>
Ben Bonnefoy <frenchben@docker.com> Ben Bonnefoy <frenchben@docker.com>
Ben Golub <ben.golub@dotcloud.com> Ben Golub <ben.golub@dotcloud.com>
@@ -99,7 +88,6 @@ Brian Goff <cpuguy83@gmail.com>
Brian Goff <cpuguy83@gmail.com> <bgoff@cpuguy83-mbp.home> Brian Goff <cpuguy83@gmail.com> <bgoff@cpuguy83-mbp.home>
Brian Goff <cpuguy83@gmail.com> <bgoff@cpuguy83-mbp.local> Brian Goff <cpuguy83@gmail.com> <bgoff@cpuguy83-mbp.local>
Brian Tracy <brian.tracy33@gmail.com> Brian Tracy <brian.tracy33@gmail.com>
Calvin Liu <flycalvin@qq.com>
Carlos de Paula <me@carlosedp.com> Carlos de Paula <me@carlosedp.com>
Chad Faragher <wyckster@hotmail.com> Chad Faragher <wyckster@hotmail.com>
Chander Govindarajan <chandergovind@gmail.com> Chander Govindarajan <chandergovind@gmail.com>
@@ -154,8 +142,6 @@ Dave Henderson <dhenderson@gmail.com> <Dave.Henderson@ca.ibm.com>
Dave Tucker <dt@docker.com> <dave@dtucker.co.uk> Dave Tucker <dt@docker.com> <dave@dtucker.co.uk>
David Alvarez <david.alvarez@flyeralarm.com> David Alvarez <david.alvarez@flyeralarm.com>
David Alvarez <david.alvarez@flyeralarm.com> <busilezas@gmail.com> David Alvarez <david.alvarez@flyeralarm.com> <busilezas@gmail.com>
David Dooling <david.dooling@docker.com>
David Dooling <david.dooling@docker.com> <dooling@gmail.com>
David Karlsson <david.karlsson@docker.com> David Karlsson <david.karlsson@docker.com>
David Karlsson <david.karlsson@docker.com> <35727626+dvdksn@users.noreply.github.com> David Karlsson <david.karlsson@docker.com> <35727626+dvdksn@users.noreply.github.com>
David M. Karr <davidmichaelkarr@gmail.com> David M. Karr <davidmichaelkarr@gmail.com>
@@ -205,8 +191,6 @@ Gaetan de Villele <gdevillele@gmail.com>
Gang Qiao <qiaohai8866@gmail.com> <1373319223@qq.com> Gang Qiao <qiaohai8866@gmail.com> <1373319223@qq.com>
George Kontridze <george@bugsnag.com> George Kontridze <george@bugsnag.com>
Gerwim Feiken <g.feiken@tfe.nl> <gerwim@gmail.com> Gerwim Feiken <g.feiken@tfe.nl> <gerwim@gmail.com>
Giau. Tran Minh <hello@giautm.dev>
Giau. Tran Minh <hello@giautm.dev> <12751435+giautm@users.noreply.github.com>
Giampaolo Mancini <giampaolo@trampolineup.com> Giampaolo Mancini <giampaolo@trampolineup.com>
Gopikannan Venugopalsamy <gopikannan.venugopalsamy@gmail.com> Gopikannan Venugopalsamy <gopikannan.venugopalsamy@gmail.com>
Gou Rao <gou@portworx.com> <gourao@users.noreply.github.com> Gou Rao <gou@portworx.com> <gourao@users.noreply.github.com>
@@ -226,7 +210,6 @@ Günther Jungbluth <gunther@gameslabs.net>
Hakan Özler <hakan.ozler@kodcu.com> Hakan Özler <hakan.ozler@kodcu.com>
Hao Shu Wei <haosw@cn.ibm.com> Hao Shu Wei <haosw@cn.ibm.com>
Hao Shu Wei <haosw@cn.ibm.com> <haoshuwei1989@163.com> Hao Shu Wei <haosw@cn.ibm.com> <haoshuwei1989@163.com>
Harald Albers <github@albersweb.de>
Harald Albers <github@albersweb.de> <albers@users.noreply.github.com> Harald Albers <github@albersweb.de> <albers@users.noreply.github.com>
Harold Cooper <hrldcpr@gmail.com> Harold Cooper <hrldcpr@gmail.com>
Harry Zhang <harryz@hyper.sh> <harryzhang@zju.edu.cn> Harry Zhang <harryz@hyper.sh> <harryzhang@zju.edu.cn>
@@ -271,7 +254,6 @@ Jessica Frazelle <jess@oxide.computer> <jessfraz@google.com>
Jessica Frazelle <jess@oxide.computer> <jfrazelle@users.noreply.github.com> Jessica Frazelle <jess@oxide.computer> <jfrazelle@users.noreply.github.com>
Jessica Frazelle <jess@oxide.computer> <me@jessfraz.com> Jessica Frazelle <jess@oxide.computer> <me@jessfraz.com>
Jessica Frazelle <jess@oxide.computer> <princess@docker.com> Jessica Frazelle <jess@oxide.computer> <princess@docker.com>
Jim Chen <njucjc@gmail.com>
Jim Galasyn <jim.galasyn@docker.com> Jim Galasyn <jim.galasyn@docker.com>
Jiuyue Ma <majiuyue@huawei.com> Jiuyue Ma <majiuyue@huawei.com>
Joey Geiger <jgeiger@gmail.com> Joey Geiger <jgeiger@gmail.com>
@@ -287,8 +269,6 @@ John Howard <github@lowenna.com> <jhowardmsft@users.noreply.github.com>
John Howard <github@lowenna.com> <John.Howard@microsoft.com> John Howard <github@lowenna.com> <John.Howard@microsoft.com>
John Howard <github@lowenna.com> <john.howard@microsoft.com> John Howard <github@lowenna.com> <john.howard@microsoft.com>
John Stephens <johnstep@docker.com> <johnstep@users.noreply.github.com> John Stephens <johnstep@docker.com> <johnstep@users.noreply.github.com>
Jonathan A. Sternberg <jonathansternberg@gmail.com>
Jonathan A. Sternberg <jonathansternberg@gmail.com> <jonathan.sternberg@docker.com>
Jordan Arentsen <blissdev@gmail.com> Jordan Arentsen <blissdev@gmail.com>
Jordan Jennings <jjn2009@gmail.com> <jjn2009@users.noreply.github.com> Jordan Jennings <jjn2009@gmail.com> <jjn2009@users.noreply.github.com>
Jorit Kleine-Möllhoff <joppich@bricknet.de> <joppich@users.noreply.github.com> Jorit Kleine-Möllhoff <joppich@bricknet.de> <joppich@users.noreply.github.com>
@@ -343,8 +323,6 @@ Lajos Papp <lajos.papp@sequenceiq.com> <lalyos@yahoo.com>
Lei Jitang <leijitang@huawei.com> Lei Jitang <leijitang@huawei.com>
Lei Jitang <leijitang@huawei.com> <leijitang@gmail.com> Lei Jitang <leijitang@huawei.com> <leijitang@gmail.com>
Li Fu Bang <lifubang@acmcoder.com> Li Fu Bang <lifubang@acmcoder.com>
Li Yi <denverdino@gmail.com>
Li Yi <denverdino@gmail.com> <weiyuan.yl@alibaba-inc.com>
Liang Mingqiang <mqliang.zju@gmail.com> Liang Mingqiang <mqliang.zju@gmail.com>
Liang-Chi Hsieh <viirya@gmail.com> Liang-Chi Hsieh <viirya@gmail.com>
Liao Qingwei <liaoqingwei@huawei.com> Liao Qingwei <liaoqingwei@huawei.com>
@@ -354,11 +332,8 @@ Lokesh Mandvekar <lsm5@fedoraproject.org> <lsm5@redhat.com>
Lorenzo Fontana <lo@linux.com> <fontanalorenzo@me.com> Lorenzo Fontana <lo@linux.com> <fontanalorenzo@me.com>
Louis Opter <kalessin@kalessin.fr> Louis Opter <kalessin@kalessin.fr>
Louis Opter <kalessin@kalessin.fr> <louis@dotcloud.com> Louis Opter <kalessin@kalessin.fr> <louis@dotcloud.com>
Lovekesh Kumar <lovekesh.kumar@rtcamp.com>
Luca Favatella <luca.favatella@erlang-solutions.com> <lucafavatella@users.noreply.github.com> Luca Favatella <luca.favatella@erlang-solutions.com> <lucafavatella@users.noreply.github.com>
Lukas Michael <lukas.23022005@gmail.com>
Luke Marsden <me@lukemarsden.net> <luke@digital-crocus.com> Luke Marsden <me@lukemarsden.net> <luke@digital-crocus.com>
Luo Jiyin <luojiyin@hotmail.com>
Lyn <energylyn@zju.edu.cn> Lyn <energylyn@zju.edu.cn>
Lynda O'Leary <lyndaoleary29@gmail.com> Lynda O'Leary <lyndaoleary29@gmail.com>
Lynda O'Leary <lyndaoleary29@gmail.com> <lyndaoleary@hotmail.com> Lynda O'Leary <lyndaoleary29@gmail.com> <lyndaoleary@hotmail.com>
@@ -405,7 +380,6 @@ Michael Huettermann <michael@huettermann.net>
Michael Käufl <docker@c.michael-kaeufl.de> <michael-k@users.noreply.github.com> Michael Käufl <docker@c.michael-kaeufl.de> <michael-k@users.noreply.github.com>
Michael Spetsiotis <michael_spets@hotmail.com> Michael Spetsiotis <michael_spets@hotmail.com>
Michal Minář <miminar@redhat.com> Michal Minář <miminar@redhat.com>
Mickael Emirkanian <mickael.emirkanian@docker.com>
Miguel Angel Alvarez Cabrerizo <doncicuto@gmail.com> <30386061+doncicuto@users.noreply.github.com> Miguel Angel Alvarez Cabrerizo <doncicuto@gmail.com> <30386061+doncicuto@users.noreply.github.com>
Miguel Angel Fernández <elmendalerenda@gmail.com> Miguel Angel Fernández <elmendalerenda@gmail.com>
Mihai Borobocea <MihaiBorob@gmail.com> <MihaiBorobocea@gmail.com> Mihai Borobocea <MihaiBorob@gmail.com> <MihaiBorobocea@gmail.com>
@@ -415,7 +389,6 @@ Mike Dalton <mikedalton@github.com> <19153140+mikedalton@users.noreply.github.co
Mike Goelzer <mike.goelzer@docker.com> <mgoelzer@docker.com> Mike Goelzer <mike.goelzer@docker.com> <mgoelzer@docker.com>
Milind Chawre <milindchawre@gmail.com> Milind Chawre <milindchawre@gmail.com>
Misty Stanley-Jones <misty@docker.com> <misty@apache.org> Misty Stanley-Jones <misty@docker.com> <misty@apache.org>
Mohammad Hossein <mhm98035@gmail.com>
Mohit Soni <mosoni@ebay.com> <mohitsoni1989@gmail.com> Mohit Soni <mosoni@ebay.com> <mohitsoni1989@gmail.com>
Moorthy RS <rsmoorthy@gmail.com> <rsmoorthy@users.noreply.github.com> Moorthy RS <rsmoorthy@gmail.com> <rsmoorthy@users.noreply.github.com>
Morten Hekkvang <morten.hekkvang@sbab.se> Morten Hekkvang <morten.hekkvang@sbab.se>
@@ -439,7 +412,6 @@ O.S. Tezer <ostezer@gmail.com> <ostezer@users.noreply.github.com>
Oh Jinkyun <tintypemolly@gmail.com> <tintypemolly@Ohui-MacBook-Pro.local> Oh Jinkyun <tintypemolly@gmail.com> <tintypemolly@Ohui-MacBook-Pro.local>
Oliver Pomeroy <oppomeroy@gmail.com> Oliver Pomeroy <oppomeroy@gmail.com>
Ouyang Liduo <oyld0210@163.com> Ouyang Liduo <oyld0210@163.com>
Patrick St. laurent <patrick@saint-laurent.us>
Patrick Stapleton <github@gdi2290.com> Patrick Stapleton <github@gdi2290.com>
Paul Liljenberg <liljenberg.paul@gmail.com> <letters@paulnotcom.se> Paul Liljenberg <liljenberg.paul@gmail.com> <letters@paulnotcom.se>
Pavel Tikhomirov <ptikhomirov@virtuozzo.com> <ptikhomirov@parallels.com> Pavel Tikhomirov <ptikhomirov@virtuozzo.com> <ptikhomirov@parallels.com>
@@ -472,7 +444,6 @@ Roch Feuillade <roch.feuillade@pandobac.com>
Roch Feuillade <roch.feuillade@pandobac.com> <46478807+rochfeu@users.noreply.github.com> Roch Feuillade <roch.feuillade@pandobac.com> <46478807+rochfeu@users.noreply.github.com>
Roman Dudin <katrmr@gmail.com> <decadent@users.noreply.github.com> Roman Dudin <katrmr@gmail.com> <decadent@users.noreply.github.com>
Ross Boucher <rboucher@gmail.com> Ross Boucher <rboucher@gmail.com>
Rui JingAn <quiterace@gmail.com>
Runshen Zhu <runshen.zhu@gmail.com> Runshen Zhu <runshen.zhu@gmail.com>
Ryan Stelly <ryan.stelly@live.com> Ryan Stelly <ryan.stelly@live.com>
Sakeven Jiang <jc5930@sina.cn> Sakeven Jiang <jc5930@sina.cn>
@@ -519,7 +490,6 @@ Stephen Day <stevvooe@gmail.com> <stephen.day@docker.com>
Stephen Day <stevvooe@gmail.com> <stevvooe@users.noreply.github.com> Stephen Day <stevvooe@gmail.com> <stevvooe@users.noreply.github.com>
Steve Desmond <steve@vtsv.ca> <stevedesmond-ca@users.noreply.github.com> Steve Desmond <steve@vtsv.ca> <stevedesmond-ca@users.noreply.github.com>
Steve Richards <steve.richards@docker.com> stevejr <> Steve Richards <steve.richards@docker.com> stevejr <>
Stuart Williams <pid@pidster.com>
Sun Gengze <690388648@qq.com> Sun Gengze <690388648@qq.com>
Sun Jianbo <wonderflow.sun@gmail.com> Sun Jianbo <wonderflow.sun@gmail.com>
Sun Jianbo <wonderflow.sun@gmail.com> <wonderflow@zju.edu.cn> Sun Jianbo <wonderflow.sun@gmail.com> <wonderflow@zju.edu.cn>
@@ -554,8 +524,6 @@ Tim Bart <tim@fewagainstmany.com>
Tim Bosse <taim@bosboot.org> <maztaim@users.noreply.github.com> Tim Bosse <taim@bosboot.org> <maztaim@users.noreply.github.com>
Tim Ruffles <oi@truffles.me.uk> <timruffles@googlemail.com> Tim Ruffles <oi@truffles.me.uk> <timruffles@googlemail.com>
Tim Terhorst <mynamewastaken+git@gmail.com> Tim Terhorst <mynamewastaken+git@gmail.com>
Tim Welsh <timothy.welsh@docker.com>
Tim Welsh <timothy.welsh@docker.com> <84401379+twelsh-aw@users.noreply.github.com>
Tim Zju <21651152@zju.edu.cn> Tim Zju <21651152@zju.edu.cn>
Timothy Hobbs <timothyhobbs@seznam.cz> Timothy Hobbs <timothyhobbs@seznam.cz>
Toli Kuznets <toli@docker.com> Toli Kuznets <toli@docker.com>
@@ -575,8 +543,6 @@ Ulysses Souza <ulysses.souza@docker.com>
Ulysses Souza <ulysses.souza@docker.com> <ulyssessouza@gmail.com> Ulysses Souza <ulysses.souza@docker.com> <ulyssessouza@gmail.com>
Umesh Yadav <umesh4257@gmail.com> Umesh Yadav <umesh4257@gmail.com>
Umesh Yadav <umesh4257@gmail.com> <dungeonmaster18@users.noreply.github.com> Umesh Yadav <umesh4257@gmail.com> <dungeonmaster18@users.noreply.github.com>
Varun Hotani <varunhotani@gmail.com>
Vibhu Anan <vibhuanand@outlook.com>
Victor Lyuboslavsky <victor@victoreda.com> Victor Lyuboslavsky <victor@victoreda.com>
Victor Vieux <victor.vieux@docker.com> <dev@vvieux.com> Victor Vieux <victor.vieux@docker.com> <dev@vvieux.com>
Victor Vieux <victor.vieux@docker.com> <victor.vieux@dotcloud.com> Victor Vieux <victor.vieux@docker.com> <victor.vieux@dotcloud.com>
+3 -87
View File
@@ -2,7 +2,6 @@
# This file lists all contributors to the repository. # This file lists all contributors to the repository.
# See scripts/docs/generate-authors.sh to make modifications. # See scripts/docs/generate-authors.sh to make modifications.
4RH1T3CT0R7 <iprintercanon@gmail.com>
A. Lester Buck III <github-reg@nbolt.com> A. Lester Buck III <github-reg@nbolt.com>
Aanand Prasad <aanand.prasad@gmail.com> Aanand Prasad <aanand.prasad@gmail.com>
Aaron L. Xu <liker.xu@foxmail.com> Aaron L. Xu <liker.xu@foxmail.com>
@@ -27,7 +26,6 @@ Akhil Mohan <akhil.mohan@mayadata.io>
Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp> Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
Akim Demaille <akim.demaille@docker.com> Akim Demaille <akim.demaille@docker.com>
Alan Thompson <cloojure@gmail.com> Alan Thompson <cloojure@gmail.com>
Alano Terblanche <alano.terblanche@docker.com>
Albert Callarisa <shark234@gmail.com> Albert Callarisa <shark234@gmail.com>
Alberto Roura <mail@albertoroura.com> Alberto Roura <mail@albertoroura.com>
Albin Kerouanton <albinker@gmail.com> Albin Kerouanton <albinker@gmail.com>
@@ -43,15 +41,12 @@ Alexander Larsson <alexl@redhat.com>
Alexander Morozov <lk4d4math@gmail.com> Alexander Morozov <lk4d4math@gmail.com>
Alexander Ryabov <i@sepa.spb.ru> Alexander Ryabov <i@sepa.spb.ru>
Alexandre González <agonzalezro@gmail.com> Alexandre González <agonzalezro@gmail.com>
Alexandre Levavasseur <alexandre+oss@13x.fr>
Alexandre Vallières-Lagacé <alexandre.valliereslagace@docker.com>
Alexey Igrychev <alexey.igrychev@flant.com> Alexey Igrychev <alexey.igrychev@flant.com>
Alexis Couvreur <alexiscouvreur.pro@gmail.com> Alexis Couvreur <alexiscouvreur.pro@gmail.com>
Alfred Landrum <alfred.landrum@docker.com> Alfred Landrum <alfred.landrum@docker.com>
Ali Rostami <rostami.ali@gmail.com> Ali Rostami <rostami.ali@gmail.com>
Alicia Lauerman <alicia@eta.im> Alicia Lauerman <alicia@eta.im>
Allen Sun <allensun.shl@alibaba-inc.com> Allen Sun <allensun.shl@alibaba-inc.com>
Allie Sadler <allie.sadler@docker.com>
Alvin Deng <alvin.q.deng@utexas.edu> Alvin Deng <alvin.q.deng@utexas.edu>
Amen Belayneh <amenbelayneh@gmail.com> Amen Belayneh <amenbelayneh@gmail.com>
Amey Shrivastava <72866602+AmeyShrivastava@users.noreply.github.com> Amey Shrivastava <72866602+AmeyShrivastava@users.noreply.github.com>
@@ -66,13 +61,10 @@ Andreas Köhler <andi5.py@gmx.net>
Andres G. Aragoneses <knocte@gmail.com> Andres G. Aragoneses <knocte@gmail.com>
Andres Leon Rangel <aleon1220@gmail.com> Andres Leon Rangel <aleon1220@gmail.com>
Andrew France <andrew@avito.co.uk> Andrew France <andrew@avito.co.uk>
Andrew He <he.andrew.mail@gmail.com>
Andrew Hopp <andrew.hopp@me.com>
Andrew Hsu <andrewhsu@docker.com> Andrew Hsu <andrewhsu@docker.com>
Andrew Macpherson <hopscotch23@gmail.com> Andrew Macpherson <hopscotch23@gmail.com>
Andrew McDonnell <bugs@andrewmcdonnell.net> Andrew McDonnell <bugs@andrewmcdonnell.net>
Andrew Po <absourd.noise@gmail.com> Andrew Po <absourd.noise@gmail.com>
Andrew-Zipperer <atzipperer@gmail.com>
Andrey Petrov <andrey.petrov@shazow.net> Andrey Petrov <andrey.petrov@shazow.net>
Andrii Berehuliak <berkusandrew@gmail.com> Andrii Berehuliak <berkusandrew@gmail.com>
André Martins <aanm90@gmail.com> André Martins <aanm90@gmail.com>
@@ -87,16 +79,13 @@ Antonis Kalipetis <akalipetis@gmail.com>
Anusha Ragunathan <anusha.ragunathan@docker.com> Anusha Ragunathan <anusha.ragunathan@docker.com>
Ao Li <la9249@163.com> Ao Li <la9249@163.com>
Arash Deshmeh <adeshmeh@ca.ibm.com> Arash Deshmeh <adeshmeh@ca.ibm.com>
Archimedes Trajano <developer@trajano.net>
Arko Dasgupta <arko@tetrate.io> Arko Dasgupta <arko@tetrate.io>
Arnaud Porterie <icecrime@gmail.com> Arnaud Porterie <icecrime@gmail.com>
Arnaud Rebillout <elboulangero@gmail.com> Arnaud Rebillout <elboulangero@gmail.com>
Arthur Flageul <arthur.flageul@gmail.com>
Arthur Peka <arthur.peka@outlook.com> Arthur Peka <arthur.peka@outlook.com>
Ashly Mathew <ashly.mathew@sap.com> Ashly Mathew <ashly.mathew@sap.com>
Ashwini Oruganti <ashwini.oruganti@gmail.com> Ashwini Oruganti <ashwini.oruganti@gmail.com>
Aslam Ahemad <aslamahemad@gmail.com> Aslam Ahemad <aslamahemad@gmail.com>
Austin Vazquez <austin.vazquez@docker.com>
Azat Khuyiyakhmetov <shadow_uz@mail.ru> Azat Khuyiyakhmetov <shadow_uz@mail.ru>
Bardia Keyoumarsi <bkeyouma@ucsc.edu> Bardia Keyoumarsi <bkeyouma@ucsc.edu>
Barnaby Gray <barnaby@pickle.me.uk> Barnaby Gray <barnaby@pickle.me.uk>
@@ -131,23 +120,17 @@ Brian Goff <cpuguy83@gmail.com>
Brian Tracy <brian.tracy33@gmail.com> Brian Tracy <brian.tracy33@gmail.com>
Brian Wieder <brian@4wieders.com> Brian Wieder <brian@4wieders.com>
Bruno Sousa <bruno.sousa@docker.com> Bruno Sousa <bruno.sousa@docker.com>
Bruno Verachten <gounthar@gmail.com>
Bryan Bess <squarejaw@bsbess.com> Bryan Bess <squarejaw@bsbess.com>
Bryan Boreham <bjboreham@gmail.com> Bryan Boreham <bjboreham@gmail.com>
Bryan Murphy <bmurphy1976@gmail.com> Bryan Murphy <bmurphy1976@gmail.com>
bryfry <bryon.fryer@gmail.com> bryfry <bryon.fryer@gmail.com>
Calvin Liu <flycalvin@qq.com>
Cameron Spear <cameronspear@gmail.com> Cameron Spear <cameronspear@gmail.com>
Cao Weiwei <cao.weiwei30@zte.com.cn> Cao Weiwei <cao.weiwei30@zte.com.cn>
Carlo Mion <mion00@gmail.com> Carlo Mion <mion00@gmail.com>
Carlos Alexandro Becker <caarlos0@gmail.com> Carlos Alexandro Becker <caarlos0@gmail.com>
Carlos de Paula <me@carlosedp.com> Carlos de Paula <me@carlosedp.com>
carsontham <carsontham@outlook.com>
Carston Schilds <Carston.Schilds@visier.com>
Casey Korver <casey@korver.dev>
Ce Gao <ce.gao@outlook.com> Ce Gao <ce.gao@outlook.com>
Cedric Davies <cedricda@microsoft.com> Cedric Davies <cedricda@microsoft.com>
Cesar Talledo <cesar.talledo@docker.com>
Cezar Sa Espinola <cezarsa@gmail.com> Cezar Sa Espinola <cezarsa@gmail.com>
Chad Faragher <wyckster@hotmail.com> Chad Faragher <wyckster@hotmail.com>
Chao Wang <wangchao.fnst@cn.fujitsu.com> Chao Wang <wangchao.fnst@cn.fujitsu.com>
@@ -162,7 +145,6 @@ Chen Chuanliang <chen.chuanliang@zte.com.cn>
Chen Hanxiao <chenhanxiao@cn.fujitsu.com> Chen Hanxiao <chenhanxiao@cn.fujitsu.com>
Chen Mingjie <chenmingjie0828@163.com> Chen Mingjie <chenmingjie0828@163.com>
Chen Qiu <cheney-90@hotmail.com> Chen Qiu <cheney-90@hotmail.com>
Ching Wei Kang <164879897+WilliamK112@users.noreply.github.com>
Chris Chinchilla <chris@chrischinchilla.com> Chris Chinchilla <chris@chrischinchilla.com>
Chris Couzens <ccouzens@gmail.com> Chris Couzens <ccouzens@gmail.com>
Chris Gavin <chris@chrisgavin.me> Chris Gavin <chris@chrisgavin.me>
@@ -178,13 +160,10 @@ Christophe Vidal <kriss@krizalys.com>
Christopher Biscardi <biscarch@sketcht.com> Christopher Biscardi <biscarch@sketcht.com>
Christopher Crone <christopher.crone@docker.com> Christopher Crone <christopher.crone@docker.com>
Christopher Jones <tophj@linux.vnet.ibm.com> Christopher Jones <tophj@linux.vnet.ibm.com>
Christopher Petito <47751006+krissetto@users.noreply.github.com>
Christopher Petito <chrisjpetito@gmail.com>
Christopher Svensson <stoffus@stoffus.com> Christopher Svensson <stoffus@stoffus.com>
Christy Norman <christy@linux.vnet.ibm.com> Christy Norman <christy@linux.vnet.ibm.com>
Chun Chen <ramichen@tencent.com> Chun Chen <ramichen@tencent.com>
Clinton Kitson <clintonskitson@gmail.com> Clinton Kitson <clintonskitson@gmail.com>
Codex <codex@openai.com>
Coenraad Loubser <coenraad@wish.org.za> Coenraad Loubser <coenraad@wish.org.za>
Colin Hebert <hebert.colin@gmail.com> Colin Hebert <hebert.colin@gmail.com>
Collin Guarino <collin.guarino@gmail.com> Collin Guarino <collin.guarino@gmail.com>
@@ -204,7 +183,6 @@ Daisuke Ito <itodaisuke00@gmail.com>
dalanlan <dalanlan925@gmail.com> dalanlan <dalanlan925@gmail.com>
Damien Nadé <github@livna.org> Damien Nadé <github@livna.org>
Dan Cotora <dan@bluevision.ro> Dan Cotora <dan@bluevision.ro>
Dan Wallis <dan@wallis.nz>
Danial Gharib <danial.mail.gh@gmail.com> Danial Gharib <danial.mail.gh@gmail.com>
Daniel Artine <daniel.artine@ufrj.br> Daniel Artine <daniel.artine@ufrj.br>
Daniel Cassidy <mail@danielcassidy.me.uk> Daniel Cassidy <mail@danielcassidy.me.uk>
@@ -231,17 +209,15 @@ David Alvarez <david.alvarez@flyeralarm.com>
David Beitey <david@davidjb.com> David Beitey <david@davidjb.com>
David Calavera <david.calavera@gmail.com> David Calavera <david.calavera@gmail.com>
David Cramer <davcrame@cisco.com> David Cramer <davcrame@cisco.com>
David Dooling <david.dooling@docker.com> David Dooling <dooling@gmail.com>
David Gageot <david@gageot.net> David Gageot <david@gageot.net>
David Karlsson <david.karlsson@docker.com> David Karlsson <david.karlsson@docker.com>
David le Blanc <systemmonkey42@users.noreply.github.com>
David Lechner <david@lechnology.com> David Lechner <david@lechnology.com>
David Scott <dave@recoil.org> David Scott <dave@recoil.org>
David Sheets <dsheets@docker.com> David Sheets <dsheets@docker.com>
David Williamson <david.williamson@docker.com> David Williamson <david.williamson@docker.com>
David Xia <dxia@spotify.com> David Xia <dxia@spotify.com>
David Young <yangboh@cn.ibm.com> David Young <yangboh@cn.ibm.com>
Davlat Davydov <literally_user@hotmail.com>
Deng Guangxing <dengguangxing@huawei.com> Deng Guangxing <dengguangxing@huawei.com>
Denis Defreyne <denis@soundcloud.com> Denis Defreyne <denis@soundcloud.com>
Denis Gladkikh <denis@gladkikh.email> Denis Gladkikh <denis@gladkikh.email>
@@ -249,13 +225,11 @@ Denis Ollier <larchunix@users.noreply.github.com>
Dennis Docter <dennis@d23.nl> Dennis Docter <dennis@d23.nl>
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Derek McGowan <derek@mcg.dev> Derek McGowan <derek@mcg.dev>
Derek Misler <derek.misler@docker.com>
Des Preston <despreston@gmail.com> Des Preston <despreston@gmail.com>
Deshi Xiao <dxiao@redhat.com> Deshi Xiao <dxiao@redhat.com>
Dharmit Shah <shahdharmit@gmail.com> Dharmit Shah <shahdharmit@gmail.com>
Dhawal Yogesh Bhanushali <dbhanushali@vmware.com> Dhawal Yogesh Bhanushali <dbhanushali@vmware.com>
Dieter Reuter <dieter.reuter@me.com> Dieter Reuter <dieter.reuter@me.com>
Dilep Dev <34891655+DilepDev@users.noreply.github.com>
Dima Stopel <dima@twistlock.com> Dima Stopel <dima@twistlock.com>
Dimitry Andric <d.andric@activevideo.com> Dimitry Andric <d.andric@activevideo.com>
Ding Fei <dingfei@stars.org.cn> Ding Fei <dingfei@stars.org.cn>
@@ -269,7 +243,6 @@ Dominik Braun <dominik.braun@nbsp.de>
Don Kjer <don.kjer@gmail.com> Don Kjer <don.kjer@gmail.com>
Dong Chen <dongluo.chen@docker.com> Dong Chen <dongluo.chen@docker.com>
DongGeon Lee <secmatth1996@gmail.com> DongGeon Lee <secmatth1996@gmail.com>
Dorin Geman <dorin.geman@docker.com>
Doug Davis <dug@us.ibm.com> Doug Davis <dug@us.ibm.com>
Drew Erny <derny@mirantis.com> Drew Erny <derny@mirantis.com>
Ed Costello <epc@epcostello.com> Ed Costello <epc@epcostello.com>
@@ -279,7 +252,6 @@ Eli Uriegas <eli.uriegas@docker.com>
Eli Uriegas <seemethere101@gmail.com> Eli Uriegas <seemethere101@gmail.com>
Elias Faxö <elias.faxo@tre.se> Elias Faxö <elias.faxo@tre.se>
Elliot Luo <956941328@qq.com> Elliot Luo <956941328@qq.com>
Eng Zer Jun <engzerjun@gmail.com>
Eric Bode <eric.bode@foundries.io> Eric Bode <eric.bode@foundries.io>
Eric Curtin <ericcurtin17@gmail.com> Eric Curtin <ericcurtin17@gmail.com>
Eric Engestrom <eric@engestrom.ch> Eric Engestrom <eric@engestrom.ch>
@@ -326,18 +298,14 @@ Gang Qiao <qiaohai8866@gmail.com>
Gary Schaetz <gary@schaetzkc.com> Gary Schaetz <gary@schaetzkc.com>
Genki Takiuchi <genki@s21g.com> Genki Takiuchi <genki@s21g.com>
George MacRorie <gmacr31@gmail.com> George MacRorie <gmacr31@gmail.com>
George Margaritis <gmargaritis@protonmail.com>
George Xie <georgexsh@gmail.com> George Xie <georgexsh@gmail.com>
Gianluca Borello <g.borello@gmail.com> Gianluca Borello <g.borello@gmail.com>
Giau. Tran Minh <hello@giautm.dev>
Giedrius Jonikas <giedriusj1@gmail.com>
Gildas Cuisinier <gildas.cuisinier@gcuisinier.net> Gildas Cuisinier <gildas.cuisinier@gcuisinier.net>
Gio d'Amelio <giodamelio@gmail.com> Gio d'Amelio <giodamelio@gmail.com>
Gleb Stsenov <gleb.stsenov@gmail.com> Gleb Stsenov <gleb.stsenov@gmail.com>
Goksu Toprak <goksu.toprak@docker.com> Goksu Toprak <goksu.toprak@docker.com>
Gou Rao <gou@portworx.com> Gou Rao <gou@portworx.com>
Govind Rai <raigovind93@gmail.com> Govind Rai <raigovind93@gmail.com>
Grace Choi <grace.54109@gmail.com>
Graeme Wiebe <graeme.wiebe@gmail.com> Graeme Wiebe <graeme.wiebe@gmail.com>
Grant Reaber <grant.reaber@gmail.com> Grant Reaber <grant.reaber@gmail.com>
Greg Pflaum <gpflaum@users.noreply.github.com> Greg Pflaum <gpflaum@users.noreply.github.com>
@@ -360,7 +328,6 @@ Henning Sprang <henning.sprang@gmail.com>
Henry N <henrynmail-github@yahoo.de> Henry N <henrynmail-github@yahoo.de>
Hernan Garcia <hernandanielg@gmail.com> Hernan Garcia <hernandanielg@gmail.com>
Hongbin Lu <hongbin034@gmail.com> Hongbin Lu <hongbin034@gmail.com>
Hossein Abbasi <16090309+hsnabszhdn@users.noreply.github.com>
Hu Keping <hukeping@huawei.com> Hu Keping <hukeping@huawei.com>
Huayi Zhang <irachex@gmail.com> Huayi Zhang <irachex@gmail.com>
Hugo Chastel <Hugo-C@users.noreply.github.com> Hugo Chastel <Hugo-C@users.noreply.github.com>
@@ -368,7 +335,6 @@ Hugo Gabriel Eyherabide <hugogabriel.eyherabide@gmail.com>
huqun <huqun@zju.edu.cn> huqun <huqun@zju.edu.cn>
Huu Nguyen <huu@prismskylabs.com> Huu Nguyen <huu@prismskylabs.com>
Hyzhou Zhy <hyzhou.zhy@alibaba-inc.com> Hyzhou Zhy <hyzhou.zhy@alibaba-inc.com>
Iain MacDonald <IJMacD@gmail.com>
Iain Samuel McLean Elder <iain@isme.es> Iain Samuel McLean Elder <iain@isme.es>
Ian Campbell <ian.campbell@docker.com> Ian Campbell <ian.campbell@docker.com>
Ian Philpot <ian.philpot@microsoft.com> Ian Philpot <ian.philpot@microsoft.com>
@@ -418,10 +384,8 @@ Jesse Adametz <jesseadametz@gmail.com>
Jessica Frazelle <jess@oxide.computer> Jessica Frazelle <jess@oxide.computer>
Jezeniel Zapanta <jpzapanta22@gmail.com> Jezeniel Zapanta <jpzapanta22@gmail.com>
Jian Zhang <zhangjian.fnst@cn.fujitsu.com> Jian Zhang <zhangjian.fnst@cn.fujitsu.com>
Jianyong Wu <wujianyong@hygon.cn>
Jie Luo <luo612@zju.edu.cn> Jie Luo <luo612@zju.edu.cn>
Jilles Oldenbeuving <ojilles@gmail.com> Jilles Oldenbeuving <ojilles@gmail.com>
Jim Chen <njucjc@gmail.com>
Jim Galasyn <jim.galasyn@docker.com> Jim Galasyn <jim.galasyn@docker.com>
Jim Lin <b04705003@ntu.edu.tw> Jim Lin <b04705003@ntu.edu.tw>
Jimmy Leger <jimmy.leger@gmail.com> Jimmy Leger <jimmy.leger@gmail.com>
@@ -452,7 +416,6 @@ John Willis <john.willis@docker.com>
Jon Johnson <jonjohnson@google.com> Jon Johnson <jonjohnson@google.com>
Jon Zeolla <zeolla@gmail.com> Jon Zeolla <zeolla@gmail.com>
Jonatas Baldin <jonatas.baldin@gmail.com> Jonatas Baldin <jonatas.baldin@gmail.com>
Jonathan A. Sternberg <jonathansternberg@gmail.com>
Jonathan Boulle <jonathanboulle@gmail.com> Jonathan Boulle <jonathanboulle@gmail.com>
Jonathan Lee <jonjohn1232009@gmail.com> Jonathan Lee <jonjohn1232009@gmail.com>
Jonathan Lomas <jonathan@floatinglomas.ca> Jonathan Lomas <jonathan@floatinglomas.ca>
@@ -472,7 +435,6 @@ Julian <gitea+julian@ic.thejulian.uk>
Julien Barbier <write0@gmail.com> Julien Barbier <write0@gmail.com>
Julien Kassar <github@kassisol.com> Julien Kassar <github@kassisol.com>
Julien Maitrehenry <julien.maitrehenry@me.com> Julien Maitrehenry <julien.maitrehenry@me.com>
Julio Cesar Garcia <juliogarciamelgarejo@gmail.com>
Justas Brazauskas <brazauskasjustas@gmail.com> Justas Brazauskas <brazauskasjustas@gmail.com>
Justin Chadwell <me@jedevc.com> Justin Chadwell <me@jedevc.com>
Justin Cormack <justin.cormack@docker.com> Justin Cormack <justin.cormack@docker.com>
@@ -481,7 +443,6 @@ Justyn Temme <justyntemme@gmail.com>
Jyrki Puttonen <jyrkiput@gmail.com> Jyrki Puttonen <jyrkiput@gmail.com>
Jérémie Drouet <jeremie.drouet@gmail.com> Jérémie Drouet <jeremie.drouet@gmail.com>
Jérôme Petazzoni <jerome.petazzoni@docker.com> Jérôme Petazzoni <jerome.petazzoni@docker.com>
Jörg Sommer <joerg@jo-so.de>
Jörg Thalheim <joerg@higgsboson.tk> Jörg Thalheim <joerg@higgsboson.tk>
Kai Blin <kai@samba.org> Kai Blin <kai@samba.org>
Kai Qiang Wu (Kennan) <wkq5325@gmail.com> Kai Qiang Wu (Kennan) <wkq5325@gmail.com>
@@ -509,7 +470,6 @@ Kevin Woblick <mail@kovah.de>
khaled souf <khaled.souf@gmail.com> khaled souf <khaled.souf@gmail.com>
Kim Eik <kim@heldig.org> Kim Eik <kim@heldig.org>
Kir Kolyshkin <kolyshkin@gmail.com> Kir Kolyshkin <kolyshkin@gmail.com>
Kirill A. Korinsky <kirill@korins.ky>
Kotaro Yoshimatsu <kotaro.yoshimatsu@gmail.com> Kotaro Yoshimatsu <kotaro.yoshimatsu@gmail.com>
Krasi Georgiev <krasi@vip-consult.solutions> Krasi Georgiev <krasi@vip-consult.solutions>
Kris-Mikael Krister <krismikael@protonmail.com> Kris-Mikael Krister <krismikael@protonmail.com>
@@ -518,22 +478,19 @@ Kunal Kushwaha <kushwaha_kunal_v7@lab.ntt.co.jp>
Kyle Mitofsky <Kylemit@gmail.com> Kyle Mitofsky <Kylemit@gmail.com>
Lachlan Cooper <lachlancooper@gmail.com> Lachlan Cooper <lachlancooper@gmail.com>
Lai Jiangshan <jiangshanlai@gmail.com> Lai Jiangshan <jiangshanlai@gmail.com>
Lajos Papp <lajos.papp@sequenceiq.com>
Lars Kellogg-Stedman <lars@redhat.com> Lars Kellogg-Stedman <lars@redhat.com>
Laura Brehm <laurabrehm@hey.com> Laura Brehm <laurabrehm@hey.com>
Laura Frank <ljfrank@gmail.com> Laura Frank <ljfrank@gmail.com>
Laurent Erignoux <lerignoux@gmail.com> Laurent Erignoux <lerignoux@gmail.com>
Laurent Goderre <laurent.goderre@docker.com>
Lee Gaines <eightlimbed@gmail.com> Lee Gaines <eightlimbed@gmail.com>
Lei Jitang <leijitang@huawei.com> Lei Jitang <leijitang@huawei.com>
Lennie <github@consolejunkie.net> Lennie <github@consolejunkie.net>
lentil32 <lentil32@icloud.com>
Leo Gallucci <elgalu3@gmail.com> Leo Gallucci <elgalu3@gmail.com>
Leonid Skorospelov <leosko94@gmail.com> Leonid Skorospelov <leosko94@gmail.com>
Lewis Daly <lewisdaly@me.com> Lewis Daly <lewisdaly@me.com>
Li Fu Bang <lifubang@acmcoder.com> Li Fu Bang <lifubang@acmcoder.com>
Li Yi <denverdino@gmail.com> Li Yi <denverdino@gmail.com>
Li Zeghong <zeghong@hotmail.com> Li Yi <weiyuan.yl@alibaba-inc.com>
Liang-Chi Hsieh <viirya@gmail.com> Liang-Chi Hsieh <viirya@gmail.com>
Lihua Tang <lhtang@alauda.io> Lihua Tang <lhtang@alauda.io>
Lily Guo <lily.guo@docker.com> Lily Guo <lily.guo@docker.com>
@@ -546,17 +503,13 @@ lixiaobing10051267 <li.xiaobing1@zte.com.cn>
Lloyd Dewolf <foolswisdom@gmail.com> Lloyd Dewolf <foolswisdom@gmail.com>
Lorenzo Fontana <lo@linux.com> Lorenzo Fontana <lo@linux.com>
Louis Opter <kalessin@kalessin.fr> Louis Opter <kalessin@kalessin.fr>
Lovekesh Kumar <lovekesh.kumar@rtcamp.com>
Luca Favatella <luca.favatella@erlang-solutions.com> Luca Favatella <luca.favatella@erlang-solutions.com>
Luca Marturana <lucamarturana@gmail.com> Luca Marturana <lucamarturana@gmail.com>
Lucas Chan <lucas-github@lucaschan.com> Lucas Chan <lucas-github@lucaschan.com>
Ludovic Temgoua Abanda <abandaludovic500@gmail.com>
Luis Henrique Mulinari <luis.mulinari@gmail.com> Luis Henrique Mulinari <luis.mulinari@gmail.com>
Luka Hartwig <mail@lukahartwig.de> Luka Hartwig <mail@lukahartwig.de>
Lukas Heeren <lukas-heeren@hotmail.com> Lukas Heeren <lukas-heeren@hotmail.com>
Lukas Michael <lukas.23022005@gmail.com>
Lukasz Zajaczkowski <Lukasz.Zajaczkowski@ts.fujitsu.com> Lukasz Zajaczkowski <Lukasz.Zajaczkowski@ts.fujitsu.com>
Luo Jiyin <luojiyin@hotmail.com>
Lydell Manganti <LydellManganti@users.noreply.github.com> Lydell Manganti <LydellManganti@users.noreply.github.com>
Lénaïc Huard <lhuard@amadeus.com> Lénaïc Huard <lhuard@amadeus.com>
Ma Shimiao <mashimiao.fnst@cn.fujitsu.com> Ma Shimiao <mashimiao.fnst@cn.fujitsu.com>
@@ -565,7 +518,6 @@ Maciej Kalisz <maciej.d.kalisz@gmail.com>
Madhav Puri <madhav.puri@gmail.com> Madhav Puri <madhav.puri@gmail.com>
Madhu Venugopal <madhu@socketplane.io> Madhu Venugopal <madhu@socketplane.io>
Madhur Batra <madhurbatra097@gmail.com> Madhur Batra <madhurbatra097@gmail.com>
Mahesh Thakur <maheshthakur9152@gmail.com>
Malte Janduda <mail@janduda.net> Malte Janduda <mail@janduda.net>
Manjunath A Kumatagi <mkumatag@in.ibm.com> Manjunath A Kumatagi <mkumatag@in.ibm.com>
Mansi Nahar <mmn4185@rit.edu> Mansi Nahar <mmn4185@rit.edu>
@@ -578,7 +530,6 @@ Marco Vedovati <mvedovati@suse.com>
Marcus Martins <marcus@docker.com> Marcus Martins <marcus@docker.com>
Marianna Tessel <mtesselh@gmail.com> Marianna Tessel <mtesselh@gmail.com>
Marius Ileana <marius.ileana@gmail.com> Marius Ileana <marius.ileana@gmail.com>
Marius Meschter <marius@meschter.me>
Marius Sturm <marius@graylog.com> Marius Sturm <marius@graylog.com>
Mark Oates <fl0yd@me.com> Mark Oates <fl0yd@me.com>
Marsh Macy <marsma@microsoft.com> Marsh Macy <marsma@microsoft.com>
@@ -587,25 +538,19 @@ Mary Anthony <mary.anthony@docker.com>
Mason Fish <mason.fish@docker.com> Mason Fish <mason.fish@docker.com>
Mason Malone <mason.malone@gmail.com> Mason Malone <mason.malone@gmail.com>
Mateusz Major <apkd@users.noreply.github.com> Mateusz Major <apkd@users.noreply.github.com>
Mathias Duedahl <64321057+Lussebullen@users.noreply.github.com>
Mathieu Champlon <mathieu.champlon@docker.com> Mathieu Champlon <mathieu.champlon@docker.com>
Mathieu Rollet <matletix@gmail.com> Mathieu Rollet <matletix@gmail.com>
Matt Gucci <matt9ucci@gmail.com> Matt Gucci <matt9ucci@gmail.com>
Matt Robenolt <matt@ydekproductions.com> Matt Robenolt <matt@ydekproductions.com>
Matteo Orefice <matteo.orefice@bites4bits.software> Matteo Orefice <matteo.orefice@bites4bits.software>
Matteo Panzeri <matteo1782@gmail.com>
Matthew Heon <mheon@redhat.com> Matthew Heon <mheon@redhat.com>
Matthieu Hauglustaine <matt.hauglustaine@gmail.com> Matthieu Hauglustaine <matt.hauglustaine@gmail.com>
Matthieu MOREL <matthieu.morel35@gmail.com>
Mauro Porras P <mauroporrasp@gmail.com> Mauro Porras P <mauroporrasp@gmail.com>
Max Morozov <gtmax.yo@gmail.com>
Max Shytikov <mshytikov@gmail.com> Max Shytikov <mshytikov@gmail.com>
Max-Julian Pogner <max-julian@pogner.at>
Maxime Petazzoni <max@signalfuse.com> Maxime Petazzoni <max@signalfuse.com>
Maximillian Fan Xavier <maximillianfx@gmail.com> Maximillian Fan Xavier <maximillianfx@gmail.com>
Mei ChunTao <mei.chuntao@zte.com.cn> Mei ChunTao <mei.chuntao@zte.com.cn>
Melroy van den Berg <melroy@melroy.org> Melroy van den Berg <melroy@melroy.org>
Mert Şişmanoğlu <mert190737fb@gmail.com>
Metal <2466052+tedhexaflow@users.noreply.github.com> Metal <2466052+tedhexaflow@users.noreply.github.com>
Micah Zoltu <micah@newrelic.com> Micah Zoltu <micah@newrelic.com>
Michael A. Smith <michael@smith-li.com> Michael A. Smith <michael@smith-li.com>
@@ -618,12 +563,9 @@ Michael Prokop <github@michael-prokop.at>
Michael Scharf <github@scharf.gr> Michael Scharf <github@scharf.gr>
Michael Spetsiotis <michael_spets@hotmail.com> Michael Spetsiotis <michael_spets@hotmail.com>
Michael Steinert <mike.steinert@gmail.com> Michael Steinert <mike.steinert@gmail.com>
Michael Tews <michael@tews.dev>
Michael West <mwest@mdsol.com> Michael West <mwest@mdsol.com>
Michael Zampani <michael.zampani@docker.com>
Michal Minář <miminar@redhat.com> Michal Minář <miminar@redhat.com>
Michał Czeraszkiewicz <czerasz@gmail.com> Michał Czeraszkiewicz <czerasz@gmail.com>
Mickael Emirkanian <mickael.emirkanian@docker.com>
Miguel Angel Alvarez Cabrerizo <doncicuto@gmail.com> Miguel Angel Alvarez Cabrerizo <doncicuto@gmail.com>
Mihai Borobocea <MihaiBorob@gmail.com> Mihai Borobocea <MihaiBorob@gmail.com>
Mihuleacc Sergiu <mihuleac.sergiu@gmail.com> Mihuleacc Sergiu <mihuleac.sergiu@gmail.com>
@@ -636,16 +578,12 @@ Mike Goelzer <mike.goelzer@docker.com>
Mike MacCana <mike.maccana@gmail.com> Mike MacCana <mike.maccana@gmail.com>
mikelinjie <294893458@qq.com> mikelinjie <294893458@qq.com>
Mikhail Vasin <vasin@cloud-tv.ru> Mikhail Vasin <vasin@cloud-tv.ru>
Milas Bowman <milas.bowman@docker.com>
Milind Chawre <milindchawre@gmail.com> Milind Chawre <milindchawre@gmail.com>
Mindaugas Rukas <momomg@gmail.com> Mindaugas Rukas <momomg@gmail.com>
Miroslav Gula <miroslav.gula@naytrolabs.com> Miroslav Gula <miroslav.gula@naytrolabs.com>
Misty Stanley-Jones <misty@docker.com> Misty Stanley-Jones <misty@docker.com>
Mohammad Banikazemi <mb@us.ibm.com> Mohammad Banikazemi <mb@us.ibm.com>
Mohammad Hossein <mhm98035@gmail.com>
Mohammed Aaqib Ansari <maaquib@gmail.com> Mohammed Aaqib Ansari <maaquib@gmail.com>
Mohammed Aminu Futa <mohammedfuta2000@gmail.com>
Mohammed Thaha <mohammedthahacse@gmail.com>
Mohini Anne Dsouza <mohini3917@gmail.com> Mohini Anne Dsouza <mohini3917@gmail.com>
Moorthy RS <rsmoorthy@gmail.com> Moorthy RS <rsmoorthy@gmail.com>
Morgan Bauer <mbauer@us.ibm.com> Morgan Bauer <mbauer@us.ibm.com>
@@ -672,7 +610,6 @@ Nathan McCauley <nathan.mccauley@docker.com>
Neil Peterson <neilpeterson@outlook.com> Neil Peterson <neilpeterson@outlook.com>
Nick Adcock <nick.adcock@docker.com> Nick Adcock <nick.adcock@docker.com>
Nick Santos <nick.santos@docker.com> Nick Santos <nick.santos@docker.com>
Nick Sieger <nick@nicksieger.com>
Nico Stapelbroek <nstapelbroek@gmail.com> Nico Stapelbroek <nstapelbroek@gmail.com>
Nicola Kabar <nicolaka@gmail.com> Nicola Kabar <nicolaka@gmail.com>
Nicolas Borboën <ponsfrilus@gmail.com> Nicolas Borboën <ponsfrilus@gmail.com>
@@ -680,11 +617,9 @@ Nicolas De Loof <nicolas.deloof@gmail.com>
Nikhil Chawla <chawlanikhil24@gmail.com> Nikhil Chawla <chawlanikhil24@gmail.com>
Nikolas Garofil <nikolas.garofil@uantwerpen.be> Nikolas Garofil <nikolas.garofil@uantwerpen.be>
Nikolay Milovanov <nmil@itransformers.net> Nikolay Milovanov <nmil@itransformers.net>
NinaLua <iturf@sina.cn>
Nir Soffer <nsoffer@redhat.com> Nir Soffer <nsoffer@redhat.com>
Nishant Totla <nishanttotla@gmail.com> Nishant Totla <nishanttotla@gmail.com>
NIWA Hideyuki <niwa.niwa@nifty.ne.jp> NIWA Hideyuki <niwa.niwa@nifty.ne.jp>
Noah Silas <noah@hustle.com>
Noah Treuhaft <noah.treuhaft@docker.com> Noah Treuhaft <noah.treuhaft@docker.com>
O.S. Tezer <ostezer@gmail.com> O.S. Tezer <ostezer@gmail.com>
Oded Arbel <oded@geek.co.il> Oded Arbel <oded@geek.co.il>
@@ -697,18 +632,15 @@ Olli Janatuinen <olli.janatuinen@gmail.com>
Oscar Wieman <oscrx@icloud.com> Oscar Wieman <oscrx@icloud.com>
Otto Kekäläinen <otto@seravo.fi> Otto Kekäläinen <otto@seravo.fi>
Ovidio Mallo <ovidio.mallo@gmail.com> Ovidio Mallo <ovidio.mallo@gmail.com>
Park Jaeon <me@finalchild.dev>
Pascal Borreli <pascal@borreli.com> Pascal Borreli <pascal@borreli.com>
Patrick Böänziger <patrick.baenziger@bsi-software.com> Patrick Böänziger <patrick.baenziger@bsi-software.com>
Patrick Daigle <114765035+pdaig@users.noreply.github.com> Patrick Daigle <114765035+pdaig@users.noreply.github.com>
Patrick Hemmer <patrick.hemmer@gmail.com> Patrick Hemmer <patrick.hemmer@gmail.com>
Patrick Lang <plang@microsoft.com> Patrick Lang <plang@microsoft.com>
Patrick St. laurent <patrick@saint-laurent.us>
Paul <paul9869@gmail.com> Paul <paul9869@gmail.com>
Paul Kehrer <paul.l.kehrer@gmail.com> Paul Kehrer <paul.l.kehrer@gmail.com>
Paul Lietar <paul@lietar.net> Paul Lietar <paul@lietar.net>
Paul Mulders <justinkb@gmail.com> Paul Mulders <justinkb@gmail.com>
Paul Rogalski <mail@paul-rogalski.de>
Paul Seyfert <pseyfert.mathphys@gmail.com> Paul Seyfert <pseyfert.mathphys@gmail.com>
Paul Weaver <pauweave@cisco.com> Paul Weaver <pauweave@cisco.com>
Pavel Pospisil <pospispa@gmail.com> Pavel Pospisil <pospispa@gmail.com>
@@ -724,13 +656,13 @@ Peter Jaffe <pjaffe@nevo.com>
Peter Kehl <peter.kehl@gmail.com> Peter Kehl <peter.kehl@gmail.com>
Peter Nagy <xificurC@gmail.com> Peter Nagy <xificurC@gmail.com>
Peter Salvatore <peter@psftw.com> Peter Salvatore <peter@psftw.com>
Peter Valdemar Mørch <peter@morch.com>
Peter Waller <p@pwaller.net> Peter Waller <p@pwaller.net>
Phil Estes <estesp@gmail.com> Phil Estes <estesp@gmail.com>
Philip Alexander Etling <paetling@gmail.com> Philip Alexander Etling <paetling@gmail.com>
Philipp Gillé <philipp.gille@gmail.com> Philipp Gillé <philipp.gille@gmail.com>
Philipp Schmied <pschmied@schutzwerk.com> Philipp Schmied <pschmied@schutzwerk.com>
Phong Tran <tran.pho@northeastern.edu> Phong Tran <tran.pho@northeastern.edu>
pidster <pid@pidster.com>
Pieter E Smit <diepes@github.com> Pieter E Smit <diepes@github.com>
pixelistik <pixelistik@users.noreply.github.com> pixelistik <pixelistik@users.noreply.github.com>
Pratik Karki <prertik@outlook.com> Pratik Karki <prertik@outlook.com>
@@ -749,7 +681,6 @@ Ray Tsang <rayt@google.com>
Reficul <xuzhenglun@gmail.com> Reficul <xuzhenglun@gmail.com>
Remy Suen <remy.suen@gmail.com> Remy Suen <remy.suen@gmail.com>
Renaud Gaubert <rgaubert@nvidia.com> Renaud Gaubert <rgaubert@nvidia.com>
René Hermenau <rene-hermenau@users.noreply.github.com>
Ricardo N Feliciano <FelicianoTech@gmail.com> Ricardo N Feliciano <FelicianoTech@gmail.com>
Rich Moyse <rich@moyse.us> Rich Moyse <rich@moyse.us>
Richard Chen Zheng <58443436+rchenzheng@users.noreply.github.com> Richard Chen Zheng <58443436+rchenzheng@users.noreply.github.com>
@@ -773,7 +704,6 @@ Rory Hunter <roryhunter2@gmail.com>
Ross Boucher <rboucher@gmail.com> Ross Boucher <rboucher@gmail.com>
Rubens Figueiredo <r.figueiredo.52@gmail.com> Rubens Figueiredo <r.figueiredo.52@gmail.com>
Rui Cao <ruicao@alauda.io> Rui Cao <ruicao@alauda.io>
Rui JingAn <quiterace@gmail.com>
Ryan Belgrave <rmb1993@gmail.com> Ryan Belgrave <rmb1993@gmail.com>
Ryan Detzel <ryan.detzel@gmail.com> Ryan Detzel <ryan.detzel@gmail.com>
Ryan Stelly <ryan.stelly@live.com> Ryan Stelly <ryan.stelly@live.com>
@@ -791,7 +721,6 @@ Samuel Cochran <sj26@sj26.com>
Samuel Karp <skarp@amazon.com> Samuel Karp <skarp@amazon.com>
Sandro Jäckel <sandro.jaeckel@gmail.com> Sandro Jäckel <sandro.jaeckel@gmail.com>
Santhosh Manohar <santhosh@docker.com> Santhosh Manohar <santhosh@docker.com>
Sarah Sanders <sarah.sanders@docker.com>
Sargun Dhillon <sargun@netflix.com> Sargun Dhillon <sargun@netflix.com>
Saswat Bhattacharya <sas.saswat@gmail.com> Saswat Bhattacharya <sas.saswat@gmail.com>
Saurabh Kumar <saurabhkumar0184@gmail.com> Saurabh Kumar <saurabhkumar0184@gmail.com>
@@ -800,7 +729,6 @@ Scott Collier <emailscottcollier@gmail.com>
Sean Christopherson <sean.j.christopherson@intel.com> Sean Christopherson <sean.j.christopherson@intel.com>
Sean Rodman <srodman7689@gmail.com> Sean Rodman <srodman7689@gmail.com>
Sebastiaan van Stijn <github@gone.nl> Sebastiaan van Stijn <github@gone.nl>
Seiya Miyata <odradek38@gmail.com>
Sergey Tryuber <Sergeant007@users.noreply.github.com> Sergey Tryuber <Sergeant007@users.noreply.github.com>
Serhat Gülçiçek <serhat25@gmail.com> Serhat Gülçiçek <serhat25@gmail.com>
Sevki Hasirci <s@sevki.org> Sevki Hasirci <s@sevki.org>
@@ -825,7 +753,6 @@ Spencer Brown <spencer@spencerbrown.org>
Spring Lee <xi.shuai@outlook.com> Spring Lee <xi.shuai@outlook.com>
squeegels <lmscrewy@gmail.com> squeegels <lmscrewy@gmail.com>
Srini Brahmaroutu <srbrahma@us.ibm.com> Srini Brahmaroutu <srbrahma@us.ibm.com>
Stavros Panakakis <stavrospanakakis@gmail.com>
Stefan S. <tronicum@user.github.com> Stefan S. <tronicum@user.github.com>
Stefan Scherer <stefan.scherer@docker.com> Stefan Scherer <stefan.scherer@docker.com>
Stefan Weil <sw@weilnetz.de> Stefan Weil <sw@weilnetz.de>
@@ -836,7 +763,6 @@ Steve Durrheimer <s.durrheimer@gmail.com>
Steve Richards <steve.richards@docker.com> Steve Richards <steve.richards@docker.com>
Steven Burgess <steven.a.burgess@hotmail.com> Steven Burgess <steven.a.burgess@hotmail.com>
Stoica-Marcu Floris-Andrei <floris.sm@gmail.com> Stoica-Marcu Floris-Andrei <floris.sm@gmail.com>
Stuart Williams <pid@pidster.com>
Subhajit Ghosh <isubuz.g@gmail.com> Subhajit Ghosh <isubuz.g@gmail.com>
Sun Jianbo <wonderflow.sun@gmail.com> Sun Jianbo <wonderflow.sun@gmail.com>
Sune Keller <absukl@almbrand.dk> Sune Keller <absukl@almbrand.dk>
@@ -871,7 +797,6 @@ Tim Hockin <thockin@google.com>
Tim Sampson <tim@sampson.fi> Tim Sampson <tim@sampson.fi>
Tim Smith <timbot@google.com> Tim Smith <timbot@google.com>
Tim Waugh <twaugh@redhat.com> Tim Waugh <twaugh@redhat.com>
Tim Welsh <timothy.welsh@docker.com>
Tim Wraight <tim.wraight@tangentlabs.co.uk> Tim Wraight <tim.wraight@tangentlabs.co.uk>
timfeirg <kkcocogogo@gmail.com> timfeirg <kkcocogogo@gmail.com>
Timothy Hobbs <timothyhobbs@seznam.cz> Timothy Hobbs <timothyhobbs@seznam.cz>
@@ -901,10 +826,8 @@ Umesh Yadav <umesh4257@gmail.com>
Vaclav Struhar <struharv@gmail.com> Vaclav Struhar <struharv@gmail.com>
Valentin Lorentz <progval+git@progval.net> Valentin Lorentz <progval+git@progval.net>
Vardan Pogosian <vardan.pogosyan@gmail.com> Vardan Pogosian <vardan.pogosyan@gmail.com>
Varun Hotani <varunhotani@gmail.com>
Venkateswara Reddy Bukkasamudram <bukkasamudram@outlook.com> Venkateswara Reddy Bukkasamudram <bukkasamudram@outlook.com>
Veres Lajos <vlajos@gmail.com> Veres Lajos <vlajos@gmail.com>
Vibhu Anan <vibhuanand@outlook.com>
Victor Vieux <victor.vieux@docker.com> Victor Vieux <victor.vieux@docker.com>
Victoria Bialas <victoria.bialas@docker.com> Victoria Bialas <victoria.bialas@docker.com>
Viktor Stanchev <me@viktorstanchev.com> Viktor Stanchev <me@viktorstanchev.com>
@@ -914,7 +837,6 @@ Vincent Batts <vbatts@redhat.com>
Vincent Bernat <Vincent.Bernat@exoscale.ch> Vincent Bernat <Vincent.Bernat@exoscale.ch>
Vincent Demeester <vincent.demeester@docker.com> Vincent Demeester <vincent.demeester@docker.com>
Vincent Woo <me@vincentwoo.com> Vincent Woo <me@vincentwoo.com>
Vineet Kumar <vineetkumar17112004@gmail.com>
Vishnu Kannan <vishnuk@google.com> Vishnu Kannan <vishnuk@google.com>
Vivek Goyal <vgoyal@redhat.com> Vivek Goyal <vgoyal@redhat.com>
Wang Jie <wangjie5@chinaskycloud.com> Wang Jie <wangjie5@chinaskycloud.com>
@@ -927,11 +849,9 @@ Wang Yumu <37442693@qq.com>
Wataru Ishida <ishida.wataru@lab.ntt.co.jp> Wataru Ishida <ishida.wataru@lab.ntt.co.jp>
Wayne Song <wsong@docker.com> Wayne Song <wsong@docker.com>
Wen Cheng Ma <wenchma@cn.ibm.com> Wen Cheng Ma <wenchma@cn.ibm.com>
Wenlong Zhang <zhangwenlong@loongson.cn>
Wenzhi Liang <wenzhi.liang@gmail.com> Wenzhi Liang <wenzhi.liang@gmail.com>
Wes Morgan <cap10morgan@gmail.com> Wes Morgan <cap10morgan@gmail.com>
Wewang Xiaorenfine <wang.xiaoren@zte.com.cn> Wewang Xiaorenfine <wang.xiaoren@zte.com.cn>
Will Wang <willww64@gmail.com>
William Henry <whenry@redhat.com> William Henry <whenry@redhat.com>
Xianglin Gao <xlgao@zju.edu.cn> Xianglin Gao <xlgao@zju.edu.cn>
Xiaodong Liu <liuxiaodong@loongson.cn> Xiaodong Liu <liuxiaodong@loongson.cn>
@@ -944,7 +864,6 @@ Yanqiang Miao <miao.yanqiang@zte.com.cn>
Yassine Tijani <yasstij11@gmail.com> Yassine Tijani <yasstij11@gmail.com>
Yi EungJun <eungjun.yi@navercorp.com> Yi EungJun <eungjun.yi@navercorp.com>
Ying Li <ying.li@docker.com> Ying Li <ying.li@docker.com>
Yoan Wainmann <thebook90yw@gmail.com>
Yong Tang <yong.tang.github@outlook.com> Yong Tang <yong.tang.github@outlook.com>
Yosef Fertel <yfertel@gmail.com> Yosef Fertel <yfertel@gmail.com>
Yu Peng <yu.peng36@zte.com.cn> Yu Peng <yu.peng36@zte.com.cn>
@@ -961,14 +880,11 @@ Zhang Wei <zhangwei555@huawei.com>
Zhang Wentao <zhangwentao234@huawei.com> Zhang Wentao <zhangwentao234@huawei.com>
ZhangHang <stevezhang2014@gmail.com> ZhangHang <stevezhang2014@gmail.com>
zhenghenghuo <zhenghenghuo@zju.edu.cn> zhenghenghuo <zhenghenghuo@zju.edu.cn>
Zhiwei Liang <zliang@akamai.com>
Zhou Hao <zhouhao@cn.fujitsu.com> Zhou Hao <zhouhao@cn.fujitsu.com>
Zhoulin Xie <zhoulin.xie@daocloud.io> Zhoulin Xie <zhoulin.xie@daocloud.io>
Zhu Guihua <zhugh.fnst@cn.fujitsu.com> Zhu Guihua <zhugh.fnst@cn.fujitsu.com>
Zhuo Zhi <h.dwwwwww@gmail.com>
Álex González <agonzalezro@gmail.com> Álex González <agonzalezro@gmail.com>
Álvaro Lázaro <alvaro.lazaro.g@gmail.com> Álvaro Lázaro <alvaro.lazaro.g@gmail.com>
Átila Camurça Alves <camurca.home@gmail.com> Átila Camurça Alves <camurca.home@gmail.com>
Александр Менщиков <__Singleton__@hackerdom.ru> Александр Менщиков <__Singleton__@hackerdom.ru>
徐俊杰 <paco.xu@daocloud.io> 徐俊杰 <paco.xu@daocloud.io>
林博仁 Buo-ren Lin <Buo.Ren.Lin@gmail.com>
+15 -50
View File
@@ -16,9 +16,9 @@ start participating.
## Reporting security issues ## Reporting security issues
The Docker maintainers take security seriously. If you discover a security The Docker maintainers take security seriously. If you discover a security
issue, bring it to their attention right away! issue, please bring it to their attention right away!
**DO NOT** file a public issue, instead send your report privately to Please **DO NOT** file a public issue, instead send your report privately to
[security@docker.com](mailto:security@docker.com). [security@docker.com](mailto:security@docker.com).
Security reports are greatly appreciated and we will publicly thank you for it. Security reports are greatly appreciated and we will publicly thank you for it.
@@ -39,7 +39,7 @@ If you find a match, you can use the "subscribe" button to get notified on
updates. Do *not* leave random "+1" or "I have this too" comments, as they updates. Do *not* leave random "+1" or "I have this too" comments, as they
only clutter the discussion, and don't help resolving it. However, if you only clutter the discussion, and don't help resolving it. However, if you
have ways to reproduce the issue or have additional information that may help have ways to reproduce the issue or have additional information that may help
resolving the issue, leave a comment. resolving the issue, please leave a comment.
When reporting issues, always include: When reporting issues, always include:
@@ -66,7 +66,7 @@ anybody starts working on it.
We are always thrilled to receive pull requests. We do our best to process them We are always thrilled to receive pull requests. We do our best to process them
quickly. If your pull request is not accepted on the first try, quickly. If your pull request is not accepted on the first try,
don't get discouraged! Our contributor's guide explains [the review process we don't get discouraged! Our contributor's guide explains [the review process we
use for simple changes](https://github.com/moby/moby/blob/master/project/REVIEWING.md). use for simple changes](https://docs.docker.com/opensource/workflow/make-a-contribution/).
### Talking to other Docker users and contributors ### Talking to other Docker users and contributors
@@ -84,7 +84,7 @@ use for simple changes](https://github.com/moby/moby/blob/master/project/REVIEWI
<tr> <tr>
<td>Community Slack</td> <td>Community Slack</td>
<td> <td>
The Docker Community has a dedicated Slack chat to discuss features and issues. You can sign-up <a href="https://dockr.ly/comm-slack" target="_blank">with this link</a>. The Docker Community has a dedicated Slack chat to discuss features and issues. You can sign-up <a href="https://dockr.ly/slack" target="_blank">with this link</a>.
</td> </td>
</tr> </tr>
<tr> <tr>
@@ -124,7 +124,8 @@ submitting a pull request.
Update the documentation when creating or modifying features. Test your Update the documentation when creating or modifying features. Test your
documentation changes for clarity, concision, and correctness, as well as a documentation changes for clarity, concision, and correctness, as well as a
clean documentation build. See our contributors guide for [our style clean documentation build. See our contributors guide for [our style
guide](https://github.com/docker/docs/blob/main/STYLE.md). guide](https://docs.docker.com/opensource/doc-style) and instructions on [building
the documentation](https://docs.docker.com/opensource/project/test-and-docs/#build-and-test-the-documentation).
Write clean code. Universally formatted code promotes ease of writing, reading, Write clean code. Universally formatted code promotes ease of writing, reading,
and maintenance. Always run `gofmt -s -w file.go` on each changed file before and maintenance. Always run `gofmt -s -w file.go` on each changed file before
@@ -133,45 +134,9 @@ committing your changes. Most editors have plug-ins that do this automatically.
Pull request descriptions should be as clear as possible and include a reference Pull request descriptions should be as clear as possible and include a reference
to all the issues that they address. to all the issues that they address.
Commit messages must be written in the imperative mood (max. 72 chars), followed Commit messages must start with a capitalized and short summary (max. 50 chars)
by an optional, more detailed explanatory text usually expanding on written in the imperative, followed by an optional, more detailed explanatory
why the work is necessary. The explanatory text should be separated by an text which is separated from the summary by an empty line.
empty line.
The commit message *could* have a prefix scoping the change, however this is
not enforced. Common prefixes are `docs: <message>`, `vendor: <message>`,
`chore: <message>` or the package/area related to the change such as `pkg/foo: <message>`
or `telemetry: <message>`.
A standard commit.
```
Fix the exploding flux capacitor
A call to function A causes the flux capacitor to blow up every time
the sun and the moon align.
```
Using a package as prefix.
```
pkg/foo: prevent panic in flux capacitor
Calling function A causes the flux capacitor to blow up every time
the sun and the moon align.
```
Updating a specific vendored dependency.
```
vendor: github.com/moby/moby/client v0.4.0
```
Fixing a broken docs link.
```
docs: fix style/lint issues in deprecated.md
```
Code review comments may be added to your pull request. Discuss, then make the Code review comments may be added to your pull request. Discuss, then make the
suggested modifications and push additional commits to your feature branch. Post suggested modifications and push additional commits to your feature branch. Post
@@ -201,10 +166,10 @@ Include an issue reference like `Closes #XXXX` or `Fixes #XXXX` in the pull requ
description that close an issue. Including references automatically closes the issue description that close an issue. Including references automatically closes the issue
on a merge. on a merge.
Do not add yourself to the `AUTHORS` file, as it is regenerated regularly Please do not add yourself to the `AUTHORS` file, as it is regenerated regularly
from the Git history. from the Git history.
See the [Coding Style](#coding-style) for further guidelines. Please see the [Coding Style](#coding-style) for further guidelines.
### Merge approval ### Merge approval
@@ -267,7 +232,7 @@ Then you just add a line to every git commit message:
Signed-off-by: Joe Smith <joe.smith@email.com> Signed-off-by: Joe Smith <joe.smith@email.com>
Use a known identity (sorry, no anonymous contributions.) Use your real name (sorry, no pseudonyms or anonymous contributions.)
If you set your `user.name` and `user.email` git configs, you can sign your If you set your `user.name` and `user.email` git configs, you can sign your
commit automatically with `git commit -s`. commit automatically with `git commit -s`.
@@ -304,8 +269,8 @@ guidelines for the community as a whole:
* Stay on topic: Make sure that you are posting to the correct channel and * Stay on topic: Make sure that you are posting to the correct channel and
avoid off-topic discussions. Remember when you update an issue or respond avoid off-topic discussions. Remember when you update an issue or respond
to an email you are potentially sending to a large number of people. Consider to an email you are potentially sending to a large number of people. Please
this before you update. Also remember that nobody likes spam. consider this before you update. Also remember that nobody likes spam.
* Don't send email to the maintainers: There's no need to send email to the * Don't send email to the maintainers: There's no need to send email to the
maintainers to ask them to investigate an issue or to take a look at a maintainers to ask them to investigate an issue or to take a look at a
+18 -39
View File
@@ -1,43 +1,22 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
ARG BASE_VARIANT=alpine ARG BASE_VARIANT=alpine
ARG ALPINE_VERSION=3.20
# 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.23
ARG BASE_DEBIAN_DISTRO=bookworm ARG BASE_DEBIAN_DISTRO=bookworm
ARG GO_VERSION=1.26.10 ARG GO_VERSION=1.22.10
ARG XX_VERSION=1.6.1
# XX_VERSION specifies the version of the xx utility to use. ARG GOVERSIONINFO_VERSION=v1.3.0
# It must be a valid tag in the docker.io/tonistiigi/xx image repository. ARG GOTESTSUM_VERSION=v1.10.0
ARG XX_VERSION=1.10.0 ARG BUILDX_VERSION=0.12.1
ARG COMPOSE_VERSION=v2.24.3
# GOVERSIONINFO_VERSION is the version of GoVersionInfo to install.
# It must be a valid tag from https://github.com/josephspurrier/goversioninfo
ARG GOVERSIONINFO_VERSION=v1.5.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.13.0
# BUILDX_VERSION sets the version of buildx to use for the e2e tests.
# It must be a tag in the docker.io/docker/buildx-bin image repository
# on Docker Hub.
ARG BUILDX_VERSION=0.38.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=v5.6.0
FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx FROM --platform=$BUILDPLATFORM tonistiigi/xx:${XX_VERSION} AS xx
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS build-base-alpine FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS build-base-alpine
ENV GOTOOLCHAIN=local ENV GOTOOLCHAIN=local
COPY --link --from=xx / / COPY --link --from=xx / /
RUN apk add --no-cache bash clang lld llvm file git git-daemon RUN apk add --no-cache bash clang lld llvm file git
WORKDIR /go/src/github.com/docker/cli WORKDIR /go/src/github.com/docker/cli
FROM build-base-alpine AS build-alpine FROM build-base-alpine AS build-alpine
@@ -84,7 +63,8 @@ ARG PACKAGER_NAME
COPY --link --from=goversioninfo /out/goversioninfo /usr/bin/goversioninfo COPY --link --from=goversioninfo /out/goversioninfo /usr/bin/goversioninfo
RUN --mount=type=bind,target=.,ro \ RUN --mount=type=bind,target=.,ro \
--mount=type=cache,target=/root/.cache \ --mount=type=cache,target=/root/.cache \
--mount=type=tmpfs,target=cmd/docker/winresources \ --mount=from=dockercore/golang-cross:xx-sdk-extras,target=/xx-sdk,src=/xx-sdk \
--mount=type=tmpfs,target=cli/winresources \
# override the default behavior of go with xx-go # override the default behavior of go with xx-go
xx-go --wrap && \ xx-go --wrap && \
# export GOCACHE=$(go env GOCACHE)/$(xx-info)$([ -f /etc/alpine-release ] && echo "alpine") && \ # export GOCACHE=$(go env GOCACHE)/$(xx-info)$([ -f /etc/alpine-release ] && echo "alpine") && \
@@ -97,7 +77,7 @@ ENV GO111MODULE=auto
RUN --mount=type=bind,target=.,rw \ RUN --mount=type=bind,target=.,rw \
--mount=type=cache,target=/root/.cache \ --mount=type=cache,target=/root/.cache \
--mount=type=cache,target=/go/pkg/mod \ --mount=type=cache,target=/go/pkg/mod \
gotestsum -- -coverprofile=/tmp/coverage.txt $(go list ./... | grep -vE '/vendor/|/e2e/|/cmd/docker-trust') gotestsum -- -coverprofile=/tmp/coverage.txt $(go list ./... | grep -vE '/vendor/|/e2e/')
FROM scratch AS test-coverage FROM scratch AS test-coverage
COPY --from=test /tmp/coverage.txt /coverage.txt COPY --from=test /tmp/coverage.txt /coverage.txt
@@ -109,6 +89,7 @@ ARG GO_STRIP
ARG CGO_ENABLED ARG CGO_ENABLED
ARG VERSION ARG VERSION
RUN --mount=ro --mount=type=cache,target=/root/.cache \ RUN --mount=ro --mount=type=cache,target=/root/.cache \
--mount=from=dockercore/golang-cross:xx-sdk-extras,target=/xx-sdk,src=/xx-sdk \
xx-go --wrap && \ xx-go --wrap && \
TARGET=/out ./scripts/build/plugins e2e/cli-plugins/plugins/* TARGET=/out ./scripts/build/plugins e2e/cli-plugins/plugins/*
@@ -122,6 +103,10 @@ FROM docker/buildx-bin:${BUILDX_VERSION} AS buildx
FROM docker/compose-bin:${COMPOSE_VERSION} AS compose FROM docker/compose-bin:${COMPOSE_VERSION} AS compose
FROM e2e-base-${BASE_VARIANT} AS e2e FROM e2e-base-${BASE_VARIANT} AS e2e
ARG NOTARY_VERSION=v0.6.1
ADD --chmod=0755 https://github.com/theupdateframework/notary/releases/download/${NOTARY_VERSION}/notary-Linux-amd64 /usr/local/bin/notary
COPY --link e2e/testdata/notary/root-ca.cert /usr/share/ca-certificates/notary.cert
RUN echo 'notary.cert' >> /etc/ca-certificates.conf && update-ca-certificates
COPY --link --from=gotestsum /out/gotestsum /usr/bin/gotestsum COPY --link --from=gotestsum /out/gotestsum /usr/bin/gotestsum
COPY --link --from=build /out ./build/ COPY --link --from=build /out ./build/
COPY --link --from=build-plugins /out ./build/ COPY --link --from=build-plugins /out ./build/
@@ -130,7 +115,7 @@ COPY --link --from=compose /docker-compose /usr/libexec/docker/cli-plugins/docke
COPY --link . . COPY --link . .
ENV DOCKER_BUILDKIT=1 ENV DOCKER_BUILDKIT=1
ENV PATH=/go/src/github.com/docker/cli/build:$PATH ENV PATH=/go/src/github.com/docker/cli/build:$PATH
CMD ["./scripts/test/e2e/entry"] CMD ./scripts/test/e2e/entry
FROM build-base-${BASE_VARIANT} AS dev FROM build-base-${BASE_VARIANT} AS dev
COPY --link . . COPY --link . .
@@ -138,14 +123,8 @@ COPY --link . .
FROM scratch AS plugins FROM scratch AS plugins
COPY --from=build-plugins /out . COPY --from=build-plugins /out .
FROM scratch AS bin-image-linux FROM scratch AS bin-image
COPY --from=build /out/docker /docker COPY --from=build /out/docker /docker
FROM scratch AS bin-image-darwin
COPY --from=build /out/docker /docker
FROM scratch AS bin-image-windows
COPY --from=build /out/docker /docker.exe
FROM bin-image-${TARGETOS} AS bin-image
FROM scratch AS binary FROM scratch AS binary
COPY --from=build /out . COPY --from=build /out .
+10 -46
View File
@@ -34,12 +34,12 @@ test: test-unit ## run tests
.PHONY: test-unit .PHONY: test-unit
test-unit: ## run unit tests, to change the output format use: GOTESTSUM_FORMAT=(dots|short|standard-quiet|short-verbose|standard-verbose) make test-unit test-unit: ## run unit tests, to change the output format use: GOTESTSUM_FORMAT=(dots|short|standard-quiet|short-verbose|standard-verbose) make test-unit
gotestsum -- $${TESTDIRS:-$(shell go list ./... | grep -vE '/vendor/|/e2e/|/cmd/docker-trust')} $(TESTFLAGS) gotestsum -- $${TESTDIRS:-$(shell go list ./... | grep -vE '/vendor/|/e2e/')} $(TESTFLAGS)
.PHONY: test-coverage .PHONY: test-coverage
test-coverage: ## run test coverage test-coverage: ## run test coverage
mkdir -p $(CURDIR)/build/coverage mkdir -p $(CURDIR)/build/coverage
gotestsum -- $(shell go list ./... | grep -vE '/vendor/|/e2e/|/cmd/docker-trust') -coverprofile=$(CURDIR)/build/coverage/coverage.txt gotestsum -- $(shell go list ./... | grep -vE '/vendor/|/e2e/') -coverprofile=$(CURDIR)/build/coverage/coverage.txt
.PHONY: lint .PHONY: lint
lint: ## run all the lint tools lint: ## run all the lint tools
@@ -52,7 +52,7 @@ shellcheck: ## run shellcheck validation
.PHONY: fmt .PHONY: fmt
fmt: ## run gofumpt (if present) or gofmt fmt: ## run gofumpt (if present) or gofmt
@if command -v gofumpt > /dev/null; then \ @if command -v gofumpt > /dev/null; then \
gofumpt -w -d -lang=1.24 . ; \ gofumpt -w -d -lang=1.19 . ; \
else \ else \
go list -f {{.Dir}} ./... | xargs gofmt -w -s -d ; \ go list -f {{.Dir}} ./... | xargs gofmt -w -s -d ; \
fi fi
@@ -67,72 +67,36 @@ dynbinary: ## build dynamically linked binary
.PHONY: plugins .PHONY: plugins
plugins: ## build example CLI plugins plugins: ## build example CLI plugins
scripts/build/plugins ./scripts/build/plugins
.PHONY: trust-plugin
trust-plugin: ## build docker-trust CLI plugins
scripts/build/trust-plugin
.PHONY: install-trust-plugin
install-trust-plugin: trust-plugin
install-trust-plugin: ## install docker-trust CLI plugins
install -D -m 0755 "$$(readlink -f build/docker-trust)" /usr/libexec/docker/cli-plugins/docker-trust
.PHONY: vendor .PHONY: vendor
vendor: ## update vendor with go modules vendor: ## update vendor with go modules
rm -rf vendor rm -rf vendor
scripts/with-go-mod.sh scripts/vendor update ./scripts/vendor update
.PHONY: validate-vendor .PHONY: validate-vendor
validate-vendor: ## validate vendor validate-vendor: ## validate vendor
scripts/with-go-mod.sh scripts/vendor validate ./scripts/vendor validate
.PHONY: mod-outdated .PHONY: mod-outdated
mod-outdated: ## check outdated dependencies mod-outdated: ## check outdated dependencies
scripts/with-go-mod.sh scripts/vendor outdated ./scripts/vendor outdated
.PHONY: authors .PHONY: authors
authors: ## generate AUTHORS file from git history authors: ## generate AUTHORS file from git history
scripts/docs/generate-authors.sh scripts/docs/generate-authors.sh
.PHONY: completion
completion: shell-completion
completion: ## generate and install the shell-completion scripts
# Note: this uses system-wide paths, and so may overwrite completion
# scripts installed as part of deb/rpm packages.
#
# Given that this target is intended to debug/test updated versions, we could
# consider installing in per-user (~/.config, XDG_DATA_DIR) paths instead, but
# this will add more complexity.
#
# See https://github.com/docker/cli/pull/5770#discussion_r1927772710
install -D -p -m 0644 ./build/completion/bash/docker /usr/share/bash-completion/completions/docker
install -D -p -m 0644 ./build/completion/fish/docker.fish debian/docker-ce-cli/usr/share/fish/vendor_completions.d/docker.fish
install -D -p -m 0644 ./build/completion/zsh/_docker debian/docker-ce-cli/usr/share/zsh/vendor-completions/_docker
build/docker:
# This target is used by the "shell-completion" target, which requires either
# "binary" or "dynbinary" to have been built. We don't want to trigger those
# to prevent replacing a static binary with a dynamic one, or vice-versa.
@echo "Run 'make binary' or 'make dynbinary' first" && exit 1
.PHONY: shell-completion
shell-completion: build/docker # requires either "binary" or "dynbinary" to be built.
shell-completion: ## generate shell-completion scripts
@ ./scripts/build/shell-completion
.PHONY: manpages .PHONY: manpages
manpages: ## generate man pages from go source and markdown manpages: ## generate man pages from go source and markdown
scripts/with-go-mod.sh scripts/docs/generate-man.sh scripts/docs/generate-man.sh
.PHONY: mddocs .PHONY: mddocs
mddocs: ## generate markdown files from go source mddocs: ## generate markdown files from go source
scripts/with-go-mod.sh scripts/docs/generate-md.sh scripts/docs/generate-md.sh
.PHONY: yamldocs .PHONY: yamldocs
yamldocs: ## generate documentation YAML files consumed by docs repo yamldocs: ## generate documentation YAML files consumed by docs repo
scripts/with-go-mod.sh scripts/docs/generate-yaml.sh scripts/docs/generate-yaml.sh
.PHONY: help .PHONY: help
help: ## print this help help: ## print this help
+1 -1
View File
@@ -14,6 +14,6 @@ United States and other governments.
It is your responsibility to ensure that your use and/or transfer does not It is your responsibility to ensure that your use and/or transfer does not
violate applicable laws. violate applicable laws.
For more information, see https://www.bis.doc.gov For more information, please see https://www.bis.doc.gov
See also https://www.apache.org/dev/crypto.html and/or seek legal counsel. See also https://www.apache.org/dev/crypto.html and/or seek legal counsel.
+5 -17
View File
@@ -1,9 +1,9 @@
# Docker CLI # Docker CLI
[![PkgGoDev](https://pkg.go.dev/badge/github.com/docker/cli)](https://pkg.go.dev/github.com/docker/cli) [![PkgGoDev](https://img.shields.io/badge/go.dev-docs-007d9c?logo=go&logoColor=white)](https://pkg.go.dev/github.com/docker/cli)
[![Build Status](https://img.shields.io/github/actions/workflow/status/docker/cli/build.yml?branch=master&label=build&logo=github)](https://github.com/docker/cli/actions?query=workflow%3Abuild) [![Build Status](https://img.shields.io/github/actions/workflow/status/docker/cli/build.yml?branch=master&label=build&logo=github)](https://github.com/docker/cli/actions?query=workflow%3Abuild)
[![Test Status](https://img.shields.io/github/actions/workflow/status/docker/cli/test.yml?branch=master&label=test&logo=github)](https://github.com/docker/cli/actions?query=workflow%3Atest) [![Test Status](https://img.shields.io/github/actions/workflow/status/docker/cli/test.yml?branch=master&label=test&logo=github)](https://github.com/docker/cli/actions?query=workflow%3Atest)
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/docker/cli/badge)](https://scorecard.dev/viewer/?uri=github.com/docker/cli) [![Go Report Card](https://goreportcard.com/badge/github.com/docker/cli)](https://goreportcard.com/report/github.com/docker/cli)
[![Codecov](https://img.shields.io/codecov/c/github/docker/cli?logo=codecov)](https://codecov.io/gh/docker/cli) [![Codecov](https://img.shields.io/codecov/c/github/docker/cli?logo=codecov)](https://codecov.io/gh/docker/cli)
## About ## About
@@ -50,18 +50,6 @@ Run test:
docker buildx bake test docker buildx bake test
``` ```
Run the unit test:
```
$ make -f docker.Makefile test-unit
```
Run the full test suite:
```
$ make -f docker.Makefile test
```
List all the available targets: List all the available targets:
```shell ```shell
@@ -73,13 +61,13 @@ make help
Start an interactive development environment: Start an interactive development environment:
```shell ```shell
make shell make -f docker.Makefile shell
``` ```
## Legal ## Legal
*Brought to you courtesy of our legal counsel. For more context, *Brought to you courtesy of our legal counsel. For more context,
see the [NOTICE](https://github.com/docker/cli/blob/master/NOTICE) document in this repo.* please see the [NOTICE](https://github.com/docker/cli/blob/master/NOTICE) document in this repo.*
Use and transfer of Docker may be subject to certain restrictions by the Use and transfer of Docker may be subject to certain restrictions by the
United States and other governments. United States and other governments.
@@ -87,7 +75,7 @@ United States and other governments.
It is your responsibility to ensure that your use and/or transfer does not It is your responsibility to ensure that your use and/or transfer does not
violate applicable laws. violate applicable laws.
For more information, see https://www.bis.doc.gov For more information, please see https://www.bis.doc.gov
## Licensing ## Licensing
-44
View File
@@ -1,44 +0,0 @@
# Security Policy
The maintainers of the Docker CLI take security seriously. If you discover
a security issue, please bring it to their attention right away!
## Reporting a Vulnerability
Please **DO NOT** file a public issue, instead send your report privately
to [security@docker.com](mailto:security@docker.com).
Reporter(s) can expect a response within 72 hours, acknowledging the issue was
received.
## Review Process
After receiving the report, an initial triage and technical analysis is
performed to confirm the report and determine its scope. We may request
additional information in this stage of the process.
Once a reviewer has confirmed the relevance of the report, a draft security
advisory will be created on GitHub. The draft advisory will be used to discuss
the issue with maintainers, the reporter(s), and where applicable, other
affected parties under embargo.
If the vulnerability is accepted, a timeline for developing a patch, public
disclosure, and patch release will be determined. If there is an embargo period
on public disclosure before the patch release, the reporter(s) are expected to
participate in the discussion of the timeline and abide by agreed upon dates
for public disclosure.
## Accreditation
Security reports are greatly appreciated and we will publicly thank you,
although we will keep your name confidential if you request it. We also like to
send gifts - if you're into swag, make sure to let us know. We do not currently
offer a paid security bounty program at this time.
## Supported Versions
This project uses long-lived branches to maintain releases, and follows
the maintenance cycle of the Moby project.
Refer to [BRANCHES-AND-TAGS.md](https://github.com/moby/moby/blob/master/project/BRANCHES-AND-TAGS.md)
in the default branch of the moby repository to learn about the current
maintenance status of each branch.
+1 -1
View File
@@ -1 +1 @@
29.9.3 25.0.0-dev
+15 -16
View File
@@ -5,32 +5,31 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/docker/cli/cli-plugins/metadata" "github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli-plugins/plugin" "github.com/docker/cli/cli-plugins/plugin"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/moby/moby/client"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func main() { func main() {
plugin.Run(func(dockerCLI command.Cli) *cobra.Command { plugin.Run(func(dockerCli command.Cli) *cobra.Command {
goodbye := &cobra.Command{ goodbye := &cobra.Command{
Use: "goodbye", Use: "goodbye",
Short: "Say Goodbye instead of Hello", Short: "Say Goodbye instead of Hello",
Run: func(cmd *cobra.Command, _ []string) { Run: func(cmd *cobra.Command, _ []string) {
_, _ = fmt.Fprintln(dockerCLI.Out(), "Goodbye World!") fmt.Fprintln(dockerCli.Out(), "Goodbye World!")
}, },
} }
apiversion := &cobra.Command{ apiversion := &cobra.Command{
Use: "apiversion", Use: "apiversion",
Short: "Print the API version of the server", Short: "Print the API version of the server",
RunE: func(_ *cobra.Command, _ []string) error { RunE: func(_ *cobra.Command, _ []string) error {
apiClient := dockerCLI.Client() cli := dockerCli.Client()
ping, err := apiClient.Ping(context.Background(), client.PingOptions{}) ping, err := cli.Ping(context.Background())
if err != nil { if err != nil {
return err return err
} }
_, _ = fmt.Println(ping.APIVersion) fmt.Println(ping.APIVersion)
return nil return nil
}, },
} }
@@ -39,7 +38,7 @@ func main() {
Use: "exitstatus2", Use: "exitstatus2",
Short: "Exit with status 2", Short: "Exit with status 2",
RunE: func(_ *cobra.Command, _ []string) error { RunE: func(_ *cobra.Command, _ []string) error {
_, _ = fmt.Fprintln(dockerCLI.Err(), "Exiting with error status 2") fmt.Fprintln(dockerCli.Err(), "Exiting with error status 2")
os.Exit(2) os.Exit(2)
return nil return nil
}, },
@@ -57,33 +56,33 @@ func main() {
return err return err
} }
if preRun { if preRun {
_, _ = fmt.Fprintln(dockerCLI.Err(), "Plugin PersistentPreRunE called") fmt.Fprintf(dockerCli.Err(), "Plugin PersistentPreRunE called")
} }
return nil return nil
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
if debug { if debug {
_, _ = fmt.Fprintln(dockerCLI.Err(), "Plugin debug mode enabled") fmt.Fprintf(dockerCli.Err(), "Plugin debug mode enabled")
} }
switch optContext { switch optContext {
case "Christmas": case "Christmas":
_, _ = fmt.Fprintln(dockerCLI.Out(), "Merry Christmas!") fmt.Fprintf(dockerCli.Out(), "Merry Christmas!\n")
return nil return nil
case "": case "":
// nothing // nothing
} }
if who == "" { if who == "" {
who, _ = dockerCLI.ConfigFile().PluginConfig("helloworld", "who") who, _ = dockerCli.ConfigFile().PluginConfig("helloworld", "who")
} }
if who == "" { if who == "" {
who = "World" who = "World"
} }
_, _ = fmt.Fprintln(dockerCLI.Out(), "Hello", who) fmt.Fprintf(dockerCli.Out(), "Hello %s!\n", who)
dockerCLI.ConfigFile().SetPluginConfig("helloworld", "lastwho", who) dockerCli.ConfigFile().SetPluginConfig("helloworld", "lastwho", who)
return dockerCLI.ConfigFile().Save() return dockerCli.ConfigFile().Save()
}, },
} }
@@ -98,7 +97,7 @@ func main() {
cmd.AddCommand(goodbye, apiversion, exitStatus2) cmd.AddCommand(goodbye, apiversion, exitStatus2)
return cmd return cmd
}, },
metadata.Metadata{ manager.Metadata{
SchemaVersion: "0.1.0", SchemaVersion: "0.1.0",
Vendor: "Docker Inc.", Vendor: "Docker Inc.",
Version: "testing", Version: "testing",
-89
View File
@@ -1,89 +0,0 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.26
// Package hooks defines the contract between the Docker CLI and CLI plugin hook
// implementations.
//
// # Audience
//
// This package is intended to be imported by CLI plugin implementations that
// implement a "hooks" subcommand, and by the Docker CLI when invoking those
// hooks.
//
// # Contract and wire format
//
// Hook inputs (see [Request]) are serialized as JSON and passed to the plugin hook
// subcommand (currently as a command-line argument). Hook outputs are emitted by
// the plugin as JSON (see [Response]).
//
// # Stability
//
// The types that represent the hook contract ([Request], [Response] and related
// constants) are considered part of Docker CLI's public Go API.
// Fields and values may be extended in a backwards-compatible way (for example,
// adding new fields), but existing fields and their meaning should remain stable.
// Plugins should ignore unknown fields and unknown hook types to remain
// forwards-compatible.
package hooks
// ResponseType is the type of response from the plugin.
type ResponseType int
const (
// NextSteps renders suggestions under a shared "What's next:" header.
NextSteps ResponseType = 0
// GenericMessage renders before NextSteps, without a header or indentation.
// CLI versions that do not recognize this type ignore the response.
GenericMessage ResponseType = 1
)
// Request is the type representing the information
// that plugins declaring support for hooks get passed when
// being invoked following a CLI command execution.
type Request struct {
// RootCmd is a string representing the matching hook configuration
// which is currently being invoked. If a hook for "docker context"
// is configured and the user executes "docker context ls", the plugin
// is invoked with "context".
RootCmd string `json:"RootCmd,omitzero"`
// Flags contains flags that were set on the command for which the
// hook was invoked. It uses flag names as key, with leading hyphens
// removed ("--flag" and "-flag" are included as "flag" and "f").
//
// Flag values are not included and are set to an empty string,
// except for boolean flags known to the CLI itself, for which
// the value is either "true", or "false".
//
// Plugins can use this information to adjust their [Response]
// based on whether the command triggering the hook was invoked
// with.
Flags map[string]string `json:"Flags,omitzero"`
// CommandError is a string containing the error output (if any)
// of the command for which the hook was invoked.
CommandError string `json:"CommandError,omitzero"`
}
// Response represents a plugin hook response. Plugins
// declaring support for CLI hooks need to print a JSON
// representation of this type when their hook subcommand
// is invoked.
type Response struct {
Type ResponseType `json:"Type"`
Template string `json:"Template,omitzero"`
}
// HookType is the type of response from the plugin.
//
// Deprecated: use [ResponseType] instead.
//
//go:fix inline
type HookType = ResponseType
// HookMessage represents a plugin hook response.
//
// Deprecated: use [Response] instead.
//
//go:fix inline
type HookMessage = Response
-75
View File
@@ -1,75 +0,0 @@
package hooks
import (
"fmt"
)
const (
hookTemplateCommandName = `{{command}}`
hookTemplateFlagValue = `{{flagValue %q}}`
hookTemplateArg = `{{argValue %d}}`
)
// TemplateReplaceSubcommandName returns a hook template string
// that will be replaced by the CLI subcommand being executed
//
// Example:
//
// Response{
// Type: NextSteps,
// Template: "you ran the subcommand: " + TemplateReplaceSubcommandName(),
// }
//
// When being executed after the command:
//
// docker run --name "my-container" alpine
//
// It results in the message:
//
// you ran the subcommand: run
func TemplateReplaceSubcommandName() string {
return hookTemplateCommandName
}
// TemplateReplaceFlagValue returns a hook template string that will be
// replaced with the flags value when printed by the CLI.
//
// Example:
//
// Response{
// Type: NextSteps,
// Template: "you ran a container named: " + TemplateReplaceFlagValue("name"),
// }
//
// when executed after the command:
//
// docker run --name "my-container" alpine
//
// it results in the message:
//
// you ran a container named: my-container
func TemplateReplaceFlagValue(flag string) string {
return fmt.Sprintf(hookTemplateFlagValue, flag)
}
// TemplateReplaceArg takes an index i and returns a hook
// template string that the CLI will replace the template with
// the ith argument after processing the passed flags.
//
// Example:
//
// Response{
// Type: NextSteps,
// Template: "run this image with `docker run " + TemplateReplaceArg(0) + "`",
// }
//
// when being executed after the command:
//
// docker pull alpine
//
// It results in the message:
//
// Run this image with `docker run alpine`
func TemplateReplaceArg(i int) string {
return fmt.Sprintf(hookTemplateArg, i)
}
-50
View File
@@ -1,50 +0,0 @@
package hooks_test
import (
"testing"
"github.com/docker/cli/cli-plugins/hooks"
)
func TestTemplateHelpers(t *testing.T) {
tests := []struct {
doc string
got func() string
want string
}{
{
doc: "subcommand name",
got: hooks.TemplateReplaceSubcommandName,
want: `{{command}}`,
},
{
doc: "flag value",
got: func() string {
return hooks.TemplateReplaceFlagValue("name")
},
want: `{{flagValue "name"}}`,
},
{
doc: "arg",
got: func() string {
return hooks.TemplateReplaceArg(0)
},
want: `{{argValue 0}}`,
},
{
doc: "arg",
got: func() string {
return hooks.TemplateReplaceArg(3)
},
want: `{{argValue 3}}`,
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
if got := tc.got(); got != tc.want {
t.Fatalf("expected %q, got %q", tc.want, got)
}
})
}
}
-48
View File
@@ -1,48 +0,0 @@
package hooks
import "io"
const (
whatsNext = "\n\033[1mWhat's next:\033[0m\n"
indent = " "
)
// PrintMessages groups messages by type and writes them to out. Generic messages
// appear first, without a header or indentation, followed by next steps under a
// shared "What's next:" header. Order within each type is preserved.
// Messages with no lines or an unknown type produce no output.
func PrintMessages(out io.Writer, messages []Response, evaluate func(Response) []string) {
grouped := make(map[ResponseType][]string)
for _, message := range messages {
lines := evaluate(message)
grouped[message.Type] = append(grouped[message.Type], lines...)
}
for _, responseType := range []ResponseType{GenericMessage, NextSteps} {
lines := grouped[responseType]
if len(lines) == 0 {
continue
}
header, prefix := "\n", ""
if responseType == NextSteps {
header, prefix = whatsNext, indent
}
_, _ = io.WriteString(out, header)
for _, line := range lines {
_, _ = io.WriteString(out, prefix)
_, _ = io.WriteString(out, line)
_, _ = io.WriteString(out, "\n")
}
}
}
// PrintNextSteps renders list of [NextSteps] messages and writes them
// to out. It is a no-op if messages is empty.
//
// Deprecated: use [PrintMessages] instead.
func PrintNextSteps(out io.Writer, messages []string) {
PrintMessages(out, []Response{{Type: NextSteps}}, func(Response) []string {
return messages
})
}
-128
View File
@@ -1,128 +0,0 @@
package hooks_test
import (
"strings"
"testing"
"github.com/docker/cli/cli-plugins/hooks"
"gotest.tools/v3/assert"
)
func TestPrintMessages(t *testing.T) {
const header = "\n\x1b[1mWhat's next:\x1b[0m\n"
for _, tc := range []struct {
doc string
messages []hooks.Response
expected string
}{
{doc: "no messages"},
{
doc: "no lines",
messages: []hooks.Response{
{Type: hooks.GenericMessage},
{Type: hooks.NextSteps},
},
},
{
doc: "generic message",
messages: []hooks.Response{
{Type: hooks.GenericMessage, Template: "Session active"},
},
expected: "\nSession active\n",
},
{
doc: "preserve message formatting",
messages: []hooks.Response{
{Type: hooks.GenericMessage, Template: "Session active\n\n Details"},
},
expected: "\nSession active\n\n Details\n",
},
{
doc: "next steps",
messages: []hooks.Response{
{Type: hooks.NextSteps, Template: "Try another command"},
},
expected: header + " Try another command\n",
},
{
doc: "group interleaved types and preserve order within each type",
messages: []hooks.Response{
{Type: hooks.NextSteps, Template: "First suggestion"},
{Type: hooks.GenericMessage, Template: "Session active"},
{Type: hooks.NextSteps, Template: "Second suggestion\nThird suggestion"},
{Type: hooks.GenericMessage, Template: "Another status"},
},
expected: "\nSession active\nAnother status\n" + header + " First suggestion\n Second suggestion\n Third suggestion\n",
},
{
doc: "unknown type",
messages: []hooks.Response{
{Type: hooks.ResponseType(99), Template: "Ignored"},
},
},
{
doc: "unknown type does not affect known messages",
messages: []hooks.Response{
{Type: hooks.ResponseType(99), Template: "Ignored"},
{Type: hooks.GenericMessage, Template: "Session active"},
{Type: hooks.NextSteps, Template: "Try another command"},
},
expected: "\nSession active\n" + header + " Try another command\n",
},
} {
t.Run(tc.doc, func(t *testing.T) {
var w strings.Builder
hooks.PrintMessages(&w, tc.messages, func(message hooks.Response) []string {
if message.Template == "" {
return nil
}
lines, err := hooks.ParseTemplate(message.Template, nil)
assert.NilError(t, err)
return lines
})
assert.Equal(t, w.String(), tc.expected)
})
}
}
func TestPrintNextSteps(t *testing.T) {
const header = "\n\x1b[1mWhat's next:\x1b[0m\n"
tests := []struct {
doc string
messages []string
expectedOutput string
}{
{
doc: "no messages",
messages: nil,
expectedOutput: "",
},
{
doc: "single message",
messages: []string{"Bork!"},
expectedOutput: header +
" Bork!\n",
},
{
doc: "multiple messages",
messages: []string{"Foo", "bar"},
expectedOutput: header +
" Foo\n" +
" bar\n",
},
{
doc: "preserve empty and indented lines",
messages: []string{"", " Details", ""},
expectedOutput: header + " \n Details\n \n",
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
var w strings.Builder
hooks.PrintNextSteps(&w, tc.messages)
assert.Equal(t, w.String(), tc.expectedOutput)
})
}
}
-105
View File
@@ -1,105 +0,0 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.26
package hooks
import (
"bytes"
"errors"
"fmt"
"slices"
"strings"
"text/template"
"github.com/spf13/cobra"
)
const maxMessages = 10
func ParseTemplate(hookTemplate string, cmd *cobra.Command) ([]string, error) {
out := hookTemplate
if strings.Contains(hookTemplate, "{{") {
// Message may be a template.
msgContext := commandInfo{cmd: cmd}
tmpl, err := template.New("").Funcs(template.FuncMap{
"command": msgContext.command,
"flagValue": msgContext.flagValue,
"argValue": msgContext.argValue,
// kept for backward-compatibility with old templates.
"flag": func(_ any, flagName string) (string, error) { return msgContext.flagValue(flagName) },
"arg": func(_ any, i int) (string, error) { return msgContext.argValue(i) },
}).Parse(hookTemplate)
if err != nil {
return nil, err
}
var b bytes.Buffer
err = tmpl.Execute(&b, msgContext)
if err != nil {
return nil, err
}
out = b.String()
}
if n := strings.Count(out, "\n"); n > maxMessages {
return nil, fmt.Errorf("hook template contains too many messages (%d): maximum is %d", n, maxMessages)
}
lines := strings.SplitN(out, "\n", maxMessages)
// Skip messages containing only whitespace, but preserve empty lines
// in messages that have content (e.g., for paragraph spacing).
if !slices.ContainsFunc(lines, func(line string) bool {
return strings.TrimSpace(line) != ""
}) {
return nil, nil
}
return lines, nil
}
var ErrHookTemplateParse = errors.New("failed to parse hook template")
// commandInfo provides info about the command for which the hook was invoked.
// It is used for templated hook-messages.
type commandInfo struct {
cmd *cobra.Command
}
// Name returns the name of the (sub)command for which the hook was invoked.
//
// It's used for backward-compatibility with old templates.
func (c commandInfo) Name() string {
return c.command()
}
// command returns the name of the (sub)command for which the hook was invoked.
func (c commandInfo) command() string {
if c.cmd == nil {
return ""
}
return c.cmd.Name()
}
// flagValue returns the value that was set for the given flag when the hook was invoked.
func (c commandInfo) flagValue(flagName string) (string, error) {
if c.cmd == nil {
return "", fmt.Errorf("%w: flagValue: cmd is nil", ErrHookTemplateParse)
}
f := c.cmd.Flag(flagName)
if f == nil {
return "", fmt.Errorf("%w: flagValue: no flags found", ErrHookTemplateParse)
}
return f.Value.String(), nil
}
// argValue returns the value of the nth argument.
func (c commandInfo) argValue(n int) (string, error) {
if c.cmd == nil {
return "", fmt.Errorf("%w: arg: cmd is nil", ErrHookTemplateParse)
}
flags := c.cmd.Flags()
v := flags.Arg(n)
if v == "" && n >= flags.NArg() {
return "", fmt.Errorf("%w: arg: %dth argument not set", ErrHookTemplateParse, n)
}
return v, nil
}
-135
View File
@@ -1,135 +0,0 @@
package hooks_test
import (
"testing"
"github.com/docker/cli/cli-plugins/hooks"
"github.com/spf13/cobra"
"gotest.tools/v3/assert"
)
// TestParseTemplate tests parsing templates as returned by plugins.
//
// It uses fixed string fixtures to lock in compatibility with existing
// plugin templates, so older formats continue to work even if we add new
// template forms.
//
// For helper-backed cases, it also verifies that templates produced by the
// current TemplateReplace* helpers parse to the same output. This lets us
// evolve the emitted template format without breaking older plugins.
func TestParseTemplate(t *testing.T) {
type testFlag struct {
name string
value string
}
tests := []struct {
doc string
template string // compatibility fixture; keep even if helpers emit a newer form
templateFunc func() string
flags []testFlag
args []string
expectedOutput []string
}{
{
doc: "empty template",
template: "",
expectedOutput: nil,
},
{
doc: "only empty lines",
template: "\n\n",
expectedOutput: nil,
},
{
doc: "whitespace-only lines",
template: " \n\t\n ",
expectedOutput: nil,
},
{
doc: "plain message",
template: "a plain template message",
expectedOutput: []string{"a plain template message"},
},
{
doc: "subcommand name",
template: "hello {{.Name}}", // NOTE: fixture; do not modify without considering plugin compatibility
templateFunc: func() string { return "hello " + hooks.TemplateReplaceSubcommandName() },
expectedOutput: []string{"hello pull"},
},
{
doc: "single flag",
template: `{{flag . "tag"}}`, // NOTE: fixture; do not modify without considering plugin compatibility
templateFunc: func() string { return hooks.TemplateReplaceFlagValue("tag") },
flags: []testFlag{
{name: "tag", value: "my-tag"},
},
expectedOutput: []string{"my-tag"},
},
{
doc: "multiple flags",
template: `{{flag . "test-one"}} {{flag . "test2"}}`, // NOTE: fixture; do not modify without considering plugin compatibility
templateFunc: func() string {
return hooks.TemplateReplaceFlagValue("test-one") + " " + hooks.TemplateReplaceFlagValue("test2")
},
flags: []testFlag{
{
name: "test-one",
value: "value",
},
{
name: "test2",
value: "value2",
},
},
expectedOutput: []string{"value value2"},
},
{
doc: "multiple args",
template: `{{arg . 0}} {{arg . 1}}`, // NOTE: fixture; do not modify without considering plugin compatibility
templateFunc: func() string { return hooks.TemplateReplaceArg(0) + " " + hooks.TemplateReplaceArg(1) },
args: []string{"zero", "one"},
expectedOutput: []string{"zero one"},
},
{
doc: "arg in sentence",
template: "You just pulled {{arg . 0}}", // NOTE: fixture; do not modify without considering plugin compatibility
templateFunc: func() string { return "You just pulled " + hooks.TemplateReplaceArg(0) },
args: []string{"alpine"},
expectedOutput: []string{"You just pulled alpine"},
},
{
doc: "multiline output",
template: "one line\nanother line!",
expectedOutput: []string{"one line", "another line!"},
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
testCmd := &cobra.Command{
Use: "pull",
Args: cobra.ExactArgs(len(tc.args)),
}
for _, f := range tc.flags {
_ = testCmd.Flags().String(f.name, "", "")
err := testCmd.Flag(f.name).Value.Set(f.value)
assert.NilError(t, err)
}
err := testCmd.Flags().Parse(tc.args)
assert.NilError(t, err)
// Validate using fixtures.
out, err := hooks.ParseTemplate(tc.template, testCmd)
assert.NilError(t, err)
assert.DeepEqual(t, out, tc.expectedOutput)
if tc.templateFunc != nil {
// Validate using the current template function equivalent.
out, err = hooks.ParseTemplate(tc.templateFunc(), testCmd)
assert.NilError(t, err)
assert.DeepEqual(t, out, tc.expectedOutput)
}
})
}
}
+7 -5
View File
@@ -1,10 +1,12 @@
package manager package manager
import ( import "os/exec"
"os/exec"
"github.com/docker/cli/cli-plugins/metadata" // Candidate represents a possible plugin candidate, for mocking purposes
) type Candidate interface {
Path() string
Metadata() ([]byte, error)
}
type candidate struct { type candidate struct {
path string path string
@@ -15,5 +17,5 @@ func (c *candidate) Path() string {
} }
func (c *candidate) Metadata() ([]byte, error) { func (c *candidate) Metadata() ([]byte, error) {
return exec.Command(c.path, metadata.MetadataSubcommandName).Output() // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments" return exec.Command(c.path, MetadataSubcommandName).Output()
} }
+32 -103
View File
@@ -6,10 +6,9 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/assert/cmp"
) )
type fakeCandidate struct { type fakeCandidate struct {
@@ -31,132 +30,62 @@ func (c *fakeCandidate) Metadata() ([]byte, error) {
func TestValidateCandidate(t *testing.T) { func TestValidateCandidate(t *testing.T) {
const ( const (
goodPluginName = metadata.NamePrefix + "goodplugin" goodPluginName = NamePrefix + "goodplugin"
builtinName = metadata.NamePrefix + "builtin"
builtinAlias = metadata.NamePrefix + "alias"
badPrefixPath = "/usr/local/libexec/cli-plugins/wobble" builtinName = NamePrefix + "builtin"
badNamePath = "/usr/local/libexec/cli-plugins/docker-123456" builtinAlias = NamePrefix + "alias"
goodPluginPath = "/usr/local/libexec/cli-plugins/" + goodPluginName
badPrefixPath = "/usr/local/libexec/cli-plugins/wobble"
badNamePath = "/usr/local/libexec/cli-plugins/docker-123456"
goodPluginPath = "/usr/local/libexec/cli-plugins/" + goodPluginName
metaExperimental = `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing", "Experimental": true}`
) )
fakeroot := &cobra.Command{Use: "docker"} fakeroot := &cobra.Command{Use: "docker"}
fakeroot.AddCommand(&cobra.Command{ fakeroot.AddCommand(&cobra.Command{
Use: strings.TrimPrefix(builtinName, metadata.NamePrefix), Use: strings.TrimPrefix(builtinName, NamePrefix),
Aliases: []string{ Aliases: []string{
strings.TrimPrefix(builtinAlias, metadata.NamePrefix), strings.TrimPrefix(builtinAlias, NamePrefix),
}, },
}) })
for _, tc := range []struct { for _, tc := range []struct {
name string name string
plugin *fakeCandidate c *fakeCandidate
// Either err or invalid may be non-empty, but not both (both can be empty for a good plugin). // Either err or invalid may be non-empty, but not both (both can be empty for a good plugin).
err string err string
invalid string invalid string
expVer string
}{ }{
// Invalid cases. /* Each failing one of the tests */
{ {name: "empty path", c: &fakeCandidate{path: ""}, err: "plugin candidate path cannot be empty"},
name: "empty path", {name: "bad prefix", c: &fakeCandidate{path: badPrefixPath}, err: fmt.Sprintf("does not have %q prefix", NamePrefix)},
plugin: &fakeCandidate{path: ""}, {name: "bad path", c: &fakeCandidate{path: badNamePath}, invalid: "did not match"},
err: "plugin candidate path cannot be empty", {name: "builtin command", c: &fakeCandidate{path: builtinName}, invalid: `plugin "builtin" duplicates builtin command`},
}, {name: "builtin alias", c: &fakeCandidate{path: builtinAlias}, invalid: `plugin "alias" duplicates an alias of builtin command "builtin"`},
{ {name: "fetch failure", c: &fakeCandidate{path: goodPluginPath, exec: false}, invalid: fmt.Sprintf("failed to fetch metadata: faked a failure to exec %q", goodPluginPath)},
name: "bad prefix", {name: "metadata not json", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `xyzzy`}, invalid: "invalid character"},
plugin: &fakeCandidate{path: badPrefixPath}, {name: "empty schemaversion", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{}`}, invalid: `plugin SchemaVersion "" is not valid`},
err: fmt.Sprintf("does not have %q prefix", metadata.NamePrefix), {name: "invalid schemaversion", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "xyzzy"}`}, invalid: `plugin SchemaVersion "xyzzy" is not valid`},
}, {name: "no vendor", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0"}`}, invalid: "plugin metadata does not define a vendor"},
{ {name: "empty vendor", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": ""}`}, invalid: "plugin metadata does not define a vendor"},
name: "bad path", // This one should work
plugin: &fakeCandidate{path: badNamePath}, {name: "valid", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing"}`}},
invalid: "did not match", {name: "experimental + allowing experimental", c: &fakeCandidate{path: goodPluginPath, exec: true, meta: metaExperimental}},
},
{
name: "builtin command",
plugin: &fakeCandidate{path: builtinName},
invalid: `plugin "builtin" duplicates builtin command`,
},
{
name: "builtin alias",
plugin: &fakeCandidate{path: builtinAlias},
invalid: `plugin "alias" duplicates an alias of builtin command "builtin"`,
},
{
name: "fetch failure",
plugin: &fakeCandidate{path: goodPluginPath, exec: false},
invalid: fmt.Sprintf("failed to fetch metadata: faked a failure to exec %q", goodPluginPath),
},
{
name: "metadata not json",
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `xyzzy`},
invalid: "invalid character",
},
{
name: "empty schemaversion",
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{}`},
invalid: `plugin SchemaVersion version cannot be empty`,
},
{
name: "invalid schemaversion",
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "xyzzy"}`},
invalid: `plugin SchemaVersion "xyzzy" has wrong format: must be <major>.<minor>.<patch>`,
},
{
name: "invalid schemaversion major",
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "2.0.0"}`},
invalid: `plugin SchemaVersion "2.0.0" is not supported: must be lower than 2.0.0`,
},
{
name: "no vendor",
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0"}`},
invalid: "plugin metadata does not define a vendor",
},
{
name: "empty vendor",
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": ""}`},
invalid: "plugin metadata does not define a vendor",
},
// Valid cases.
{
name: "valid",
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing"}`},
expVer: "0.1.0",
},
{
// Including the deprecated "experimental" field should not break processing.
name: "with legacy experimental",
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.1.0", "Vendor": "e2e-testing", "Experimental": true}`},
expVer: "0.1.0",
},
{
// note that this may not be supported by older CLIs
name: "new minor schema version",
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "0.2.0", "Vendor": "e2e-testing"}`},
expVer: "0.2.0",
},
{
// note that this may not be supported by older CLIs
name: "new major schema version",
plugin: &fakeCandidate{path: goodPluginPath, exec: true, meta: `{"SchemaVersion": "1.0.0", "Vendor": "e2e-testing"}`},
expVer: "1.0.0",
},
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
p, err := newPlugin(tc.plugin, fakeroot.Commands()) p, err := newPlugin(tc.c, fakeroot.Commands())
switch { switch {
case tc.err != "": case tc.err != "":
assert.ErrorContains(t, err, tc.err) assert.ErrorContains(t, err, tc.err)
case tc.invalid != "": case tc.invalid != "":
assert.NilError(t, err) assert.NilError(t, err)
assert.Assert(t, is.ErrorType(p.Err, reflect.TypeFor[*pluginError]())) assert.Assert(t, cmp.ErrorType(p.Err, reflect.TypeOf(&pluginError{})))
assert.ErrorContains(t, p.Err, tc.invalid) assert.ErrorContains(t, p.Err, tc.invalid)
default: default:
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, metadata.NamePrefix+p.Name, goodPluginName) assert.Equal(t, NamePrefix+p.Name, goodPluginName)
assert.Equal(t, p.SchemaVersion, tc.expVer) assert.Equal(t, p.SchemaVersion, "0.1.0")
assert.Equal(t, p.Vendor, "e2e-testing") assert.Equal(t, p.Vendor, "e2e-testing")
} }
}) })
+36 -17
View File
@@ -5,66 +5,85 @@ import (
"os" "os"
"sync" "sync"
"github.com/docker/cli/cli-plugins/metadata" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
const (
// CommandAnnotationPlugin is added to every stub command added by
// AddPluginCommandStubs with the value "true" and so can be
// used to distinguish plugin stubs from regular commands.
CommandAnnotationPlugin = "com.docker.cli.plugin"
// CommandAnnotationPluginVendor is added to every stub command
// added by AddPluginCommandStubs and contains the vendor of
// that plugin.
CommandAnnotationPluginVendor = "com.docker.cli.plugin.vendor"
// CommandAnnotationPluginVersion is added to every stub command
// added by AddPluginCommandStubs and contains the version of
// that plugin.
CommandAnnotationPluginVersion = "com.docker.cli.plugin.version"
// CommandAnnotationPluginInvalid is added to any stub command
// added by AddPluginCommandStubs for an invalid command (that
// is, one which failed it's candidate test) and contains the
// reason for the failure.
CommandAnnotationPluginInvalid = "com.docker.cli.plugin-invalid"
)
var pluginCommandStubsOnce sync.Once var pluginCommandStubsOnce sync.Once
// AddPluginCommandStubs adds a stub cobra.Commands for each valid and invalid // AddPluginCommandStubs adds a stub cobra.Commands for each valid and invalid
// plugin. The command stubs will have several annotations added, see // plugin. The command stubs will have several annotations added, see
// `CommandAnnotationPlugin*`. // `CommandAnnotationPlugin*`.
func AddPluginCommandStubs(dockerCLI config.Provider, rootCmd *cobra.Command) (err error) { func AddPluginCommandStubs(dockerCli command.Cli, rootCmd *cobra.Command) (err error) {
pluginCommandStubsOnce.Do(func() { pluginCommandStubsOnce.Do(func() {
var plugins []Plugin var plugins []Plugin
plugins, err = ListPlugins(dockerCLI, rootCmd) plugins, err = ListPlugins(dockerCli, rootCmd)
if err != nil { if err != nil {
return return
} }
for _, p := range plugins { for _, p := range plugins {
p := p
vendor := p.Vendor vendor := p.Vendor
if vendor == "" { if vendor == "" {
vendor = "unknown" vendor = "unknown"
} }
annotations := map[string]string{ annotations := map[string]string{
metadata.CommandAnnotationPlugin: "true", CommandAnnotationPlugin: "true",
metadata.CommandAnnotationPluginVendor: vendor, CommandAnnotationPluginVendor: vendor,
metadata.CommandAnnotationPluginVersion: p.Version, CommandAnnotationPluginVersion: p.Version,
} }
if p.Err != nil { if p.Err != nil {
annotations[metadata.CommandAnnotationPluginInvalid] = p.Err.Error() annotations[CommandAnnotationPluginInvalid] = p.Err.Error()
} }
rootCmd.AddCommand(&cobra.Command{ rootCmd.AddCommand(&cobra.Command{
Use: p.Name, Use: p.Name,
Short: p.ShortDescription, Short: p.ShortDescription,
Hidden: p.Hidden,
Run: func(_ *cobra.Command, _ []string) {}, Run: func(_ *cobra.Command, _ []string) {},
Annotations: annotations, Annotations: annotations,
DisableFlagParsing: true, DisableFlagParsing: true,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
flags := rootCmd.PersistentFlags() flags := rootCmd.PersistentFlags()
flags.SetOutput(nil) flags.SetOutput(nil)
if err := flags.Parse(args); err != nil { perr := flags.Parse(args)
if perr != nil {
return err return err
} }
if flags.Changed("help") { if flags.Changed("help") {
cmd.HelpFunc()(rootCmd, args) cmd.HelpFunc()(rootCmd, args)
return nil return nil
} }
return fmt.Errorf("docker: unknown command: docker %s\n\nRun 'docker --help' for more information", cmd.Name()) return fmt.Errorf("docker: '%s' is not a docker command.\nSee 'docker --help'", cmd.Name())
}, },
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// Delegate completion to plugin // Delegate completion to plugin
cargs := []string{p.Path, cobra.ShellCompRequestCmd, p.Name} //nolint:prealloc // no need to over-complicate things. cargs := []string{p.Path, cobra.ShellCompRequestCmd, p.Name}
cargs = append(cargs, args...) cargs = append(cargs, args...)
cargs = append(cargs, toComplete) cargs = append(cargs, toComplete)
origArgs := os.Args
os.Args = cargs os.Args = cargs
defer func() { runCommand, runErr := PluginRunCommand(dockerCli, p.Name, cmd)
os.Args = origArgs
}()
runCommand, runErr := PluginRunCommand(dockerCLI, p.Name, cmd)
if runErr != nil { if runErr != nil {
return nil, cobra.ShellCompDirectiveError return nil, cobra.ShellCompDirectiveError
} }
-86
View File
@@ -1,86 +0,0 @@
package manager
import (
"os"
"path/filepath"
"slices"
"sync"
"testing"
"github.com/docker/cli/internal/test"
"github.com/spf13/cobra"
"gotest.tools/v3/assert"
)
func TestPluginResourceAttributesEnvvar(t *testing.T) {
cmd := &cobra.Command{
Annotations: map[string]string{
cobra.CommandDisplayNameAnnotation: "docker",
},
}
// Ensure basic usage is fine.
env := appendPluginResourceAttributesEnvvar(nil, cmd, Plugin{Name: "compose"})
assert.DeepEqual(t, []string{"OTEL_RESOURCE_ATTRIBUTES=docker.cli.cobra.command_path=docker%20compose"}, env)
// Add a user-based environment variable to OTEL_RESOURCE_ATTRIBUTES.
t.Setenv("OTEL_RESOURCE_ATTRIBUTES", "a.b.c=foo")
env = appendPluginResourceAttributesEnvvar(nil, cmd, Plugin{Name: "compose"})
assert.DeepEqual(t, []string{"OTEL_RESOURCE_ATTRIBUTES=a.b.c=foo,docker.cli.cobra.command_path=docker%20compose"}, env)
}
func TestPluginStubRunEReturnsParseError(t *testing.T) {
cmd, err := preparePluginStubCommand(t)
assert.NilError(t, err)
err = cmd.RunE(cmd, []string{"--definitely-not-a-real-flag"})
assert.ErrorContains(t, err, "unknown flag: --definitely-not-a-real-flag")
}
func TestPluginStubCompletionRestoresOSArgs(t *testing.T) {
cmd, err := preparePluginStubCommand(t)
assert.NilError(t, err)
savedArgs := os.Args
t.Cleanup(func() { os.Args = savedArgs })
originalArgs := []string{"docker", "image", "ls"}
os.Args = slices.Clone(originalArgs)
_, directive := cmd.ValidArgsFunction(cmd, []string{"--all"}, "alp")
assert.Equal(t, directive, cobra.ShellCompDirectiveError)
assert.DeepEqual(t, os.Args, originalArgs)
}
func preparePluginStubCommand(t *testing.T) (*cobra.Command, error) {
t.Helper()
pluginCommandStubsOnce = sync.Once{}
tmpDir := t.TempDir()
const cliPlugin = `#!/bin/sh
printf '%s' '{"SchemaVersion":"0.1.0"}'
`
if err := os.WriteFile(filepath.Join(tmpDir, "docker-testplugin"), []byte(cliPlugin), 0o777); err != nil {
return nil, err
}
cli := test.NewFakeCli(nil)
cli.ConfigFile().CLIPluginsExtraDirs = []string{tmpDir}
root := &cobra.Command{Use: "docker"}
root.PersistentFlags().Bool("debug", false, "")
if err := AddPluginCommandStubs(cli, root); err != nil {
return nil, err
}
cmd, _, err := root.Find([]string{"testplugin"})
if err != nil {
return nil, err
}
if cmd == nil {
return nil, os.ErrNotExist
}
return cmd, nil
}
+13 -8
View File
@@ -1,10 +1,10 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.26 //go:build go1.19
package manager package manager
import ( import (
"fmt" "github.com/pkg/errors"
) )
// pluginError is set as Plugin.Err by NewPlugin if the plugin // pluginError is set as Plugin.Err by NewPlugin if the plugin
@@ -23,24 +23,29 @@ func (e *pluginError) Error() string {
return e.cause.Error() return e.cause.Error()
} }
// Cause satisfies the errors.causer interface for pluginError.
func (e *pluginError) Cause() error {
return e.cause
}
// Unwrap provides compatibility for Go 1.13 error chains. // Unwrap provides compatibility for Go 1.13 error chains.
func (e *pluginError) Unwrap() error { func (e *pluginError) Unwrap() error {
return e.cause return e.cause
} }
// MarshalText marshals the pluginError into a textual form. // MarshalText marshalls the pluginError into a textual form.
func (e *pluginError) MarshalText() (text []byte, err error) { func (e *pluginError) MarshalText() (text []byte, err error) {
return []byte(e.cause.Error()), nil return []byte(e.cause.Error()), nil
} }
// wrapAsPluginError wraps an error in a pluginError with an // wrapAsPluginError wraps an error in a pluginError with an
// additional message. // additional message, analogous to errors.Wrapf.
func wrapAsPluginError(err error, msg string) error { func wrapAsPluginError(err error, msg string) error {
return &pluginError{cause: fmt.Errorf("%s: %w", msg, err)} return &pluginError{cause: errors.Wrap(err, msg)}
} }
// newPluginError creates a new pluginError, analogous to // NewPluginError creates a new pluginError, analogous to
// errors.Errorf. // errors.Errorf.
func newPluginError(msg string, args ...any) error { func NewPluginError(msg string, args ...any) error {
return &pluginError{cause: fmt.Errorf(msg, args...)} return &pluginError{cause: errors.Errorf(msg, args...)}
} }
+3 -6
View File
@@ -2,7 +2,7 @@ package manager
import ( import (
"encoding/json" "encoding/json"
"errors" "fmt"
"testing" "testing"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
@@ -10,10 +10,10 @@ import (
) )
func TestPluginError(t *testing.T) { func TestPluginError(t *testing.T) {
err := newPluginError("new error") err := NewPluginError("new error")
assert.Check(t, is.Error(err, "new error")) assert.Check(t, is.Error(err, "new error"))
inner := errors.New("testing") inner := fmt.Errorf("testing")
err = wrapAsPluginError(inner, "wrapping") err = wrapAsPluginError(inner, "wrapping")
assert.Check(t, is.Error(err, "wrapping: testing")) assert.Check(t, is.Error(err, "wrapping: testing"))
assert.Check(t, is.ErrorIs(err, inner)) assert.Check(t, is.ErrorIs(err, inner))
@@ -21,7 +21,4 @@ func TestPluginError(t *testing.T) {
actual, err := json.Marshal(err) actual, err := json.Marshal(err)
assert.Check(t, err) assert.Check(t, err)
assert.Check(t, is.Equal(`"wrapping: testing"`, string(actual))) assert.Check(t, is.Equal(`"wrapping: testing"`, string(actual)))
err = wrapAsPluginError(nil, "wrapping")
assert.Check(t, is.Error(err, "wrapping: %!w(<nil>)"))
} }
-213
View File
@@ -1,213 +0,0 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.26
package manager
import (
"context"
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"github.com/docker/cli/cli-plugins/hooks"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// HookPluginData is the type representing the information
// that plugins declaring support for hooks get passed when
// being invoked following a CLI command execution.
//
// Deprecated: use [hooks.Request] instead.
//
//go:fix inline
type HookPluginData = hooks.Request
// RunCLICommandHooks is the entrypoint into the hooks execution flow after
// a main CLI command was executed. It calls the hook subcommand for all
// present CLI plugins that declare support for hooks in their metadata and
// parses/prints their responses.
func RunCLICommandHooks(ctx context.Context, dockerCLI config.Provider, rootCmd, subCommand *cobra.Command, cmdErrorMessage string) {
commandName := strings.TrimPrefix(subCommand.CommandPath(), rootCmd.Name()+" ")
flags := getCommandFlags(subCommand)
runHooks(ctx, dockerCLI.ConfigFile(), rootCmd, subCommand, commandName, flags, cmdErrorMessage)
}
// RunPluginHooks is the entrypoint for the hooks execution flow
// after a plugin command was just executed by the CLI.
func RunPluginHooks(ctx context.Context, dockerCLI config.Provider, rootCmd, subCommand *cobra.Command, args []string, cmdErrorMessage string) {
commandName := strings.Join(args, " ")
flags := getNaiveFlags(args)
runHooks(ctx, dockerCLI.ConfigFile(), rootCmd, subCommand, commandName, flags, cmdErrorMessage)
}
func runHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string, cmdErrorMessage string) {
messages := invokeAndCollectHooks(ctx, cfg, rootCmd, invokedCommand, flags, cmdErrorMessage)
hooks.PrintMessages(subCommand.ErrOrStderr(), messages, func(message hooks.Response) []string {
lines, err := hooks.ParseTemplate(message.Template, subCommand)
if err != nil {
logrus.WithError(err).Debug("Failed to evaluate hook template")
return nil
}
return lines
})
}
func invokeAndCollectHooks(ctx context.Context, cfg *configfile.ConfigFile, rootCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []hooks.Response {
if ctx.Err() != nil {
return nil
}
pluginsCfg := cfg.Plugins
if pluginsCfg == nil {
return nil
}
pluginDirs := getPluginDirs(cfg)
var collected []hooks.Response
tryInvokeHook := func(pluginName string, pluginCfg map[string]string) (hooks.Response, error) {
match, matched := pluginMatch(pluginCfg, subCmdStr, cmdErrorMessage)
if !matched {
return hooks.Response{}, nil
}
p, err := getPlugin(pluginName, pluginDirs, rootCmd)
if err != nil {
return hooks.Response{}, err
}
resp, err := p.RunHook(ctx, hooks.Request{
RootCmd: match,
Flags: flags,
CommandError: cmdErrorMessage,
})
if err != nil {
return hooks.Response{}, err
}
var message hooks.Response
if err := json.Unmarshal(resp, &message); err != nil {
return hooks.Response{}, fmt.Errorf("failed to unmarshal hook response (%q): %w", string(resp), err)
}
if message.Type != hooks.NextSteps && message.Type != hooks.GenericMessage {
return hooks.Response{}, errors.New("unexpected hook response type: " + strconv.Itoa(int(message.Type)))
}
return message, nil
}
for pluginName, pluginCfg := range pluginsCfg {
message, err := tryInvokeHook(pluginName, pluginCfg)
if err != nil {
// skip misbehaving plugins, but don't halt execution
logrus.WithFields(logrus.Fields{
"error": err,
"plugin": pluginName,
}).Debug("Plugin hook invocation failed")
continue
}
if message.Template == "" {
continue
}
collected = append(collected, message)
}
return collected
}
// pluginMatch takes a plugin configuration and a string representing the
// command being executed (such as 'image ls' – the root 'docker' is omitted)
// and, if the configuration includes a hook for the invoked command, returns
// the configured hook string.
//
// Plugins can declare two types of hooks in their configuration:
// - "hooks": fires on every command invocation (success or failure)
// - "error-hooks": fires only when a command fails (cmdErrorMessage is non-empty)
func pluginMatch(pluginCfg map[string]string, subCmd string, cmdErrorMessage string) (string, bool) {
// Check "hooks" first — these always fire regardless of command outcome.
if match, ok := matchHookConfig(pluginCfg["hooks"], subCmd); ok {
return match, true
}
// Check "error-hooks" — these only fire when there was an error.
if cmdErrorMessage != "" {
if match, ok := matchHookConfig(pluginCfg["error-hooks"], subCmd); ok {
return match, true
}
}
return "", false
}
// matchHookConfig checks if a comma-separated hook configuration string
// contains a prefix match for the given subcommand.
func matchHookConfig(configuredHooks string, subCmd string) (string, bool) {
if configuredHooks == "" {
return "", false
}
for hookCmd := range strings.SplitSeq(configuredHooks, ",") {
if hookMatch(hookCmd, subCmd) {
return hookCmd, true
}
}
return "", false
}
func hookMatch(hookCmd, subCmd string) bool {
hookCmdTokens := strings.Split(hookCmd, " ")
subCmdTokens := strings.Split(subCmd, " ")
if len(hookCmdTokens) > len(subCmdTokens) {
return false
}
for i, v := range hookCmdTokens {
if v != subCmdTokens[i] {
return false
}
}
return true
}
func getCommandFlags(cmd *cobra.Command) map[string]string {
flags := make(map[string]string)
cmd.Flags().Visit(func(f *pflag.Flag) {
var fValue string
if f.Value.Type() == "bool" {
fValue = f.Value.String()
}
flags[f.Name] = fValue
})
return flags
}
// getNaiveFlags string-matches argv and parses them into a map.
// This is used when calling hooks after a plugin command, since
// in this case we can't rely on the cobra command tree to parse
// flags in this case. In this case, no values are ever passed,
// since we don't have enough information to process them.
func getNaiveFlags(args []string) map[string]string {
flags := make(map[string]string)
for _, arg := range args {
if strings.HasPrefix(arg, "--") {
flags[arg[2:]] = ""
continue
}
if strings.HasPrefix(arg, "-") {
flags[arg[1:]] = ""
}
}
return flags
}
-448
View File
@@ -1,448 +0,0 @@
package manager
import (
"context"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/docker/cli/cli/config/configfile"
"github.com/spf13/cobra"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
type fakeConfigProvider struct {
cfg *configfile.ConfigFile
}
func (f *fakeConfigProvider) ConfigFile() *configfile.ConfigFile {
return f.cfg
}
func TestGetNaiveFlags(t *testing.T) {
testCases := []struct {
args []string
expectedFlags map[string]string
}{
{
args: []string{"docker"},
expectedFlags: map[string]string{},
},
{
args: []string{"docker", "build", "-q", "--file", "test.Dockerfile", "."},
expectedFlags: map[string]string{
"q": "",
"file": "",
},
},
{
args: []string{"docker", "--context", "a-context", "pull", "-q", "--progress", "auto", "alpine"},
expectedFlags: map[string]string{
"context": "",
"q": "",
"progress": "",
},
},
}
for _, tc := range testCases {
assert.DeepEqual(t, getNaiveFlags(tc.args), tc.expectedFlags)
}
}
func TestPluginMatch(t *testing.T) {
testCases := []struct {
doc string
commandString string
pluginConfig map[string]string
cmdErrorMessage string
expectedMatch string
expectedOk bool
}{
{
doc: "hooks prefix match",
commandString: "image ls",
pluginConfig: map[string]string{
"hooks": "image",
},
expectedMatch: "image",
expectedOk: true,
},
{
doc: "hooks no match",
commandString: "context ls",
pluginConfig: map[string]string{
"hooks": "build",
},
expectedMatch: "",
expectedOk: false,
},
{
doc: "hooks exact match",
commandString: "context ls",
pluginConfig: map[string]string{
"hooks": "context ls",
},
expectedMatch: "context ls",
expectedOk: true,
},
{
doc: "hooks first match wins",
commandString: "image ls",
pluginConfig: map[string]string{
"hooks": "image ls,image",
},
expectedMatch: "image ls",
expectedOk: true,
},
{
doc: "hooks empty string",
commandString: "image ls",
pluginConfig: map[string]string{
"hooks": "",
},
expectedMatch: "",
expectedOk: false,
},
{
doc: "hooks partial token no match",
commandString: "image inspect",
pluginConfig: map[string]string{
"hooks": "image i",
},
expectedMatch: "",
expectedOk: false,
},
{
doc: "hooks prefix token match",
commandString: "image inspect",
pluginConfig: map[string]string{
"hooks": "image",
},
expectedMatch: "image",
expectedOk: true,
},
{
doc: "error-hooks match on error",
commandString: "build",
pluginConfig: map[string]string{
"error-hooks": "build",
},
cmdErrorMessage: "exit status 1",
expectedMatch: "build",
expectedOk: true,
},
{
doc: "error-hooks no match on success",
commandString: "build",
pluginConfig: map[string]string{
"error-hooks": "build",
},
cmdErrorMessage: "",
expectedMatch: "",
expectedOk: false,
},
{
doc: "error-hooks prefix match on error",
commandString: "compose up",
pluginConfig: map[string]string{
"error-hooks": "compose",
},
cmdErrorMessage: "exit status 1",
expectedMatch: "compose",
expectedOk: true,
},
{
doc: "error-hooks no match for wrong command",
commandString: "pull",
pluginConfig: map[string]string{
"error-hooks": "build",
},
cmdErrorMessage: "exit status 1",
expectedMatch: "",
expectedOk: false,
},
{
doc: "hooks takes precedence over error-hooks",
commandString: "build",
pluginConfig: map[string]string{
"hooks": "build",
"error-hooks": "build",
},
cmdErrorMessage: "exit status 1",
expectedMatch: "build",
expectedOk: true,
},
{
doc: "hooks fires on success even with error-hooks configured",
commandString: "build",
pluginConfig: map[string]string{
"hooks": "build",
"error-hooks": "build",
},
cmdErrorMessage: "",
expectedMatch: "build",
expectedOk: true,
},
{
doc: "error-hooks with multiple commands",
commandString: "compose up",
pluginConfig: map[string]string{
"error-hooks": "build,compose up,pull",
},
cmdErrorMessage: "exit status 1",
expectedMatch: "compose up",
expectedOk: true,
},
}
for _, tc := range testCases {
t.Run(tc.doc, func(t *testing.T) {
match, ok := pluginMatch(tc.pluginConfig, tc.commandString, tc.cmdErrorMessage)
assert.Equal(t, ok, tc.expectedOk)
assert.Equal(t, match, tc.expectedMatch)
})
}
}
func TestMatchHookConfig(t *testing.T) {
testCases := []struct {
doc string
configuredHooks string
subCmd string
expectedMatch string
expectedOk bool
}{
{
doc: "empty config",
configuredHooks: "",
subCmd: "build",
expectedMatch: "",
expectedOk: false,
},
{
doc: "exact match",
configuredHooks: "build",
subCmd: "build",
expectedMatch: "build",
expectedOk: true,
},
{
doc: "prefix match",
configuredHooks: "image",
subCmd: "image ls",
expectedMatch: "image",
expectedOk: true,
},
{
doc: "comma-separated match",
configuredHooks: "pull,build,push",
subCmd: "build",
expectedMatch: "build",
expectedOk: true,
},
{
doc: "no match",
configuredHooks: "pull,push",
subCmd: "build",
expectedMatch: "",
expectedOk: false,
},
}
for _, tc := range testCases {
t.Run(tc.doc, func(t *testing.T) {
match, ok := matchHookConfig(tc.configuredHooks, tc.subCmd)
assert.Equal(t, ok, tc.expectedOk)
assert.Equal(t, match, tc.expectedMatch)
})
}
}
func TestRunPluginHooksPassesErrorMessage(t *testing.T) {
cfg := configfile.New("")
cfg.Plugins = map[string]map[string]string{
"test-plugin": {"hooks": "build"},
}
provider := &fakeConfigProvider{cfg: cfg}
root := &cobra.Command{Use: "docker"}
sub := &cobra.Command{Use: "build"}
root.AddCommand(sub)
// Should not panic with empty error message (success case)
RunPluginHooks(context.Background(), provider, root, sub, []string{"build"}, "")
// Should not panic with non-empty error message (failure case)
RunPluginHooks(context.Background(), provider, root, sub, []string{"build"}, "exit status 1")
}
func TestRunPluginHooksErrorHooks(t *testing.T) {
cfg := configfile.New("")
cfg.Plugins = map[string]map[string]string{
"test-plugin": {"error-hooks": "build"},
}
provider := &fakeConfigProvider{cfg: cfg}
root := &cobra.Command{Use: "docker"}
sub := &cobra.Command{Use: "build"}
root.AddCommand(sub)
// Should not panic — error-hooks with error message
RunPluginHooks(context.Background(), provider, root, sub, []string{"build"}, "exit status 1")
// Should not panic — error-hooks with no error (should be skipped)
RunPluginHooks(context.Background(), provider, root, sub, []string{"build"}, "")
}
func TestInvokeAndCollectHooksErrorHooksSkippedOnSuccess(t *testing.T) {
cfg := configfile.New("")
cfg.Plugins = map[string]map[string]string{
"nonexistent": {"error-hooks": "build"},
}
root := &cobra.Command{Use: "docker"}
sub := &cobra.Command{Use: "build"}
root.AddCommand(sub)
// On success, error-hooks should not match, so the plugin
// binary is never looked up and no results are returned.
result := invokeAndCollectHooks(
context.Background(), cfg, root,
"build", map[string]string{}, "",
)
assert.Check(t, is.Len(result, 0))
}
func TestInvokeAndCollectHooksNoPlugins(t *testing.T) {
cfg := configfile.New("")
root := &cobra.Command{Use: "docker"}
sub := &cobra.Command{Use: "build"}
root.AddCommand(sub)
result := invokeAndCollectHooks(
context.Background(), cfg, root,
"build", map[string]string{}, "some error",
)
assert.Check(t, is.Len(result, 0))
}
func TestInvokeAndCollectHooksCancelledContext(t *testing.T) {
cfg := configfile.New("")
cfg.Plugins = map[string]map[string]string{
"test-plugin": {"hooks": "build"},
}
root := &cobra.Command{Use: "docker"}
sub := &cobra.Command{Use: "build"}
root.AddCommand(sub)
ctx, cancel := context.WithCancel(context.Background())
cancel() // cancel immediately
result := invokeAndCollectHooks(
ctx, cfg, root,
"build", map[string]string{}, "exit status 1",
)
assert.Check(t, is.Nil(result))
}
func TestRunHooksMessages(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("test plugins require a POSIX shell")
}
const header = "\n\x1b[1mWhat's next:\x1b[0m\n"
for _, tc := range []struct {
doc string
responses map[string]string
expected string
}{
{
doc: "generic message preserves formatting and expands templates",
responses: map[string]string{
"status": `{"Type":1,"Template":"Status for {{command}}\n\n Details"}`,
},
expected: "\nStatus for build\n\n Details\n",
},
{
doc: "omitted type defaults to next steps",
responses: map[string]string{
"suggestion": `{"Template":"Try another command"}`,
},
expected: header + " Try another command\n",
},
{
doc: "generic messages precede shared next steps header",
responses: map[string]string{
"statusone": `{"Type":1,"Template":"Session active"}`,
"statustwo": `{"Type":1,"Template":"Session active"}`,
"suggestionone": `{"Type":0,"Template":"Try another command"}`,
"suggestiontwo": `{"Type":0,"Template":"Try another command"}`,
},
expected: "\nSession active\nSession active\n" + header + " Try another command\n Try another command\n",
},
{
doc: "empty responses produce no output",
responses: map[string]string{
"status": `{"Type":1,"Template":" \n\t"}`,
"suggestion": `{"Type":0,"Template":""}`,
},
},
{
doc: "empty generic message does not affect next steps",
responses: map[string]string{
"status": `{"Type":1,"Template":""}`,
"suggestion": `{"Type":0,"Template":"Try another command"}`,
},
expected: header + " Try another command\n",
},
{
doc: "invalid responses do not affect other plugins",
responses: map[string]string{
"unknown": `{"Type":99,"Template":"Ignored"}`,
"invalid": `{"Type":1,"Template":"{{"}`,
"status": `{"Type":1,"Template":"Session active"}`,
"suggestion": `{"Type":0,"Template":"Try another command"}`,
},
expected: "\nSession active\n" + header + " Try another command\n",
},
} {
t.Run(tc.doc, func(t *testing.T) {
pluginDir := t.TempDir()
cfg := configfile.New("")
cfg.CLIPluginsExtraDirs = []string{pluginDir}
cfg.Plugins = make(map[string]map[string]string)
for name, response := range tc.responses {
plugin := fmt.Sprintf(`#!/bin/sh
case "$1" in
docker-cli-plugin-metadata)
printf '%%s\n' '{"SchemaVersion":"0.1.0","Vendor":"test"}'
;;
*)
test "$2" = docker-cli-plugin-hooks || exit 1
printf 'called\n' >> "$0.calls"
printf '%%s\n' '%s'
;;
esac
`, response)
assert.NilError(t, os.WriteFile(filepath.Join(pluginDir, "docker-"+name), []byte(plugin), 0o755))
cfg.Plugins[name] = map[string]string{"hooks": "build"}
}
root := &cobra.Command{Use: "docker"}
sub := &cobra.Command{Use: "build"}
root.AddCommand(sub)
var output strings.Builder
sub.SetErr(&output)
runHooks(context.Background(), cfg, root, sub, "build", nil, "")
assert.Equal(t, output.String(), tc.expected)
for name := range tc.responses {
calls, err := os.ReadFile(filepath.Join(pluginDir, "docker-"+name+".calls"))
assert.NilError(t, err)
assert.Equal(t, string(calls), "called\n", "each plugin hook must run exactly once")
}
})
}
}
+88 -75
View File
@@ -1,112 +1,124 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.26
package manager package manager
import ( import (
"context" "context"
"errors"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
slices "slices" "sort"
"strings" "strings"
"sync" "sync"
"github.com/containerd/errdefs" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/debug"
"github.com/fvbommel/sortorder" "github.com/fvbommel/sortorder"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
// ReexecEnvvar is the name of an ennvar which is set to the command
// used to originally invoke the docker CLI when executing a
// plugin. Assuming $PATH and $CWD remain unchanged this should allow
// the plugin to re-execute the original CLI.
const ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND"
// errPluginNotFound is the error returned when a plugin could not be found. // errPluginNotFound is the error returned when a plugin could not be found.
type errPluginNotFound string type errPluginNotFound string
func (errPluginNotFound) NotFound() {} func (e errPluginNotFound) NotFound() {}
func (e errPluginNotFound) Error() string { func (e errPluginNotFound) Error() string {
return "Error: No such CLI plugin: " + string(e) return "Error: No such CLI plugin: " + string(e)
} }
// getPluginDirs returns the platform-specific locations to search for plugins type notFound interface{ NotFound() }
// in order of preference.
// // IsNotFound is true if the given error is due to a plugin not being found.
// Plugin-discovery is performed in the following order of preference: func IsNotFound(err error) bool {
// if e, ok := err.(*pluginError); ok {
// 1. The "cli-plugins" directory inside the CLIs [config.Path] (usually "~/.docker/cli-plugins"). err = e.Cause()
// 2. Additional plugin directories as configured through [ConfigFile.CLIPluginsExtraDirs]. }
// 3. Platform-specific defaultSystemPluginDirs. _, ok := err.(notFound)
// return ok
// [ConfigFile.CLIPluginsExtraDirs]: https://pkg.go.dev/github.com/docker/cli@v26.1.4+incompatible/cli/config/configfile#ConfigFile.CLIPluginsExtraDirs }
func getPluginDirs(cfg *configfile.ConfigFile) []string {
func getPluginDirs(cfg *configfile.ConfigFile) ([]string, error) {
var pluginDirs []string var pluginDirs []string
if cfg != nil { if cfg != nil {
pluginDirs = append(pluginDirs, cfg.CLIPluginsExtraDirs...) pluginDirs = append(pluginDirs, cfg.CLIPluginsExtraDirs...)
} }
pluginDir := filepath.Join(config.Dir(), "cli-plugins") pluginDir, err := config.Path("cli-plugins")
if err != nil {
return nil, err
}
pluginDirs = append(pluginDirs, pluginDir) pluginDirs = append(pluginDirs, pluginDir)
pluginDirs = append(pluginDirs, defaultSystemPluginDirs...) pluginDirs = append(pluginDirs, defaultSystemPluginDirs...)
return pluginDirs return pluginDirs, nil
} }
func addPluginCandidatesFromDir(res map[string][]string, d string) { func addPluginCandidatesFromDir(res map[string][]string, d string) error {
dentries, err := os.ReadDir(d) dentries, err := os.ReadDir(d)
// Silently ignore any directories which we cannot list (e.g. due to
// permissions or anything else) or which is not a directory
if err != nil { if err != nil {
return return err
} }
for _, dentry := range dentries { for _, dentry := range dentries {
switch mode := dentry.Type() & os.ModeType; mode { //nolint:exhaustive,nolintlint // no need to include all possible file-modes in this list switch dentry.Type() & os.ModeType {
case os.ModeSymlink: case 0, os.ModeSymlink:
if !debug.IsEnabled() { // Regular file or symlink, keep going
// Skip broken symlinks unless debug is enabled. With debug
// enabled, this will print a warning in "docker info".
if _, err := os.Stat(filepath.Join(d, dentry.Name())); errors.Is(err, os.ErrNotExist) {
continue
}
}
case 0:
// Regular file, keep going
default: default:
// Something else, ignore. // Something else, ignore.
continue continue
} }
name := dentry.Name() name := dentry.Name()
if !strings.HasPrefix(name, metadata.NamePrefix) { if !strings.HasPrefix(name, NamePrefix) {
continue continue
} }
name = strings.TrimPrefix(name, metadata.NamePrefix) name = strings.TrimPrefix(name, NamePrefix)
var err error var err error
if name, err = trimExeSuffix(name); err != nil { if name, err = trimExeSuffix(name); err != nil {
continue continue
} }
res[name] = append(res[name], filepath.Join(d, dentry.Name())) res[name] = append(res[name], filepath.Join(d, dentry.Name()))
} }
return nil
} }
// listPluginCandidates returns a map from plugin name to the list of (unvalidated) Candidates. The list is in descending order of priority. // listPluginCandidates returns a map from plugin name to the list of (unvalidated) Candidates. The list is in descending order of priority.
func listPluginCandidates(dirs []string) map[string][]string { func listPluginCandidates(dirs []string) (map[string][]string, error) {
result := make(map[string][]string) result := make(map[string][]string)
for _, d := range dirs { for _, d := range dirs {
addPluginCandidatesFromDir(result, d) // Silently ignore any directories which we cannot
// Stat (e.g. due to permissions or anything else) or
// which is not a directory.
if fi, err := os.Stat(d); err != nil || !fi.IsDir() {
continue
}
if err := addPluginCandidatesFromDir(result, d); err != nil {
// Silently ignore paths which don't exist.
if os.IsNotExist(err) {
continue
}
return nil, err // Or return partial result?
}
} }
return result return result, nil
} }
// GetPlugin returns a plugin on the system by its name // GetPlugin returns a plugin on the system by its name
func GetPlugin(name string, dockerCLI config.Provider, rootcmd *cobra.Command) (*Plugin, error) { func GetPlugin(name string, dockerCli command.Cli, rootcmd *cobra.Command) (*Plugin, error) {
pluginDirs := getPluginDirs(dockerCLI.ConfigFile()) pluginDirs, err := getPluginDirs(dockerCli.ConfigFile())
return getPlugin(name, pluginDirs, rootcmd) if err != nil {
} return nil, err
}
candidates, err := listPluginCandidates(pluginDirs)
if err != nil {
return nil, err
}
func getPlugin(name string, pluginDirs []string, rootcmd *cobra.Command) (*Plugin, error) {
candidates := listPluginCandidates(pluginDirs)
if paths, ok := candidates[name]; ok { if paths, ok := candidates[name]; ok {
if len(paths) == 0 { if len(paths) == 0 {
return nil, errPluginNotFound(name) return nil, errPluginNotFound(name)
@@ -116,7 +128,7 @@ func getPlugin(name string, pluginDirs []string, rootcmd *cobra.Command) (*Plugi
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !errdefs.IsNotFound(p.Err) { if !IsNotFound(p.Err) {
p.ShadowedPaths = paths[1:] p.ShadowedPaths = paths[1:]
} }
return &p, nil return &p, nil
@@ -126,21 +138,20 @@ func getPlugin(name string, pluginDirs []string, rootcmd *cobra.Command) (*Plugi
} }
// ListPlugins produces a list of the plugins available on the system // ListPlugins produces a list of the plugins available on the system
func ListPlugins(dockerCli config.Provider, rootcmd *cobra.Command) ([]Plugin, error) { func ListPlugins(dockerCli command.Cli, rootcmd *cobra.Command) ([]Plugin, error) {
pluginDirs := getPluginDirs(dockerCli.ConfigFile()) pluginDirs, err := getPluginDirs(dockerCli.ConfigFile())
candidates := listPluginCandidates(pluginDirs) if err != nil {
if len(candidates) == 0 { return nil, err
return nil, nil }
candidates, err := listPluginCandidates(pluginDirs)
if err != nil {
return nil, err
} }
var plugins []Plugin var plugins []Plugin
var mu sync.Mutex var mu sync.Mutex
ctx := rootcmd.Context() eg, _ := errgroup.WithContext(context.TODO())
if ctx == nil {
// Fallback, mostly for tests that pass a bare cobra.command
ctx = context.Background()
}
eg, _ := errgroup.WithContext(ctx)
cmds := rootcmd.Commands() cmds := rootcmd.Commands()
for _, paths := range candidates { for _, paths := range candidates {
func(paths []string) { func(paths []string) {
@@ -153,7 +164,7 @@ func ListPlugins(dockerCli config.Provider, rootcmd *cobra.Command) ([]Plugin, e
if err != nil { if err != nil {
return err return err
} }
if !errdefs.IsNotFound(p.Err) { if !IsNotFound(p.Err) {
p.ShadowedPaths = paths[1:] p.ShadowedPaths = paths[1:]
mu.Lock() mu.Lock()
defer mu.Unlock() defer mu.Unlock()
@@ -167,28 +178,31 @@ func ListPlugins(dockerCli config.Provider, rootcmd *cobra.Command) ([]Plugin, e
return nil, err return nil, err
} }
slices.SortFunc(plugins, func(a, b Plugin) int { sort.Slice(plugins, func(i, j int) bool {
return sortorder.NaturalCompare(a.Name, b.Name) return sortorder.NaturalLess(plugins[i].Name, plugins[j].Name)
}) })
return plugins, nil return plugins, nil
} }
// PluginRunCommand returns an [os/exec.Cmd] which when [os/exec.Cmd.Run] will execute the named plugin. // PluginRunCommand returns an "os/exec".Cmd which when .Run() will execute the named plugin.
// The rootcmd argument is referenced to determine the set of builtin commands in order to detect conflicts. // The rootcmd argument is referenced to determine the set of builtin commands in order to detect conficts.
// The error returned satisfies the [errdefs.IsNotFound] predicate if no plugin was found or if the first candidate plugin was invalid somehow. // The error returned satisfies the IsNotFound() predicate if no plugin was found or if the first candidate plugin was invalid somehow.
func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Command) (*exec.Cmd, error) { func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command) (*exec.Cmd, error) {
// This uses the full original args, not the args which may // This uses the full original args, not the args which may
// have been provided by cobra to our caller. This is because // have been provided by cobra to our caller. This is because
// they lack e.g. global options which we must propagate here. // they lack e.g. global options which we must propagate here.
args := os.Args[1:] args := os.Args[1:]
if !isValidPluginName(name) { if !pluginNameRe.MatchString(name) {
// We treat this as "not found" so that callers will // We treat this as "not found" so that callers will
// fallback to their "invalid" command path. // fallback to their "invalid" command path.
return nil, errPluginNotFound(name) return nil, errPluginNotFound(name)
} }
exename := addExeSuffix(metadata.NamePrefix + name) exename := addExeSuffix(NamePrefix + name)
pluginDirs := getPluginDirs(dockerCli.ConfigFile()) pluginDirs, err := getPluginDirs(dockerCli.ConfigFile())
if err != nil {
return nil, err
}
for _, d := range pluginDirs { for _, d := range pluginDirs {
path := filepath.Join(d, exename) path := filepath.Join(d, exename)
@@ -210,8 +224,7 @@ func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Com
// TODO: why are we not returning plugin.Err? // TODO: why are we not returning plugin.Err?
return nil, errPluginNotFound(name) return nil, errPluginNotFound(name)
} }
cmd := exec.Command(plugin.Path, args...) // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments" cmd := exec.Command(plugin.Path, args...)
// Using dockerCli.{In,Out,Err}() here results in a hang until something is input. // Using dockerCli.{In,Out,Err}() here results in a hang until something is input.
// See: - https://github.com/golang/go/issues/10338 // See: - https://github.com/golang/go/issues/10338
// - https://github.com/golang/go/commit/d000e8742a173aa0659584aa01b7ba2834ba28ab // - https://github.com/golang/go/commit/d000e8742a173aa0659584aa01b7ba2834ba28ab
@@ -221,8 +234,8 @@ func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Com
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
cmd.Env = append(cmd.Environ(), metadata.ReexecEnvvar+"="+os.Args[0]) cmd.Env = os.Environ()
cmd.Env = appendPluginResourceAttributesEnvvar(cmd.Env, rootcmd, plugin) cmd.Env = append(cmd.Env, ReexecEnvvar+"="+os.Args[0])
return cmd, nil return cmd, nil
} }
@@ -231,5 +244,5 @@ func PluginRunCommand(dockerCli config.Provider, name string, rootcmd *cobra.Com
// IsPluginCommand checks if the given cmd is a plugin-stub. // IsPluginCommand checks if the given cmd is a plugin-stub.
func IsPluginCommand(cmd *cobra.Command) bool { func IsPluginCommand(cmd *cobra.Command) bool {
return cmd.Annotations[metadata.CommandAnnotationPlugin] == "true" return cmd.Annotations[CommandAnnotationPlugin] == "true"
} }
+19 -41
View File
@@ -1,11 +1,9 @@
package manager package manager
import ( import (
"path/filepath"
"strings" "strings"
"testing" "testing"
"github.com/containerd/errdefs"
"github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
@@ -38,7 +36,7 @@ func TestListPluginCandidates(t *testing.T) {
"plugins3-target", // Will be referenced as a symlink from below "plugins3-target", // Will be referenced as a symlink from below
fs.WithFile("docker-plugin1", ""), fs.WithFile("docker-plugin1", ""),
fs.WithDir("ignored3"), fs.WithDir("ignored3"),
fs.WithSymlink("docker-brokensymlink", "broken"), // A broken symlink is ignored fs.WithSymlink("docker-brokensymlink", "broken"), // A broken symlink is still a candidate (but would fail tests later)
fs.WithFile("non-plugin-symlinked", ""), // This shouldn't appear, but ... fs.WithFile("non-plugin-symlinked", ""), // This shouldn't appear, but ...
fs.WithSymlink("docker-symlinked", "non-plugin-symlinked"), // ... this link to it should. fs.WithSymlink("docker-symlinked", "non-plugin-symlinked"), // ... this link to it should.
), ),
@@ -53,7 +51,8 @@ func TestListPluginCandidates(t *testing.T) {
dirs = append(dirs, dir.Join(d)) dirs = append(dirs, dir.Join(d))
} }
candidates := listPluginCandidates(dirs) candidates, err := listPluginCandidates(dirs)
assert.NilError(t, err)
exp := map[string][]string{ exp := map[string][]string{
"plugin1": { "plugin1": {
dir.Join("plugins1", "docker-plugin1"), dir.Join("plugins1", "docker-plugin1"),
@@ -72,6 +71,9 @@ func TestListPluginCandidates(t *testing.T) {
"hardlink2": { "hardlink2": {
dir.Join("plugins2", "docker-hardlink2"), dir.Join("plugins2", "docker-hardlink2"),
}, },
"brokensymlink": {
dir.Join("plugins3", "docker-brokensymlink"),
},
"symlinked": { "symlinked": {
dir.Join("plugins3", "docker-symlinked"), dir.Join("plugins3", "docker-symlinked"),
}, },
@@ -80,35 +82,6 @@ func TestListPluginCandidates(t *testing.T) {
assert.DeepEqual(t, candidates, exp) assert.DeepEqual(t, candidates, exp)
} }
func TestListPluginCandidatesEmpty(t *testing.T) {
tmpDir := t.TempDir()
candidates := listPluginCandidates([]string{tmpDir, filepath.Join(tmpDir, "no-such-dir")})
assert.Assert(t, len(candidates) == 0)
}
// Regression test for https://github.com/docker/cli/issues/5643.
// Check that inaccessible directories that come before accessible ones are ignored
// and do not prevent the latter from being processed.
func TestListPluginCandidatesInaccessibleDir(t *testing.T) {
dir := fs.NewDir(t, t.Name(),
fs.WithDir("no-perm", fs.WithMode(0)),
fs.WithDir("plugins",
fs.WithFile("docker-buildx", ""),
),
)
defer dir.Remove()
candidates := listPluginCandidates([]string{
dir.Join("no-perm"),
dir.Join("plugins"),
})
assert.DeepEqual(t, candidates, map[string][]string{
"buildx": {
dir.Join("plugins", "docker-buildx"),
},
})
}
func TestGetPlugin(t *testing.T) { func TestGetPlugin(t *testing.T) {
dir := fs.NewDir(t, t.Name(), dir := fs.NewDir(t, t.Name(),
fs.WithFile("docker-bbb", ` fs.WithFile("docker-bbb", `
@@ -129,7 +102,7 @@ echo '{"SchemaVersion":"0.1.0"}'`, fs.WithMode(0o777)),
_, err = GetPlugin("ccc", cli, &cobra.Command{}) _, err = GetPlugin("ccc", cli, &cobra.Command{})
assert.Error(t, err, "Error: No such CLI plugin: ccc") assert.Error(t, err, "Error: No such CLI plugin: ccc")
assert.Assert(t, errdefs.IsNotFound(err)) assert.Assert(t, IsNotFound(err))
} }
func TestListPluginsIsSorted(t *testing.T) { func TestListPluginsIsSorted(t *testing.T) {
@@ -164,25 +137,30 @@ func TestErrPluginNotFound(t *testing.T) {
var err error = errPluginNotFound("test") var err error = errPluginNotFound("test")
err.(errPluginNotFound).NotFound() err.(errPluginNotFound).NotFound()
assert.Error(t, err, "Error: No such CLI plugin: test") assert.Error(t, err, "Error: No such CLI plugin: test")
assert.Assert(t, errdefs.IsNotFound(err)) assert.Assert(t, IsNotFound(err))
assert.Assert(t, !errdefs.IsNotFound(nil)) assert.Assert(t, !IsNotFound(nil))
} }
func TestGetPluginDirs(t *testing.T) { func TestGetPluginDirs(t *testing.T) {
cli := test.NewFakeCli(nil) cli := test.NewFakeCli(nil)
pluginDir := filepath.Join(config.Dir(), "cli-plugins") pluginDir, err := config.Path("cli-plugins")
assert.NilError(t, err)
expected := append([]string{pluginDir}, defaultSystemPluginDirs...) expected := append([]string{pluginDir}, defaultSystemPluginDirs...)
pluginDirs := getPluginDirs(cli.ConfigFile()) var pluginDirs []string
pluginDirs, err = getPluginDirs(cli.ConfigFile())
assert.Equal(t, strings.Join(expected, ":"), strings.Join(pluginDirs, ":")) assert.Equal(t, strings.Join(expected, ":"), strings.Join(pluginDirs, ":"))
assert.NilError(t, err)
extras := make([]string, 0, 3+len(expected)) extras := []string{
extras = append(extras, "foo", "bar", "baz") "foo", "bar", "baz",
}
expected = append(extras, expected...) expected = append(extras, expected...)
cli.SetConfigFile(&configfile.ConfigFile{ cli.SetConfigFile(&configfile.ConfigFile{
CLIPluginsExtraDirs: extras, CLIPluginsExtraDirs: extras,
}) })
pluginDirs = getPluginDirs(cli.ConfigFile()) pluginDirs, err = getPluginDirs(cli.ConfigFile())
assert.DeepEqual(t, expected, pluginDirs) assert.DeepEqual(t, expected, pluginDirs)
assert.NilError(t, err)
} }
+2 -14
View File
@@ -2,19 +2,7 @@
package manager package manager
// defaultSystemPluginDirs are the platform-specific locations to search
// for plugins in order of preference.
//
// Plugin-discovery is performed in the following order of preference:
//
// 1. The "cli-plugins" directory inside the CLIs config-directory (usually "~/.docker/cli-plugins").
// 2. Additional plugin directories as configured through [ConfigFile.CLIPluginsExtraDirs].
// 3. Platform-specific defaultSystemPluginDirs (as defined below).
//
// [ConfigFile.CLIPluginsExtraDirs]: https://pkg.go.dev/github.com/docker/cli@v26.1.4+incompatible/cli/config/configfile#ConfigFile.CLIPluginsExtraDirs
var defaultSystemPluginDirs = []string{ var defaultSystemPluginDirs = []string{
"/usr/local/lib/docker/cli-plugins", "/usr/local/lib/docker/cli-plugins", "/usr/local/libexec/docker/cli-plugins",
"/usr/local/libexec/docker/cli-plugins", "/usr/lib/docker/cli-plugins", "/usr/libexec/docker/cli-plugins",
"/usr/lib/docker/cli-plugins",
"/usr/libexec/docker/cli-plugins",
} }
+1 -10
View File
@@ -5,16 +5,7 @@ import (
"path/filepath" "path/filepath"
) )
// defaultSystemPluginDirs are the platform-specific locations to search
// for plugins in order of preference.
//
// Plugin-discovery is performed in the following order of preference:
//
// 1. The "cli-plugins" directory inside the CLIs config-directory (usually "~/.docker/cli-plugins").
// 2. Additional plugin directories as configured through [ConfigFile.CLIPluginsExtraDirs].
// 3. Platform-specific defaultSystemPluginDirs (as defined below).
//
// [ConfigFile.CLIPluginsExtraDirs]: https://pkg.go.dev/github.com/docker/cli@v26.1.4+incompatible/cli/config/configfile#ConfigFile.CLIPluginsExtraDirs
var defaultSystemPluginDirs = []string{ var defaultSystemPluginDirs = []string{
filepath.Join(os.Getenv("ProgramData"), "Docker", "cli-plugins"),
filepath.Join(os.Getenv("ProgramFiles"), "Docker", "cli-plugins"), filepath.Join(os.Getenv("ProgramFiles"), "Docker", "cli-plugins"),
} }
+25
View File
@@ -0,0 +1,25 @@
package manager
const (
// NamePrefix is the prefix required on all plugin binary names
NamePrefix = "docker-"
// MetadataSubcommandName is the name of the plugin subcommand
// which must be supported by every plugin and returns the
// plugin metadata.
MetadataSubcommandName = "docker-cli-plugin-metadata"
)
// Metadata provided by the plugin.
type Metadata struct {
// SchemaVersion describes the version of this struct. Mandatory, must be "0.1.0"
SchemaVersion string `json:",omitempty"`
// Vendor is the name of the plugin vendor. Mandatory
Vendor string `json:",omitempty"`
// Version is the optional version of this plugin.
Version string `json:",omitempty"`
// ShortDescription should be suitable for a single line help message.
ShortDescription string `json:",omitempty"`
// URL is a pointer to the plugin's homepage.
URL string `json:",omitempty"`
}
+18 -118
View File
@@ -1,28 +1,20 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.26
package manager package manager
import ( import (
"context"
"encoding"
"encoding/json" "encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath" "path/filepath"
"strconv" "regexp"
"strings" "strings"
"github.com/docker/cli/cli-plugins/hooks" "github.com/pkg/errors"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var pluginNameRe = regexp.MustCompile("^[a-z][a-z0-9]*$")
// Plugin represents a potential plugin with all it's metadata. // Plugin represents a potential plugin with all it's metadata.
type Plugin struct { type Plugin struct {
metadata.Metadata Metadata
Name string `json:",omitempty"` Name string `json:",omitempty"`
Path string `json:",omitempty"` Path string `json:",omitempty"`
@@ -34,34 +26,12 @@ type Plugin struct {
ShadowedPaths []string `json:",omitempty"` ShadowedPaths []string `json:",omitempty"`
} }
// MarshalJSON implements [json.Marshaler] to handle marshaling the
// [Plugin.Err] field (Go doesn't marshal errors by default).
func (p *Plugin) MarshalJSON() ([]byte, error) {
type Alias Plugin // avoid recursion
cp := *p // shallow copy to avoid mutating original
if cp.Err != nil {
if _, ok := cp.Err.(encoding.TextMarshaler); !ok {
cp.Err = &pluginError{cp.Err}
}
}
return json.Marshal((*Alias)(&cp))
}
// pluginCandidate represents a possible plugin candidate, for mocking purposes.
type pluginCandidate interface {
Path() string
Metadata() ([]byte, error)
}
// newPlugin determines if the given candidate is valid and returns a // newPlugin determines if the given candidate is valid and returns a
// Plugin. If the candidate fails one of the tests then `Plugin.Err` // Plugin. If the candidate fails one of the tests then `Plugin.Err`
// is set, and is always a `pluginError`, but the `Plugin` is still // is set, and is always a `pluginError`, but the `Plugin` is still
// returned with no error. An error is only returned due to a // returned with no error. An error is only returned due to a
// non-recoverable error. // non-recoverable error.
func newPlugin(c pluginCandidate, cmds []*cobra.Command) (Plugin, error) { func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) {
path := c.Path() path := c.Path()
if path == "" { if path == "" {
return Plugin{}, errors.New("plugin candidate path cannot be empty") return Plugin{}, errors.New("plugin candidate path cannot be empty")
@@ -71,24 +41,24 @@ func newPlugin(c pluginCandidate, cmds []*cobra.Command) (Plugin, error) {
// which would fail here, so there are all real errors. // which would fail here, so there are all real errors.
fullname := filepath.Base(path) fullname := filepath.Base(path)
if fullname == "." { if fullname == "." {
return Plugin{}, fmt.Errorf("unable to determine basename of plugin candidate %q", path) return Plugin{}, errors.Errorf("unable to determine basename of plugin candidate %q", path)
} }
var err error var err error
if fullname, err = trimExeSuffix(fullname); err != nil { if fullname, err = trimExeSuffix(fullname); err != nil {
return Plugin{}, fmt.Errorf("plugin candidate %q: %w", path, err) return Plugin{}, errors.Wrapf(err, "plugin candidate %q", path)
} }
if !strings.HasPrefix(fullname, metadata.NamePrefix) { if !strings.HasPrefix(fullname, NamePrefix) {
return Plugin{}, fmt.Errorf("plugin candidate %q: does not have %q prefix", path, metadata.NamePrefix) return Plugin{}, errors.Errorf("plugin candidate %q: does not have %q prefix", path, NamePrefix)
} }
p := Plugin{ p := Plugin{
Name: strings.TrimPrefix(fullname, metadata.NamePrefix), Name: strings.TrimPrefix(fullname, NamePrefix),
Path: path, Path: path,
} }
// Now apply the candidate tests, so these update p.Err. // Now apply the candidate tests, so these update p.Err.
if !isValidPluginName(p.Name) { if !pluginNameRe.MatchString(p.Name) {
p.Err = newPluginError("plugin candidate %q did not match %q", p.Name, pluginNameFormat) p.Err = NewPluginError("plugin candidate %q did not match %q", p.Name, pluginNameRe.String())
return p, nil return p, nil
} }
@@ -100,11 +70,11 @@ func newPlugin(c pluginCandidate, cmds []*cobra.Command) (Plugin, error) {
continue continue
} }
if cmd.Name() == p.Name { if cmd.Name() == p.Name {
p.Err = newPluginError("plugin %q duplicates builtin command", p.Name) p.Err = NewPluginError("plugin %q duplicates builtin command", p.Name)
return p, nil return p, nil
} }
if cmd.HasAlias(p.Name) { if cmd.HasAlias(p.Name) {
p.Err = newPluginError("plugin %q duplicates an alias of builtin command %q", p.Name, cmd.Name()) p.Err = NewPluginError("plugin %q duplicates an alias of builtin command %q", p.Name, cmd.Name())
return p, nil return p, nil
} }
} }
@@ -120,83 +90,13 @@ func newPlugin(c pluginCandidate, cmds []*cobra.Command) (Plugin, error) {
p.Err = wrapAsPluginError(err, "invalid metadata") p.Err = wrapAsPluginError(err, "invalid metadata")
return p, nil return p, nil
} }
if err := validateSchemaVersion(p.Metadata.SchemaVersion); err != nil { if p.Metadata.SchemaVersion != "0.1.0" {
p.Err = &pluginError{cause: err} p.Err = NewPluginError("plugin SchemaVersion %q is not valid, must be 0.1.0", p.Metadata.SchemaVersion)
return p, nil return p, nil
} }
if p.Metadata.Vendor == "" { if p.Metadata.Vendor == "" {
p.Err = newPluginError("plugin metadata does not define a vendor") p.Err = NewPluginError("plugin metadata does not define a vendor")
return p, nil return p, nil
} }
return p, nil return p, nil
} }
// validateSchemaVersion validates if the plugin's schemaVersion is supported.
//
// The current schema-version is "0.1.0", but we don't want to break compatibility
// until v2.0.0 of the schema version. Check for the major version to be < 2.0.0.
//
// Note that CLI versions before 28.4.1 may not support these versions as they were
// hard-coded to only accept "0.1.0".
func validateSchemaVersion(version string) error {
if version == "0.1.0" {
return nil
}
if version == "" {
return errors.New("plugin SchemaVersion version cannot be empty")
}
major, _, ok := strings.Cut(version, ".")
majorVersion, err := strconv.Atoi(major)
if !ok || err != nil {
return fmt.Errorf("plugin SchemaVersion %q has wrong format: must be <major>.<minor>.<patch>", version)
}
if majorVersion > 1 {
return fmt.Errorf("plugin SchemaVersion %q is not supported: must be lower than 2.0.0", version)
}
return nil
}
// RunHook executes the plugin's hooks command
// and returns its unprocessed output.
func (p *Plugin) RunHook(ctx context.Context, hookData hooks.Request) ([]byte, error) {
hDataBytes, err := json.Marshal(hookData)
if err != nil {
return nil, wrapAsPluginError(err, "failed to marshall hook data")
}
pCmd := exec.CommandContext(ctx, p.Path, p.Name, metadata.HookSubcommandName, string(hDataBytes)) // #nosec G204 -- ignore "Subprocess launched with a potential tainted input or cmd arguments"
pCmd.Env = os.Environ()
pCmd.Env = append(pCmd.Env, metadata.ReexecEnvvar+"="+os.Args[0])
out, err := pCmd.Output()
if err != nil {
if _, ok := errors.AsType[*exec.ExitError](err); ok {
return nil, wrapAsPluginError(err, "plugin hook subcommand exited unsuccessfully")
}
return nil, wrapAsPluginError(err, "failed to execute plugin hook subcommand: "+pCmd.String())
}
return out, nil
}
// pluginNameFormat is used as part of errors for invalid plugin-names.
// We should consider making this less technical ("must start with "a-z",
// and only consist of lowercase alphanumeric characters").
const pluginNameFormat = `^[a-z][a-z0-9]*$`
func isValidPluginName(s string) bool {
if len(s) == 0 {
return false
}
// first character must be a-z
if c := s[0]; c < 'a' || c > 'z' {
return false
}
// followed by a-z or 0-9
for i := 1; i < len(s); i++ {
c := s[i]
if (c < 'a' || c > 'z') && (c < '0' || c > '9') {
return false
}
}
return true
}
-43
View File
@@ -1,43 +0,0 @@
package manager
import (
"encoding/json"
"errors"
"testing"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestPluginMarshal(t *testing.T) {
const jsonWithError = `{"Name":"some-plugin","Err":"something went wrong"}`
const jsonNoError = `{"Name":"some-plugin"}`
tests := []struct {
doc string
error error
expected string
}{
{
doc: "no error",
expected: jsonNoError,
},
{
doc: "regular error",
error: errors.New("something went wrong"),
expected: jsonWithError,
},
{
doc: "custom error",
error: newPluginError("something went wrong"),
expected: jsonWithError,
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
actual, err := json.Marshal(&Plugin{Name: "some-plugin", Err: tc.error})
assert.NilError(t, err)
assert.Check(t, is.Equal(string(actual), tc.expected))
})
}
}
+10 -4
View File
@@ -1,16 +1,22 @@
package manager package manager
import ( import (
"fmt"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/pkg/errors"
) )
// This is made slightly more complex due to needing to be case-insensitive. // This is made slightly more complex due to needing to be case insensitive.
func trimExeSuffix(s string) (string, error) { func trimExeSuffix(s string) (string, error) {
ext := filepath.Ext(s) ext := filepath.Ext(s)
if ext == "" || !strings.EqualFold(ext, ".exe") { if ext == "" {
return "", fmt.Errorf("path %q lacks required file extension (.exe)", s) return "", errors.Errorf("path %q lacks required file extension", s)
}
exe := ".exe"
if !strings.EqualFold(ext, exe) {
return "", errors.Errorf("path %q lacks required %q suffix", s, exe)
} }
return strings.TrimSuffix(s, ext), nil return strings.TrimSuffix(s, ext), nil
} }
-85
View File
@@ -1,85 +0,0 @@
package manager
import (
"fmt"
"os"
"strings"
"github.com/docker/cli/cli-plugins/metadata"
"github.com/spf13/cobra"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/baggage"
)
const (
// resourceAttributesEnvVar is the name of the envvar that includes additional
// resource attributes for OTEL as defined in the [OpenTelemetry specification].
//
// [OpenTelemetry specification]: https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#general-sdk-configuration
resourceAttributesEnvVar = "OTEL_RESOURCE_ATTRIBUTES"
// dockerCLIAttributePrefix is the prefix for any docker cli OTEL attributes.
//
// It is a copy of the const defined in [command.dockerCLIAttributePrefix].
dockerCLIAttributePrefix = "docker.cli."
cobraCommandPath = attribute.Key("cobra.command_path")
)
func getPluginResourceAttributes(cmd *cobra.Command, plugin Plugin) attribute.Set {
commandPath := cmd.Annotations[metadata.CommandAnnotationPluginCommandPath]
if commandPath == "" {
commandPath = fmt.Sprintf("%s %s", cmd.CommandPath(), plugin.Name)
}
attrSet := attribute.NewSet(
cobraCommandPath.String(commandPath),
)
kvs := make([]attribute.KeyValue, 0, attrSet.Len())
for iter := attrSet.Iter(); iter.Next(); {
attr := iter.Attribute()
kvs = append(kvs, attribute.KeyValue{
Key: dockerCLIAttributePrefix + attr.Key,
Value: attr.Value,
})
}
return attribute.NewSet(kvs...)
}
func appendPluginResourceAttributesEnvvar(env []string, cmd *cobra.Command, plugin Plugin) []string {
if attrs := getPluginResourceAttributes(cmd, plugin); attrs.Len() > 0 {
// Construct baggage members for each of the attributes.
// Ignore any failures as these aren't significant and
// represent an internal issue.
members := make([]baggage.Member, 0, attrs.Len())
for iter := attrs.Iter(); iter.Next(); {
attr := iter.Attribute()
m, err := baggage.NewMemberRaw(string(attr.Key), attr.Value.AsString())
if err != nil {
otel.Handle(err)
continue
}
members = append(members, m)
}
// Combine plugin added resource attributes with ones found in the environment
// variable. Our own attributes should be namespaced so there shouldn't be a
// conflict. We do not parse the environment variable because we do not want
// to handle errors in user configuration.
attrsSlice := make([]string, 0, 2)
if v := strings.TrimSpace(os.Getenv(resourceAttributesEnvVar)); v != "" {
attrsSlice = append(attrsSlice, v)
}
if b, err := baggage.New(members...); err != nil {
otel.Handle(err)
} else if b.Len() > 0 {
attrsSlice = append(attrsSlice, b.String())
}
if len(attrsSlice) > 0 {
env = append(env, resourceAttributesEnvVar+"="+strings.Join(attrsSlice, ","))
}
}
return env
}
-28
View File
@@ -1,28 +0,0 @@
package metadata
const (
// CommandAnnotationPlugin is added to every stub command added by
// AddPluginCommandStubs with the value "true" and so can be
// used to distinguish plugin stubs from regular commands.
CommandAnnotationPlugin = "com.docker.cli.plugin"
// CommandAnnotationPluginVendor is added to every stub command
// added by AddPluginCommandStubs and contains the vendor of
// that plugin.
CommandAnnotationPluginVendor = "com.docker.cli.plugin.vendor"
// CommandAnnotationPluginVersion is added to every stub command
// added by AddPluginCommandStubs and contains the version of
// that plugin.
CommandAnnotationPluginVersion = "com.docker.cli.plugin.version"
// CommandAnnotationPluginInvalid is added to any stub command
// added by AddPluginCommandStubs for an invalid command (that
// is, one which failed it's candidate test) and contains the
// reason for the failure.
CommandAnnotationPluginInvalid = "com.docker.cli.plugin-invalid"
// CommandAnnotationPluginCommandPath is added to overwrite the
// command path for a plugin invocation.
CommandAnnotationPluginCommandPath = "com.docker.cli.plugin.command_path"
)
-44
View File
@@ -1,44 +0,0 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.26
package metadata
const (
// NamePrefix is the prefix required on all plugin binary names
NamePrefix = "docker-"
// MetadataSubcommandName is the name of the plugin subcommand
// which must be supported by every plugin and returns the
// plugin metadata.
MetadataSubcommandName = "docker-cli-plugin-metadata"
// HookSubcommandName is the name of the plugin subcommand
// which must be implemented by plugins declaring support
// for hooks in their metadata.
HookSubcommandName = "docker-cli-plugin-hooks"
// ReexecEnvvar is the name of an ennvar which is set to the command
// used to originally invoke the docker CLI when executing a
// plugin. Assuming $PATH and $CWD remain unchanged this should allow
// the plugin to re-execute the original CLI.
ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND"
)
// Metadata provided by the plugin.
type Metadata struct {
// SchemaVersion describes the version of this struct. Mandatory, must be "0.1.0"
SchemaVersion string `json:",omitempty"`
// Vendor is the name of the plugin vendor. Mandatory
Vendor string `json:",omitempty"`
// Version is the optional version of this plugin.
Version string `json:",omitempty"`
// ShortDescription should be suitable for a single line help message.
ShortDescription string `json:",omitempty"`
// URL is a pointer to the plugin's homepage.
URL string `json:",omitempty"`
// Hidden hides the plugin in completion and help message output.
Hidden bool `json:",omitempty"`
// Features declares optional contracts supported by the plugin, keyed by
// feature name.
Features map[string]any `json:",omitempty"`
}
+42 -86
View File
@@ -1,46 +1,46 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.26
package plugin package plugin
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"os" "os"
"sync" "sync"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli-plugins/metadata" "github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli-plugins/socket" "github.com/docker/cli/cli-plugins/socket"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/connhelper" "github.com/docker/cli/cli/connhelper"
"github.com/docker/cli/cli/debug" "github.com/docker/docker/client"
"github.com/moby/moby/client"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"go.opentelemetry.io/otel"
) )
// PersistentPreRunE must be called by any plugin command (or // PersistentPreRunE must be called by any plugin command (or
// subcommand) which uses the cobra `PersistentPreRun*` hook. Plugins // subcommand) which uses the cobra `PersistentPreRun*` hook. Plugins
// which do not make use of `PersistentPreRun*` do not need to call // which do not make use of `PersistentPreRun*` do not need to call
// this (although it remains safe to do so). Plugins are recommended // this (although it remains safe to do so). Plugins are recommended
// to use `PersistentPreRunE` to enable the error to be // to use `PersistenPreRunE` to enable the error to be
// returned. Should not be called outside of a command's // returned. Should not be called outside of a command's
// PersistentPreRunE hook and must not be run unless Run has been // PersistentPreRunE hook and must not be run unless Run has been
// called. // called.
var PersistentPreRunE func(*cobra.Command, []string) error var PersistentPreRunE func(*cobra.Command, []string) error
// RunPlugin executes the specified plugin command // RunPlugin executes the specified plugin command
func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadata.Metadata) error { func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) error {
tcmd := newPluginCommand(dockerCli, plugin, meta) tcmd := newPluginCommand(dockerCli, plugin, meta)
var persistentPreRunOnce sync.Once var persistentPreRunOnce sync.Once
PersistentPreRunE = func(cmd *cobra.Command, _ []string) error { PersistentPreRunE = func(cmd *cobra.Command, _ []string) error {
var retErr error var err error
persistentPreRunOnce.Do(func() { persistentPreRunOnce.Do(func() {
ctx, cancel := context.WithCancel(cmd.Context()) cmdContext := cmd.Context()
// TODO: revisit and make sure this check makes sense
// see: https://github.com/docker/cli/pull/4599#discussion_r1422487271
if cmdContext == nil {
cmdContext = context.TODO()
}
ctx, cancel := context.WithCancel(cmdContext)
cmd.SetContext(ctx) cmd.SetContext(ctx)
// Set up the context to cancel based on signalling via CLI socket. // Set up the context to cancel based on signalling via CLI socket.
socket.ConnectAndWait(cancel) socket.ConnectAndWait(cancel)
@@ -49,28 +49,9 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadat
if os.Getenv("DOCKER_CLI_PLUGIN_USE_DIAL_STDIO") != "" { if os.Getenv("DOCKER_CLI_PLUGIN_USE_DIAL_STDIO") != "" {
opts = append(opts, withPluginClientConn(plugin.Name())) opts = append(opts, withPluginClientConn(plugin.Name()))
} }
opts = append(opts, command.WithEnableGlobalMeterProvider(), command.WithEnableGlobalTracerProvider()) err = tcmd.Initialize(opts...)
retErr = tcmd.Initialize(opts...)
ogRunE := cmd.RunE
if ogRunE == nil {
ogRun := cmd.Run
// necessary because error will always be nil here
// see: https://github.com/golangci/golangci-lint/issues/1379
//nolint:unparam
ogRunE = func(cmd *cobra.Command, args []string) error {
ogRun(cmd, args)
return nil
}
cmd.Run = nil
}
cmd.RunE = func(cmd *cobra.Command, args []string) error {
stopInstrumentation := dockerCli.StartInstrumentation(cmd)
err := ogRunE(cmd, args)
stopInstrumentation(err)
return err
}
}) })
return retErr return err
} }
cmd, args, err := tcmd.HandleGlobalFlags() cmd, args, err := tcmd.HandleGlobalFlags()
@@ -83,41 +64,37 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadat
return cmd.Execute() return cmd.Execute()
} }
// Run is the top-level entry point to the CLI plugin framework. It should // Run is the top-level entry point to the CLI plugin framework. It should be called from your plugin's `main()` function.
// be called from the plugin's "main()" function. It initializes a new func Run(makeCmd func(command.Cli) *cobra.Command, meta manager.Metadata) {
// [command.DockerCli] instance with the given options before calling dockerCli, err := command.NewDockerCli()
// makeCmd to construct the plugin command, then invokes the plugin command
// using [RunPlugin].
func Run(makeCmd func(command.Cli) *cobra.Command, meta metadata.Metadata, ops ...command.CLIOption) {
otel.SetErrorHandler(debug.OTELErrorHandler)
dockerCLI, err := command.NewDockerCli(ops...)
if err != nil { if err != nil {
_, _ = fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, err)
os.Exit(1) os.Exit(1)
} }
plugin := makeCmd(dockerCLI) plugin := makeCmd(dockerCli)
if err := RunPlugin(dockerCLI, plugin, meta); err != nil { if err := RunPlugin(dockerCli, plugin, meta); err != nil {
if stErr, ok := errors.AsType[cli.StatusError](err); ok { if sterr, ok := err.(cli.StatusError); ok {
if sterr.Status != "" {
fmt.Fprintln(dockerCli.Err(), sterr.Status)
}
// StatusError should only be used for errors, and all errors should // StatusError should only be used for errors, and all errors should
// have a non-zero exit status, so never exit with 0 // have a non-zero exit status, so never exit with 0
if stErr.StatusCode == 0 { // FIXME(thaJeztah): this should never be used with a zero status-code. Check if we do this anywhere. if sterr.StatusCode == 0 {
stErr.StatusCode = 1 os.Exit(1)
} }
_, _ = fmt.Fprintln(dockerCLI.Err(), stErr) os.Exit(sterr.StatusCode)
os.Exit(stErr.StatusCode)
} }
_, _ = fmt.Fprintln(dockerCLI.Err(), err) fmt.Fprintln(dockerCli.Err(), err)
os.Exit(1) os.Exit(1)
} }
} }
func withPluginClientConn(name string) command.CLIOption { func withPluginClientConn(name string) command.CLIOption {
return func(cli *command.DockerCli) error { return command.WithInitializeClient(func(dockerCli *command.DockerCli) (client.APIClient, error) {
cmd := "docker" cmd := "docker"
if x := os.Getenv(metadata.ReexecEnvvar); x != "" { if x := os.Getenv(manager.ReexecEnvvar); x != "" {
cmd = x cmd = x
} }
var flags []string var flags []string
@@ -139,19 +116,16 @@ func withPluginClientConn(name string) command.CLIOption {
helper, err := connhelper.GetCommandConnectionHelper(cmd, flags...) helper, err := connhelper.GetCommandConnectionHelper(cmd, flags...)
if err != nil { if err != nil {
return err return nil, err
} }
apiClient, err := client.New(client.WithDialContext(helper.Dialer))
if err != nil { return client.NewClientWithOpts(client.WithDialContext(helper.Dialer))
return err })
}
return command.WithAPIClient(apiClient)(cli)
}
} }
func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadata.Metadata) *cli.TopLevelCommand { func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) *cli.TopLevelCommand {
name := plugin.Name() name := plugin.Name()
fullname := metadata.NamePrefix + name fullname := manager.NamePrefix + name
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: fmt.Sprintf("docker [OPTIONS] %s [ARG...]", name), Use: fmt.Sprintf("docker [OPTIONS] %s [ARG...]", name),
@@ -167,14 +141,9 @@ func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta
CompletionOptions: cobra.CompletionOptions{ CompletionOptions: cobra.CompletionOptions{
DisableDefaultCmd: false, DisableDefaultCmd: false,
HiddenDefaultCmd: true, HiddenDefaultCmd: true,
DisableDescriptions: os.Getenv("DOCKER_CLI_DISABLE_COMPLETION_DESCRIPTION") != "", DisableDescriptions: true,
}, },
} }
// Disable file-completion by default. Most commands and flags should not
// complete with filenames.
cmd.CompletionOptions.SetDefaultShellCompDirective(cobra.ShellCompDirectiveNoFileComp)
opts, _ := cli.SetupPluginRootCommand(cmd) opts, _ := cli.SetupPluginRootCommand(cmd)
cmd.SetIn(dockerCli.In()) cmd.SetIn(dockerCli.In())
@@ -186,30 +155,17 @@ func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta
newMetadataSubcommand(plugin, meta), newMetadataSubcommand(plugin, meta),
) )
visitAll(cmd, cli.DisableFlagsInUseLine(cmd)
// prevent adding "[flags]" to the end of the usage line.
func(c *cobra.Command) { c.DisableFlagsInUseLine = true },
)
return cli.NewTopLevelCommand(cmd, dockerCli, opts, cmd.Flags()) return cli.NewTopLevelCommand(cmd, dockerCli, opts, cmd.Flags())
} }
// visitAll traverses all commands from the root. func newMetadataSubcommand(plugin *cobra.Command, meta manager.Metadata) *cobra.Command {
func visitAll(root *cobra.Command, fns ...func(*cobra.Command)) {
for _, cmd := range root.Commands() {
visitAll(cmd, fns...)
}
for _, fn := range fns {
fn(root)
}
}
func newMetadataSubcommand(plugin *cobra.Command, meta metadata.Metadata) *cobra.Command {
if meta.ShortDescription == "" { if meta.ShortDescription == "" {
meta.ShortDescription = plugin.Short meta.ShortDescription = plugin.Short
} }
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: metadata.MetadataSubcommandName, Use: manager.MetadataSubcommandName,
Hidden: true, Hidden: true,
// Suppress the global/parent PersistentPreRunE, which // Suppress the global/parent PersistentPreRunE, which
// needlessly initializes the client and tries to // needlessly initializes the client and tries to
@@ -227,8 +183,8 @@ func newMetadataSubcommand(plugin *cobra.Command, meta metadata.Metadata) *cobra
// RunningStandalone tells a CLI plugin it is run standalone by direct execution // RunningStandalone tells a CLI plugin it is run standalone by direct execution
func RunningStandalone() bool { func RunningStandalone() bool {
if os.Getenv(metadata.ReexecEnvvar) != "" { if os.Getenv(manager.ReexecEnvvar) != "" {
return false return false
} }
return len(os.Args) < 2 || os.Args[1] != metadata.MetadataSubcommandName return len(os.Args) < 2 || os.Args[1] != manager.MetadataSubcommandName
} }
-28
View File
@@ -1,28 +0,0 @@
package plugin
import (
"slices"
"testing"
"github.com/spf13/cobra"
)
func TestVisitAll(t *testing.T) {
root := &cobra.Command{Use: "root"}
sub1 := &cobra.Command{Use: "sub1"}
sub1sub1 := &cobra.Command{Use: "sub1sub1"}
sub1sub2 := &cobra.Command{Use: "sub1sub2"}
sub2 := &cobra.Command{Use: "sub2"}
root.AddCommand(sub1, sub2)
sub1.AddCommand(sub1sub1, sub1sub2)
var visited []string
visitAll(root, func(ccmd *cobra.Command) {
visited = append(visited, ccmd.Name())
})
expected := []string{"sub1sub1", "sub1sub2", "sub1", "sub2", "root"}
if !slices.Equal(expected, visited) {
t.Errorf("expected %#v, got %#v", expected, visited)
}
}
+17 -114
View File
@@ -1,139 +1,42 @@
package socket package socket
import ( import (
"crypto/rand"
"encoding/hex"
"errors" "errors"
"io" "io"
"net" "net"
"os" "os"
"runtime"
"sync"
"github.com/sirupsen/logrus" "github.com/docker/distribution/uuid"
) )
// EnvKey represents the well-known environment variable used to pass the // EnvKey represents the well-known environment variable used to pass the plugin being
// plugin being executed the socket name it should listen on to coordinate with // executed the socket name it should listen on to coordinate with the host CLI.
// the host CLI.
const EnvKey = "DOCKER_CLI_PLUGIN_SOCKET" const EnvKey = "DOCKER_CLI_PLUGIN_SOCKET"
// NewPluginServer creates a plugin server that listens on a new Unix domain // SetupConn sets up a Unix socket listener, establishes a goroutine to handle connections
// socket. h is called for each new connection to the socket in a goroutine. // and update the conn pointer, and returns the listener for the socket (which the caller
func NewPluginServer(h func(net.Conn)) (*PluginServer, error) { // is responsible for closing when it's no longer needed).
// Listen on a Unix socket, with the address being platform-dependent. func SetupConn(conn **net.UnixConn) (*net.UnixListener, error) {
// When a non-abstract address is used, Go will unlink(2) the socket listener, err := listen("docker_cli_" + uuid.Generate().String())
// for us once the listener is closed, as documented in
// [net.UnixListener.SetUnlinkOnClose].
l, err := net.ListenUnix("unix", &net.UnixAddr{
Name: socketName("docker_cli_" + randomID()),
Net: "unix",
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
logrus.Trace("Plugin server listening on ", l.Addr())
if h == nil { accept(listener, conn)
h = func(net.Conn) {}
}
pl := &PluginServer{ return listener, nil
l: l, }
h: h,
}
func accept(listener *net.UnixListener, conn **net.UnixConn) {
go func() { go func() {
defer pl.Close()
for { for {
err := pl.accept() // ignore error here, if we failed to accept a connection,
if err != nil { // conn is nil and we fallback to previous behavior
return *conn, _ = listener.AcceptUnix()
} // perform any platform-specific actions on accept (e.g. unlink non-abstract sockets)
onAccept(*conn, listener)
} }
}() }()
return pl, nil
}
type PluginServer struct {
mu sync.Mutex
conns []net.Conn
l *net.UnixListener
h func(net.Conn)
closed bool
}
func (pl *PluginServer) accept() error {
conn, err := pl.l.Accept()
if err != nil {
return err
}
pl.mu.Lock()
defer pl.mu.Unlock()
if pl.closed {
// Handle potential race between Close and accept.
conn.Close()
return errors.New("plugin server is closed")
}
pl.conns = append(pl.conns, conn)
go pl.h(conn)
return nil
}
// Addr returns the [net.Addr] of the underlying [net.Listener].
func (pl *PluginServer) Addr() net.Addr {
return pl.l.Addr()
}
// Close ensures that the server is no longer accepting new connections and
// closes all existing connections. Existing connections will receive [io.EOF].
//
// The error value is that of the underlying [net.Listener.Close] call.
func (pl *PluginServer) Close() error {
if pl == nil {
return nil
}
logrus.Trace("Closing plugin server")
// Close connections first to ensure the connections get io.EOF instead
// of a connection reset.
pl.closeAllConns()
// Try to ensure that any active connections have a chance to receive
// io.EOF.
runtime.Gosched()
return pl.l.Close()
}
func (pl *PluginServer) closeAllConns() {
pl.mu.Lock()
defer pl.mu.Unlock()
if pl.closed {
return
}
// Prevent new connections from being accepted.
pl.closed = true
for _, conn := range pl.conns {
conn.Close()
}
pl.conns = nil
}
func randomID() string {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
panic(err) // This shouldn't happen
}
return hex.EncodeToString(b)
} }
// ConnectAndWait connects to the socket passed via well-known env var, // ConnectAndWait connects to the socket passed via well-known env var,
-9
View File
@@ -1,9 +0,0 @@
//go:build windows || linux
package socket
func socketName(basename string) string {
// Address of an abstract socket -- this socket can be opened by name,
// but is not present in the filesystem.
return "@" + basename
}
+19
View File
@@ -0,0 +1,19 @@
package socket
import (
"net"
"os"
"path/filepath"
"syscall"
)
func listen(socketname string) (*net.UnixListener, error) {
return net.ListenUnix("unix", &net.UnixAddr{
Name: filepath.Join(os.TempDir(), socketname),
Net: "unix",
})
}
func onAccept(conn *net.UnixConn, listener *net.UnixListener) {
syscall.Unlink(listener.Addr().String())
}
-14
View File
@@ -1,14 +0,0 @@
//go:build !windows && !linux
package socket
import (
"os"
"path/filepath"
)
func socketName(basename string) string {
// Because abstract sockets are unavailable, use a socket path in the
// system temporary directory.
return filepath.Join(os.TempDir(), basename)
}
+20
View File
@@ -0,0 +1,20 @@
//go:build !darwin && !openbsd
package socket
import (
"net"
)
func listen(socketname string) (*net.UnixListener, error) {
return net.ListenUnix("unix", &net.UnixAddr{
Name: "@" + socketname,
Net: "unix",
})
}
func onAccept(conn *net.UnixConn, listener *net.UnixListener) {
// do nothing
// while on darwin and OpenBSD we would unlink here;
// on non-darwin the socket is abstract and not present on the filesystem
}
+19
View File
@@ -0,0 +1,19 @@
package socket
import (
"net"
"os"
"path/filepath"
"syscall"
)
func listen(socketname string) (*net.UnixListener, error) {
return net.ListenUnix("unix", &net.UnixAddr{
Name: filepath.Join(os.TempDir(), socketname),
Net: "unix",
})
}
func onAccept(conn *net.UnixConn, listener *net.UnixListener) {
syscall.Unlink(listener.Addr().String())
}
+53 -136
View File
@@ -1,14 +1,11 @@
package socket package socket
import ( import (
"errors"
"io"
"io/fs" "io/fs"
"net" "net"
"os" "os"
"runtime" "runtime"
"strings" "strings"
"sync/atomic"
"testing" "testing"
"time" "time"
@@ -16,122 +13,54 @@ import (
"gotest.tools/v3/poll" "gotest.tools/v3/poll"
) )
func TestPluginServer(t *testing.T) { func TestSetupConn(t *testing.T) {
t.Run("connection closes with EOF when server closes", func(t *testing.T) { t.Run("updates conn when connected", func(t *testing.T) {
called := make(chan struct{}) var conn *net.UnixConn
srv, err := NewPluginServer(func(_ net.Conn) { close(called) }) listener, err := SetupConn(&conn)
assert.NilError(t, err) assert.NilError(t, err)
assert.Assert(t, srv != nil, "returned nil server but no error") assert.Check(t, listener != nil, "returned nil listener but no error")
addr, err := net.ResolveUnixAddr("unix", listener.Addr().String())
assert.NilError(t, err, "failed to resolve listener address")
addr, err := net.ResolveUnixAddr("unix", srv.Addr().String()) _, err = net.DialUnix("unix", nil, addr)
assert.NilError(t, err, "failed to resolve server address") assert.NilError(t, err, "failed to dial returned listener")
conn, err := net.DialUnix("unix", nil, addr) pollConnNotNil(t, &conn)
assert.NilError(t, err, "failed to dial returned server")
defer conn.Close()
done := make(chan error, 1)
go func() {
_, err := conn.Read(make([]byte, 1))
done <- err
}()
select {
case <-called:
case <-time.After(10 * time.Millisecond):
t.Fatal("handler not called")
}
srv.Close()
select {
case err := <-done:
if !errors.Is(err, io.EOF) {
t.Fatalf("expected EOF error, got: %v", err)
}
case <-time.After(10 * time.Millisecond):
}
}) })
t.Run("allows reconnects", func(t *testing.T) { t.Run("allows reconnects", func(t *testing.T) {
var calls atomic.Int32 var conn *net.UnixConn
h := func(_ net.Conn) { listener, err := SetupConn(&conn)
calls.Add(1)
}
srv, err := NewPluginServer(h)
assert.NilError(t, err) assert.NilError(t, err)
defer srv.Close() assert.Check(t, listener != nil, "returned nil listener but no error")
addr, err := net.ResolveUnixAddr("unix", listener.Addr().String())
assert.Check(t, srv.Addr() != nil, "returned nil addr but no error") assert.NilError(t, err, "failed to resolve listener address")
addr, err := net.ResolveUnixAddr("unix", srv.Addr().String())
assert.NilError(t, err, "failed to resolve server address")
waitForCalls := func(n int) {
poll.WaitOn(t, func(t poll.LogT) poll.Result {
if calls.Load() == int32(n) {
return poll.Success()
}
return poll.Continue("waiting for handler to be called")
})
}
otherConn, err := net.DialUnix("unix", nil, addr) otherConn, err := net.DialUnix("unix", nil, addr)
assert.NilError(t, err, "failed to dial returned server") assert.NilError(t, err, "failed to dial returned listener")
otherConn.Close() otherConn.Close()
waitForCalls(1)
conn, err := net.DialUnix("unix", nil, addr) _, err = net.DialUnix("unix", nil, addr)
assert.NilError(t, err, "failed to redial server") assert.NilError(t, err, "failed to redial listener")
defer conn.Close()
waitForCalls(2)
// and again but don't close the existing connection
conn2, err := net.DialUnix("unix", nil, addr)
assert.NilError(t, err, "failed to redial server")
defer conn2.Close()
waitForCalls(3)
srv.Close()
// now make sure we get EOF on the existing connections
buf := make([]byte, 1)
_, err = conn.Read(buf)
assert.ErrorIs(t, err, io.EOF, "expected EOF error, got: %v", err)
_, err = conn2.Read(buf)
assert.ErrorIs(t, err, io.EOF, "expected EOF error, got: %v", err)
}) })
t.Run("does not leak sockets to local directory", func(t *testing.T) { t.Run("does not leak sockets to local directory", func(t *testing.T) {
srv, err := NewPluginServer(nil) var conn *net.UnixConn
listener, err := SetupConn(&conn)
assert.NilError(t, err) assert.NilError(t, err)
assert.Check(t, srv != nil, "returned nil server but no error") assert.Check(t, listener != nil, "returned nil listener but no error")
checkDirNoNewPluginServer(t) checkDirNoPluginSocket(t)
addr, err := net.ResolveUnixAddr("unix", srv.Addr().String())
assert.NilError(t, err, "failed to resolve server address")
addr, err := net.ResolveUnixAddr("unix", listener.Addr().String())
assert.NilError(t, err, "failed to resolve listener address")
_, err = net.DialUnix("unix", nil, addr) _, err = net.DialUnix("unix", nil, addr)
assert.NilError(t, err, "failed to dial returned server") assert.NilError(t, err, "failed to dial returned listener")
checkDirNoNewPluginServer(t) checkDirNoPluginSocket(t)
})
t.Run("does not panic on Close if server is nil", func(t *testing.T) {
var srv *PluginServer
defer func() {
if r := recover(); r != nil {
t.Errorf("panicked on Close")
}
}()
err := srv.Close()
assert.NilError(t, err)
}) })
} }
func checkDirNoNewPluginServer(t *testing.T) { func checkDirNoPluginSocket(t *testing.T) {
t.Helper() t.Helper()
files, err := os.ReadDir(".") files, err := os.ReadDir(".")
@@ -149,24 +78,18 @@ func checkDirNoNewPluginServer(t *testing.T) {
func TestConnectAndWait(t *testing.T) { func TestConnectAndWait(t *testing.T) {
t.Run("calls cancel func on EOF", func(t *testing.T) { t.Run("calls cancel func on EOF", func(t *testing.T) {
srv, err := NewPluginServer(nil) var conn *net.UnixConn
assert.NilError(t, err, "failed to setup server") listener, err := SetupConn(&conn)
defer srv.Close() assert.NilError(t, err, "failed to setup listener")
done := make(chan struct{}) done := make(chan struct{})
t.Setenv(EnvKey, srv.Addr().String()) t.Setenv(EnvKey, listener.Addr().String())
cancelFunc := func() { cancelFunc := func() {
done <- struct{}{} done <- struct{}{}
} }
ConnectAndWait(cancelFunc) ConnectAndWait(cancelFunc)
pollConnNotNil(t, &conn)
select { conn.Close()
case <-done:
t.Fatal("unexpectedly done")
default:
}
srv.Close()
select { select {
case <-done: case <-done:
@@ -178,39 +101,33 @@ func TestConnectAndWait(t *testing.T) {
// TODO: this test cannot be executed with `t.Parallel()`, due to // TODO: this test cannot be executed with `t.Parallel()`, due to
// relying on goroutine numbers to ensure correct behaviour // relying on goroutine numbers to ensure correct behaviour
t.Run("connect goroutine exits after EOF", func(t *testing.T) { t.Run("connect goroutine exits after EOF", func(t *testing.T) {
runtime.LockOSThread() var conn *net.UnixConn
defer runtime.UnlockOSThread() listener, err := SetupConn(&conn)
srv, err := NewPluginServer(nil) assert.NilError(t, err, "failed to setup listener")
assert.NilError(t, err, "failed to setup server") t.Setenv(EnvKey, listener.Addr().String())
defer srv.Close()
t.Setenv(EnvKey, srv.Addr().String())
runtime.Gosched()
numGoroutines := runtime.NumGoroutine() numGoroutines := runtime.NumGoroutine()
ConnectAndWait(func() {}) ConnectAndWait(func() {})
assert.Equal(t, runtime.NumGoroutine(), numGoroutines+1)
runtime.Gosched() pollConnNotNil(t, &conn)
conn.Close()
poll.WaitOn(t, func(t poll.LogT) poll.Result { poll.WaitOn(t, func(t poll.LogT) poll.Result {
// +1 goroutine for the poll.WaitOn
// +1 goroutine for the connect goroutine
if runtime.NumGoroutine() < numGoroutines+1+1 {
return poll.Continue("waiting for connect goroutine to spawn")
}
return poll.Success()
}, poll.WithDelay(1*time.Millisecond), poll.WithTimeout(500*time.Millisecond))
srv.Close()
runtime.Gosched()
poll.WaitOn(t, func(t poll.LogT) poll.Result {
// +1 goroutine for the poll.WaitOn
if runtime.NumGoroutine() > numGoroutines+1 { if runtime.NumGoroutine() > numGoroutines+1 {
return poll.Continue("waiting for connect goroutine to exit") return poll.Continue("waiting for connect goroutine to exit")
} }
return poll.Success() return poll.Success()
}, poll.WithDelay(1*time.Millisecond), poll.WithTimeout(500*time.Millisecond)) }, poll.WithDelay(1*time.Millisecond), poll.WithTimeout(10*time.Millisecond))
}) })
} }
func pollConnNotNil(t *testing.T, conn **net.UnixConn) {
t.Helper()
poll.WaitOn(t, func(t poll.LogT) poll.Result {
if *conn == nil {
return poll.Continue("waiting for conn to not be nil")
}
return poll.Success()
}, poll.WithDelay(1*time.Millisecond), poll.WithTimeout(10*time.Millisecond))
}
+67 -26
View File
@@ -1,20 +1,21 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.26
package cli package cli
import ( import (
"fmt" "fmt"
"os" "os"
"slices" "path/filepath"
"sort"
"strings" "strings"
"github.com/docker/cli/cli-plugins/metadata" pluginmanager "github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
cliflags "github.com/docker/cli/cli/flags" cliflags "github.com/docker/cli/cli/flags"
"github.com/docker/docker/pkg/homedir"
"github.com/docker/docker/registry"
"github.com/fvbommel/sortorder" "github.com/fvbommel/sortorder"
"github.com/moby/term" "github.com/moby/term"
"github.com/morikuni/aec" "github.com/morikuni/aec"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
) )
@@ -53,7 +54,7 @@ func setupCommonRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *c
rootCmd.SetHelpCommand(helpCommand) rootCmd.SetHelpCommand(helpCommand)
rootCmd.PersistentFlags().BoolP("help", "h", false, "Print usage") rootCmd.PersistentFlags().BoolP("help", "h", false, "Print usage")
rootCmd.PersistentFlags().MarkShorthandDeprecated("help", "use --help") rootCmd.PersistentFlags().MarkShorthandDeprecated("help", "please use --help")
rootCmd.PersistentFlags().Lookup("help").Hidden = true rootCmd.PersistentFlags().Lookup("help").Hidden = true
rootCmd.Annotations = map[string]string{ rootCmd.Annotations = map[string]string{
@@ -61,6 +62,13 @@ func setupCommonRootCommand(rootCmd *cobra.Command) (*cliflags.ClientOptions, *c
"docs.code-delimiter": `"`, // https://github.com/docker/cli-docs-tool/blob/77abede22166eaea4af7335096bdcedd043f5b19/annotation/annotation.go#L20-L22 "docs.code-delimiter": `"`, // https://github.com/docker/cli-docs-tool/blob/77abede22166eaea4af7335096bdcedd043f5b19/annotation/annotation.go#L20-L22
} }
// Configure registry.CertsDir() when running in rootless-mode
if os.Getenv("ROOTLESSKIT_STATE_DIR") != "" {
if configHome, err := homedir.GetConfigHome(); err == nil {
registry.SetCertsDir(filepath.Join(configHome, "docker/certs.d"))
}
}
return opts, helpCommand return opts, helpCommand
} }
@@ -84,8 +92,12 @@ func FlagErrorFunc(cmd *cobra.Command, err error) error {
return nil return nil
} }
usage := ""
if cmd.HasSubCommands() {
usage = "\n\n" + cmd.UsageString()
}
return StatusError{ return StatusError{
Status: fmt.Sprintf("%s\n\nUsage: %s\n\nRun '%s --help' for more information", err, cmd.UseLine(), cmd.CommandPath()), Status: fmt.Sprintf("%s\nSee '%s --help'.%s", err, cmd.CommandPath(), usage),
StatusCode: 125, StatusCode: 125,
} }
} }
@@ -169,6 +181,35 @@ func (tcmd *TopLevelCommand) Initialize(ops ...command.CLIOption) error {
return tcmd.dockerCli.Initialize(tcmd.opts, ops...) return tcmd.dockerCli.Initialize(tcmd.opts, ops...)
} }
// VisitAll will traverse all commands from the root.
// This is different from the VisitAll of cobra.Command where only parents
// are checked.
func VisitAll(root *cobra.Command, fn func(*cobra.Command)) {
for _, cmd := range root.Commands() {
VisitAll(cmd, fn)
}
fn(root)
}
// DisableFlagsInUseLine sets the DisableFlagsInUseLine flag on all
// commands within the tree rooted at cmd.
func DisableFlagsInUseLine(cmd *cobra.Command) {
VisitAll(cmd, func(ccmd *cobra.Command) {
// do not add a `[flags]` to the end of the usage line.
ccmd.DisableFlagsInUseLine = true
})
}
// HasCompletionArg returns true if a cobra completion arg request is found.
func HasCompletionArg(args []string) bool {
for _, arg := range args {
if arg == cobra.ShellCompRequestCmd || arg == cobra.ShellCompNoDescRequestCmd {
return true
}
}
return false
}
var helpCommand = &cobra.Command{ var helpCommand = &cobra.Command{
Use: "help [command]", Use: "help [command]",
Short: "Help about the command", Short: "Help about the command",
@@ -177,7 +218,7 @@ var helpCommand = &cobra.Command{
RunE: func(c *cobra.Command, args []string) error { RunE: func(c *cobra.Command, args []string) error {
cmd, args, e := c.Root().Find(args) cmd, args, e := c.Root().Find(args)
if cmd == nil || e != nil || len(args) > 0 { if cmd == nil || e != nil || len(args) > 0 {
return fmt.Errorf("unknown help topic: %v", strings.Join(args, " ")) return errors.Errorf("unknown help topic: %v", strings.Join(args, " "))
} }
helpFunc := cmd.HelpFunc() helpFunc := cmd.HelpFunc()
helpFunc(cmd, args) helpFunc(cmd, args)
@@ -215,7 +256,7 @@ func hasAdditionalHelp(cmd *cobra.Command) bool {
} }
func isPlugin(cmd *cobra.Command) bool { func isPlugin(cmd *cobra.Command) bool {
return cmd.Annotations[metadata.CommandAnnotationPlugin] == "true" return pluginmanager.IsPluginCommand(cmd)
} }
func hasAliases(cmd *cobra.Command) bool { func hasAliases(cmd *cobra.Command) bool {
@@ -253,12 +294,11 @@ func commandAliases(cmd *cobra.Command) string {
if cmd.HasParent() { if cmd.HasParent() {
parentPath = cmd.Parent().CommandPath() + " " parentPath = cmd.Parent().CommandPath() + " "
} }
var aliases strings.Builder aliases := cmd.CommandPath()
aliases.WriteString(cmd.CommandPath())
for _, alias := range cmd.Aliases { for _, alias := range cmd.Aliases {
aliases.WriteString(", " + parentPath + alias) aliases += ", " + parentPath + alias
} }
return aliases.String() return aliases
} }
func topCommands(cmd *cobra.Command) []*cobra.Command { func topCommands(cmd *cobra.Command) []*cobra.Command {
@@ -276,8 +316,8 @@ func topCommands(cmd *cobra.Command) []*cobra.Command {
cmds = append(cmds, sub) cmds = append(cmds, sub)
} }
} }
slices.SortStableFunc(cmds, func(a, b *cobra.Command) int { sort.SliceStable(cmds, func(i, j int) bool {
return sortorder.NaturalCompare(a.Annotations["category-top"], b.Annotations["category-top"]) return sortorder.NaturalLess(cmds[i].Annotations["category-top"], cmds[j].Annotations["category-top"])
}) })
return cmds return cmds
} }
@@ -301,10 +341,8 @@ func operationSubCommands(cmd *cobra.Command) []*cobra.Command {
return cmds return cmds
} }
const defaultTermWidth = 80
func wrappedFlagUsages(cmd *cobra.Command) string { func wrappedFlagUsages(cmd *cobra.Command) string {
width := defaultTermWidth width := 80
if ws, err := term.GetWinsize(0); err == nil { if ws, err := term.GetWinsize(0); err == nil {
width = int(ws.Width) width = int(ws.Width)
} }
@@ -320,9 +358,9 @@ func decoratedName(cmd *cobra.Command) string {
} }
func vendorAndVersion(cmd *cobra.Command) string { func vendorAndVersion(cmd *cobra.Command) string {
if vendor, ok := cmd.Annotations[metadata.CommandAnnotationPluginVendor]; ok && isPlugin(cmd) { if vendor, ok := cmd.Annotations[pluginmanager.CommandAnnotationPluginVendor]; ok && isPlugin(cmd) {
version := "" version := ""
if v, ok := cmd.Annotations[metadata.CommandAnnotationPluginVersion]; ok && v != "" { if v, ok := cmd.Annotations[pluginmanager.CommandAnnotationPluginVersion]; ok && v != "" {
version = ", " + v version = ", " + v
} }
return fmt.Sprintf("(%s%s)", vendor, version) return fmt.Sprintf("(%s%s)", vendor, version)
@@ -354,10 +392,13 @@ func orchestratorSubCommands(cmd *cobra.Command) []*cobra.Command {
func allManagementSubCommands(cmd *cobra.Command) []*cobra.Command { func allManagementSubCommands(cmd *cobra.Command) []*cobra.Command {
cmds := []*cobra.Command{} cmds := []*cobra.Command{}
for _, sub := range cmd.Commands() { for _, sub := range cmd.Commands() {
if invalidPluginReason(sub) != "" { if isPlugin(sub) {
if invalidPluginReason(sub) == "" {
cmds = append(cmds, sub)
}
continue continue
} }
if sub.IsAvailableCommand() && (isPlugin(sub) || sub.HasSubCommands()) { if sub.IsAvailableCommand() && sub.HasSubCommands() {
cmds = append(cmds, sub) cmds = append(cmds, sub)
} }
} }
@@ -378,7 +419,7 @@ func invalidPlugins(cmd *cobra.Command) []*cobra.Command {
} }
func invalidPluginReason(cmd *cobra.Command) string { func invalidPluginReason(cmd *cobra.Command) string {
return cmd.Annotations[metadata.CommandAnnotationPluginInvalid] return cmd.Annotations[pluginmanager.CommandAnnotationPluginInvalid]
} }
const usageTemplate = `Usage: const usageTemplate = `Usage:
@@ -429,7 +470,7 @@ Common Commands:
Management Commands: Management Commands:
{{- range managementSubCommands . }} {{- range managementSubCommands . }}
{{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}} {{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}{{ if isPlugin .}} {{vendorAndVersion .}}{{ end}}
{{- end}} {{- end}}
{{- end}} {{- end}}
@@ -438,7 +479,7 @@ Management Commands:
Swarm Commands: Swarm Commands:
{{- range orchestratorSubCommands . }} {{- range orchestratorSubCommands . }}
{{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}} {{rpad (decoratedName .) (add .NamePadding 1)}}{{.Short}}{{ if isPlugin .}} {{vendorAndVersion .}}{{ end}}
{{- end}} {{- end}}
{{- end}} {{- end}}
@@ -481,4 +522,4 @@ Run '{{.CommandPath}} COMMAND --help' for more information on a command.
` `
const helpTemplate = ` const helpTemplate = `
{{- if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}` {{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}`
+29 -34
View File
@@ -3,13 +3,35 @@ package cli
import ( import (
"testing" "testing"
"github.com/docker/cli/cli-plugins/metadata" pluginmanager "github.com/docker/cli/cli-plugins/manager"
"github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-cmp/cmp/cmpopts"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp" is "gotest.tools/v3/assert/cmp"
) )
func TestVisitAll(t *testing.T) {
root := &cobra.Command{Use: "root"}
sub1 := &cobra.Command{Use: "sub1"}
sub1sub1 := &cobra.Command{Use: "sub1sub1"}
sub1sub2 := &cobra.Command{Use: "sub1sub2"}
sub2 := &cobra.Command{Use: "sub2"}
root.AddCommand(sub1, sub2)
sub1.AddCommand(sub1sub1, sub1sub2)
// Take the opportunity to test DisableFlagsInUseLine too
DisableFlagsInUseLine(root)
var visited []string
VisitAll(root, func(ccmd *cobra.Command) {
visited = append(visited, ccmd.Name())
assert.Assert(t, ccmd.DisableFlagsInUseLine, "DisableFlagsInUseLine not set on %q", ccmd.Name())
})
expected := []string{"sub1sub1", "sub1sub2", "sub1", "sub2", "root"}
assert.DeepEqual(t, expected, visited)
}
func TestVendorAndVersion(t *testing.T) { func TestVendorAndVersion(t *testing.T) {
// Non plugin. // Non plugin.
assert.Equal(t, vendorAndVersion(&cobra.Command{Use: "test"}), "") assert.Equal(t, vendorAndVersion(&cobra.Command{Use: "test"}), "")
@@ -27,9 +49,9 @@ func TestVendorAndVersion(t *testing.T) {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "test", Use: "test",
Annotations: map[string]string{ Annotations: map[string]string{
metadata.CommandAnnotationPlugin: "true", pluginmanager.CommandAnnotationPlugin: "true",
metadata.CommandAnnotationPluginVendor: tc.vendor, pluginmanager.CommandAnnotationPluginVendor: tc.vendor,
metadata.CommandAnnotationPluginVersion: tc.version, pluginmanager.CommandAnnotationPluginVersion: tc.version,
}, },
} }
assert.Equal(t, vendorAndVersion(cmd), tc.expected) assert.Equal(t, vendorAndVersion(cmd), tc.expected)
@@ -47,8 +69,8 @@ func TestInvalidPlugin(t *testing.T) {
assert.Assert(t, is.Len(invalidPlugins(root), 0)) assert.Assert(t, is.Len(invalidPlugins(root), 0))
sub1.Annotations = map[string]string{ sub1.Annotations = map[string]string{
metadata.CommandAnnotationPlugin: "true", pluginmanager.CommandAnnotationPlugin: "true",
metadata.CommandAnnotationPluginInvalid: "foo", pluginmanager.CommandAnnotationPluginInvalid: "foo",
} }
root.AddCommand(sub1, sub2) root.AddCommand(sub1, sub2)
sub1.AddCommand(sub1sub1, sub1sub2) sub1.AddCommand(sub1sub1, sub1sub2)
@@ -56,33 +78,6 @@ func TestInvalidPlugin(t *testing.T) {
assert.DeepEqual(t, invalidPlugins(root), []*cobra.Command{sub1}, cmpopts.IgnoreUnexported(cobra.Command{})) assert.DeepEqual(t, invalidPlugins(root), []*cobra.Command{sub1}, cmpopts.IgnoreUnexported(cobra.Command{}))
} }
func TestHiddenPlugin(t *testing.T) {
root := &cobra.Command{Use: "root"}
sub1 := &cobra.Command{
Use: "sub1",
Hidden: true,
Annotations: map[string]string{
metadata.CommandAnnotationPlugin: "true",
},
Run: func(cmd *cobra.Command, args []string) {},
}
sub1sub1 := &cobra.Command{Use: "sub1sub1"}
sub1sub2 := &cobra.Command{Use: "sub1sub2"}
sub2 := &cobra.Command{
Use: "sub2",
Annotations: map[string]string{
metadata.CommandAnnotationPlugin: "true",
},
Run: func(cmd *cobra.Command, args []string) {},
}
root.AddCommand(sub1, sub2)
sub1.AddCommand(sub1sub1, sub1sub2)
assert.DeepEqual(t, allManagementSubCommands(root), []*cobra.Command{sub2}, cmpopts.IgnoreFields(cobra.Command{}, "Run"), cmpopts.IgnoreUnexported(cobra.Command{}))
}
func TestCommandAliases(t *testing.T) { func TestCommandAliases(t *testing.T) {
root := &cobra.Command{Use: "root"} root := &cobra.Command{Use: "root"}
sub := &cobra.Command{Use: "subcommand", Aliases: []string{"alias1", "alias2"}} sub := &cobra.Command{Use: "subcommand", Aliases: []string{"alias1", "alias2"}}
@@ -105,6 +100,6 @@ func TestDecoratedName(t *testing.T) {
topLevelCommand := &cobra.Command{Use: "pluginTopLevelCommand"} topLevelCommand := &cobra.Command{Use: "pluginTopLevelCommand"}
root.AddCommand(topLevelCommand) root.AddCommand(topLevelCommand)
assert.Equal(t, decoratedName(topLevelCommand), "pluginTopLevelCommand ") assert.Equal(t, decoratedName(topLevelCommand), "pluginTopLevelCommand ")
topLevelCommand.Annotations = map[string]string{metadata.CommandAnnotationPlugin: "true"} topLevelCommand.Annotations = map[string]string{pluginmanager.CommandAnnotationPlugin: "true"}
assert.Equal(t, decoratedName(topLevelCommand), "pluginTopLevelCommand*") assert.Equal(t, decoratedName(topLevelCommand), "pluginTopLevelCommand*")
} }
-19
View File
@@ -1,19 +0,0 @@
package builder
import (
"context"
"github.com/moby/moby/client"
)
type fakeClient struct {
client.Client
builderPruneFunc func(ctx context.Context, opts client.BuildCachePruneOptions) (client.BuildCachePruneResult, error)
}
func (c *fakeClient) BuildCachePrune(ctx context.Context, opts client.BuildCachePruneOptions) (client.BuildCachePruneResult, error) {
if c.builderPruneFunc != nil {
return c.builderPruneFunc(ctx, opts)
}
return client.BuildCachePruneResult{}, nil
}
+5 -37
View File
@@ -6,52 +6,20 @@ import (
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/image" "github.com/docker/cli/cli/command/image"
"github.com/docker/cli/internal/commands"
) )
func init() { // NewBuilderCommand returns a cobra command for `builder` subcommands
commands.Register(newBuilderCommand) func NewBuilderCommand(dockerCli command.Cli) *cobra.Command {
commands.Register(func(c command.Cli) *cobra.Command {
return newBakeStubCommand(c)
})
}
// newBuilderCommand returns a cobra command for `builder` subcommands
func newBuilderCommand(dockerCLI command.Cli) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "builder", Use: "builder",
Short: "Manage builds", Short: "Manage builds",
Args: cli.NoArgs, Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCLI.Err()), RunE: command.ShowHelp(dockerCli.Err()),
Annotations: map[string]string{"version": "1.31"}, Annotations: map[string]string{"version": "1.31"},
DisableFlagsInUseLine: true,
} }
cmd.AddCommand( cmd.AddCommand(
newPruneCommand(dockerCLI), NewPruneCommand(dockerCli),
// we should have a mechanism for registering sub-commands in the cli/internal/commands.Register function. image.NewBuildCommand(dockerCli),
//nolint:staticcheck // TODO: Remove when migration to cli/internal/commands.Register is complete. (see #6283)
image.NewBuildCommand(dockerCLI),
) )
return cmd return cmd
} }
// newBakeStubCommand returns a cobra command "stub" for the "bake" subcommand.
// This command is a placeholder / stub that is dynamically replaced by an
// alias for "docker buildx bake" if BuildKit is enabled (and the buildx plugin
// installed).
func newBakeStubCommand(dockerCLI command.Streams) *cobra.Command {
return &cobra.Command{
Use: "bake [OPTIONS] [TARGET...]",
Short: "Build from a file",
RunE: command.ShowHelp(dockerCLI.Err()),
Annotations: map[string]string{
// We want to show this command in the "top" category in --help
// output, and not to be grouped under "management commands".
"category-top": "5",
"aliases": "docker buildx bake",
"version": "1.31",
},
DisableFlagsInUseLine: true,
}
}
+27 -61
View File
@@ -2,36 +2,27 @@ package builder
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strings" "strings"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/system/pruner" "github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/internal/prompt"
"github.com/docker/cli/opts" "github.com/docker/cli/opts"
"github.com/docker/go-units" "github.com/docker/docker/api/types"
"github.com/moby/moby/client" units "github.com/docker/go-units"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func init() {
// Register the prune command to run as part of "docker system prune"
if err := pruner.Register(pruner.TypeBuildCache, pruneFn); err != nil {
panic(err)
}
}
type pruneOptions struct { type pruneOptions struct {
force bool force bool
all bool all bool
filter opts.FilterOpt filter opts.FilterOpt
reservedSpace opts.MemBytes keepStorage opts.MemBytes
} }
// newPruneCommand returns a new cobra prune command for images // NewPruneCommand returns a new cobra prune command for images
func newPruneCommand(dockerCLI command.Cli) *cobra.Command { func NewPruneCommand(dockerCli command.Cli) *cobra.Command {
options := pruneOptions{filter: opts.NewFilterOpt()} options := pruneOptions{filter: opts.NewFilterOpt()}
cmd := &cobra.Command{ cmd := &cobra.Command{
@@ -39,26 +30,25 @@ func newPruneCommand(dockerCLI command.Cli) *cobra.Command {
Short: "Remove build cache", Short: "Remove build cache",
Args: cli.NoArgs, Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
spaceReclaimed, output, err := runPrune(cmd.Context(), dockerCLI, options) spaceReclaimed, output, err := runPrune(cmd.Context(), dockerCli, options)
if err != nil { if err != nil {
return err return err
} }
if output != "" { if output != "" {
_, _ = fmt.Fprintln(dockerCLI.Out(), output) fmt.Fprintln(dockerCli.Out(), output)
} }
_, _ = fmt.Fprintln(dockerCLI.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed))) fmt.Fprintln(dockerCli.Out(), "Total reclaimed space:", units.HumanSize(float64(spaceReclaimed)))
return nil return nil
}, },
Annotations: map[string]string{"version": "1.39"}, Annotations: map[string]string{"version": "1.39"},
ValidArgsFunction: cobra.NoFileCompletions, ValidArgsFunction: completion.NoComplete,
DisableFlagsInUseLine: true,
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.BoolVarP(&options.force, "force", "f", false, "Do not prompt for confirmation") flags.BoolVarP(&options.force, "force", "f", false, "Do not prompt for confirmation")
flags.BoolVarP(&options.all, "all", "a", false, "Remove all unused build cache, not just dangling ones") flags.BoolVarP(&options.all, "all", "a", false, "Remove all unused build cache, not just dangling ones")
flags.Var(&options.filter, "filter", `Provide filter values (e.g. "until=24h")`) flags.Var(&options.filter, "filter", `Provide filter values (e.g. "until=24h")`)
flags.Var(&options.reservedSpace, "keep-storage", "Amount of disk space to keep for cache") flags.Var(&options.keepStorage, "keep-storage", "Amount of disk space to keep for cache")
return cmd return cmd
} }
@@ -69,31 +59,26 @@ const (
) )
func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint64, output string, err error) { func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions) (spaceReclaimed uint64, output string, err error) {
pruneFilters := command.PruneFilters(dockerCli, options.filter.Value()) pruneFilters := options.filter.Value()
pruneFilters = command.PruneFilters(dockerCli, pruneFilters)
warning := normalWarning warning := normalWarning
if options.all { if options.all {
warning = allCacheWarning warning = allCacheWarning
} }
if !options.force { if !options.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) {
r, err := prompt.Confirm(ctx, dockerCli.In(), dockerCli.Out(), warning) return 0, "", nil
if err != nil {
return 0, "", err
}
if !r {
return 0, "", cancelledErr{errors.New("builder prune has been cancelled")}
}
} }
resp, err := dockerCli.Client().BuildCachePrune(ctx, client.BuildCachePruneOptions{ report, err := dockerCli.Client().BuildCachePrune(ctx, types.BuildCachePruneOptions{
All: options.all, All: options.all,
ReservedSpace: options.reservedSpace.Value(), KeepStorage: options.keepStorage.Value(),
Filters: pruneFilters, Filters: pruneFilters,
}) })
if err != nil { if err != nil {
return 0, "", err return 0, "", err
} }
report := resp.Report
if len(report.CachesDeleted) > 0 { if len(report.CachesDeleted) > 0 {
var sb strings.Builder var sb strings.Builder
sb.WriteString("Deleted build cache objects:\n") sb.WriteString("Deleted build cache objects:\n")
@@ -107,26 +92,7 @@ func runPrune(ctx context.Context, dockerCli command.Cli, options pruneOptions)
return report.SpaceReclaimed, output, nil return report.SpaceReclaimed, output, nil
} }
type cancelledErr struct{ error } // CachePrune executes a prune command for build cache
func CachePrune(ctx context.Context, dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) {
func (cancelledErr) Cancelled() {} return runPrune(ctx, dockerCli, pruneOptions{force: true, all: all, filter: filter})
// pruneFn prunes the build cache for use in "docker system prune" and
// returns the amount of space reclaimed and a detailed output string.
func pruneFn(ctx context.Context, dockerCLI command.Cli, options pruner.PruneOptions) (uint64, string, error) {
if !options.Confirmed {
// Dry-run: perform validation and produce confirmation before pruning.
var confirmMsg string
if options.All {
confirmMsg = "all build cache"
} else {
confirmMsg = "unused build cache"
}
return 0, confirmMsg, cancelledErr{errors.New("builder prune has been cancelled")}
}
return runPrune(ctx, dockerCLI, pruneOptions{
force: true,
all: options.All,
filter: options.Filter,
})
} }
-26
View File
@@ -1,26 +0,0 @@
package builder
import (
"context"
"errors"
"io"
"testing"
"github.com/docker/cli/internal/test"
"github.com/moby/moby/client"
)
func TestBuilderPromptTermination(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
cli := test.NewFakeCli(&fakeClient{
builderPruneFunc: func(ctx context.Context, opts client.BuildCachePruneOptions) (client.BuildCachePruneResult, error) {
return client.BuildCachePruneResult{}, errors.New("fakeClient builderPruneFunc should not be called")
},
})
cmd := newPruneCommand(cli)
cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
test.TerminatePrompt(ctx, t, cmd, cli)
}
+11 -10
View File
@@ -3,33 +3,34 @@ package checkpoint
import ( import (
"context" "context"
"github.com/moby/moby/client" "github.com/docker/docker/api/types/checkpoint"
"github.com/docker/docker/client"
) )
type fakeClient struct { type fakeClient struct {
client.Client client.Client
checkpointCreateFunc func(container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error) checkpointCreateFunc func(container string, options checkpoint.CreateOptions) error
checkpointDeleteFunc func(container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error) checkpointDeleteFunc func(container string, options checkpoint.DeleteOptions) error
checkpointListFunc func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error) checkpointListFunc func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error)
} }
func (cli *fakeClient) CheckpointCreate(_ context.Context, container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error) { func (cli *fakeClient) CheckpointCreate(_ context.Context, container string, options checkpoint.CreateOptions) error {
if cli.checkpointCreateFunc != nil { if cli.checkpointCreateFunc != nil {
return cli.checkpointCreateFunc(container, options) return cli.checkpointCreateFunc(container, options)
} }
return client.CheckpointCreateResult{}, nil return nil
} }
func (cli *fakeClient) CheckpointRemove(_ context.Context, container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error) { func (cli *fakeClient) CheckpointDelete(_ context.Context, container string, options checkpoint.DeleteOptions) error {
if cli.checkpointDeleteFunc != nil { if cli.checkpointDeleteFunc != nil {
return cli.checkpointDeleteFunc(container, options) return cli.checkpointDeleteFunc(container, options)
} }
return client.CheckpointRemoveResult{}, nil return nil
} }
func (cli *fakeClient) CheckpointList(_ context.Context, container string, options client.CheckpointListOptions) (client.CheckpointListResult, error) { func (cli *fakeClient) CheckpointList(_ context.Context, container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) {
if cli.checkpointListFunc != nil { if cli.checkpointListFunc != nil {
return cli.checkpointListFunc(container, options) return cli.checkpointListFunc(container, options)
} }
return client.CheckpointListResult{}, nil return []checkpoint.Summary{}, nil
} }
+6 -12
View File
@@ -3,32 +3,26 @@ package checkpoint
import ( import (
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/internal/commands"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func init() { // NewCheckpointCommand returns the `checkpoint` subcommand (only in experimental)
commands.Register(newCheckpointCommand) func NewCheckpointCommand(dockerCli command.Cli) *cobra.Command {
}
// newCheckpointCommand returns the `checkpoint` subcommand (only in experimental)
func newCheckpointCommand(dockerCLI command.Cli) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "checkpoint", Use: "checkpoint",
Short: "Manage checkpoints", Short: "Manage checkpoints",
Args: cli.NoArgs, Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCLI.Err()), RunE: command.ShowHelp(dockerCli.Err()),
Annotations: map[string]string{ Annotations: map[string]string{
"experimental": "", "experimental": "",
"ostype": "linux", "ostype": "linux",
"version": "1.25", "version": "1.25",
}, },
DisableFlagsInUseLine: true,
} }
cmd.AddCommand( cmd.AddCommand(
newCreateCommand(dockerCLI), newCreateCommand(dockerCli),
newListCommand(dockerCLI), newListCommand(dockerCli),
newRemoveCommand(dockerCLI), newRemoveCommand(dockerCli),
) )
return cmd return cmd
} }
+9 -9
View File
@@ -6,7 +6,8 @@ import (
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/moby/moby/client" "github.com/docker/cli/cli/command/completion"
"github.com/docker/docker/api/types/checkpoint"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -17,7 +18,7 @@ type createOptions struct {
leaveRunning bool leaveRunning bool
} }
func newCreateCommand(dockerCLI command.Cli) *cobra.Command { func newCreateCommand(dockerCli command.Cli) *cobra.Command {
var opts createOptions var opts createOptions
cmd := &cobra.Command{ cmd := &cobra.Command{
@@ -27,21 +28,20 @@ func newCreateCommand(dockerCLI command.Cli) *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
opts.container = args[0] opts.container = args[0]
opts.checkpoint = args[1] opts.checkpoint = args[1]
return runCreate(cmd.Context(), dockerCLI, opts) return runCreate(cmd.Context(), dockerCli, opts)
}, },
ValidArgsFunction: cobra.NoFileCompletions, ValidArgsFunction: completion.NoComplete,
DisableFlagsInUseLine: true,
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.BoolVar(&opts.leaveRunning, "leave-running", false, "Leave the container running after checkpoint") flags.BoolVar(&opts.leaveRunning, "leave-running", false, "Leave the container running after checkpoint")
flags.StringVar(&opts.checkpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory") flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory")
return cmd return cmd
} }
func runCreate(ctx context.Context, dockerCLI command.Cli, opts createOptions) error { func runCreate(ctx context.Context, dockerCli command.Cli, opts createOptions) error {
_, err := dockerCLI.Client().CheckpointCreate(ctx, opts.container, client.CheckpointCreateOptions{ err := dockerCli.Client().CheckpointCreate(ctx, opts.container, checkpoint.CreateOptions{
CheckpointID: opts.checkpoint, CheckpointID: opts.checkpoint,
CheckpointDir: opts.checkpointDir, CheckpointDir: opts.checkpointDir,
Exit: !opts.leaveRunning, Exit: !opts.leaveRunning,
@@ -50,6 +50,6 @@ func runCreate(ctx context.Context, dockerCLI command.Cli, opts createOptions) e
return err return err
} }
_, _ = fmt.Fprintln(dockerCLI.Out(), opts.checkpoint) fmt.Fprintf(dockerCli.Out(), "%s\n", opts.checkpoint)
return nil return nil
} }
+29 -44
View File
@@ -1,14 +1,13 @@
package checkpoint package checkpoint
import ( import (
"errors"
"io" "io"
"strconv"
"strings" "strings"
"testing" "testing"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/moby/moby/client" "github.com/docker/docker/api/types/checkpoint"
"github.com/pkg/errors"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp" is "gotest.tools/v3/assert/cmp"
) )
@@ -16,21 +15,21 @@ import (
func TestCheckpointCreateErrors(t *testing.T) { func TestCheckpointCreateErrors(t *testing.T) {
testCases := []struct { testCases := []struct {
args []string args []string
checkpointCreateFunc func(container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error) checkpointCreateFunc func(container string, options checkpoint.CreateOptions) error
expectedError string expectedError string
}{ }{
{ {
args: []string{"too-few-arguments"}, args: []string{"too-few-arguments"},
expectedError: "requires 2 arguments", expectedError: "requires exactly 2 arguments",
}, },
{ {
args: []string{"too", "many", "arguments"}, args: []string{"too", "many", "arguments"},
expectedError: "requires 2 arguments", expectedError: "requires exactly 2 arguments",
}, },
{ {
args: []string{"foo", "bar"}, args: []string{"foo", "bar"},
checkpointCreateFunc: func(container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error) { checkpointCreateFunc: func(container string, options checkpoint.CreateOptions) error {
return client.CheckpointCreateResult{}, errors.New("error creating checkpoint for container foo") return errors.Errorf("error creating checkpoint for container foo")
}, },
expectedError: "error creating checkpoint for container foo", expectedError: "error creating checkpoint for container foo",
}, },
@@ -43,45 +42,31 @@ func TestCheckpointCreateErrors(t *testing.T) {
cmd := newCreateCommand(cli) cmd := newCreateCommand(cli)
cmd.SetArgs(tc.args) cmd.SetArgs(tc.args)
cmd.SetOut(io.Discard) cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError) assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
} }
} }
func TestCheckpointCreateWithOptions(t *testing.T) { func TestCheckpointCreateWithOptions(t *testing.T) {
const ( var containerID, checkpointID, checkpointDir string
containerName = "container-foo" var exit bool
checkpointName = "checkpoint-bar" cli := test.NewFakeCli(&fakeClient{
checkpointDir = "/dir/foo" checkpointCreateFunc: func(container string, options checkpoint.CreateOptions) error {
) containerID = container
checkpointID = options.CheckpointID
for _, tc := range []bool{true, false} { checkpointDir = options.CheckpointDir
leaveRunning := strconv.FormatBool(tc) exit = options.Exit
t.Run("leave-running="+leaveRunning, func(t *testing.T) { return nil
var actualContainerName string },
var actualOptions client.CheckpointCreateOptions })
cli := test.NewFakeCli(&fakeClient{ cmd := newCreateCommand(cli)
checkpointCreateFunc: func(container string, options client.CheckpointCreateOptions) (client.CheckpointCreateResult, error) { cp := "checkpoint-bar"
actualContainerName = container cmd.SetArgs([]string{"container-foo", cp})
actualOptions = options cmd.Flags().Set("leave-running", "true")
return client.CheckpointCreateResult{}, nil cmd.Flags().Set("checkpoint-dir", "/dir/foo")
}, assert.NilError(t, cmd.Execute())
}) assert.Check(t, is.Equal("container-foo", containerID))
cmd := newCreateCommand(cli) assert.Check(t, is.Equal(cp, checkpointID))
cmd.SetOut(io.Discard) assert.Check(t, is.Equal("/dir/foo", checkpointDir))
cmd.SetErr(io.Discard) assert.Check(t, is.Equal(false, exit))
cmd.SetArgs([]string{containerName, checkpointName}) assert.Check(t, is.Equal(cp, strings.TrimSpace(cli.OutBuffer().String())))
assert.Check(t, cmd.Flags().Set("leave-running", leaveRunning))
assert.Check(t, cmd.Flags().Set("checkpoint-dir", checkpointDir))
assert.NilError(t, cmd.Execute())
assert.Check(t, is.Equal(actualContainerName, containerName))
expected := client.CheckpointCreateOptions{
CheckpointID: checkpointName,
CheckpointDir: checkpointDir,
Exit: !tc,
}
assert.Check(t, is.Equal(actualOptions, expected))
assert.Check(t, is.Equal(strings.TrimSpace(cli.OutBuffer().String()), checkpointName))
})
}
} }
+16 -14
View File
@@ -2,7 +2,7 @@ package checkpoint
import ( import (
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
"github.com/moby/moby/api/types/checkpoint" "github.com/docker/docker/api/types/checkpoint"
) )
const ( const (
@@ -10,31 +10,25 @@ const (
checkpointNameHeader = "CHECKPOINT NAME" checkpointNameHeader = "CHECKPOINT NAME"
) )
// newFormat returns a format for use with a checkpointContext. // NewFormat returns a format for use with a checkpoint Context
func newFormat(source string) formatter.Format { func NewFormat(source string) formatter.Format {
if source == formatter.TableFormatKey { if source == formatter.TableFormatKey {
return defaultCheckpointFormat return defaultCheckpointFormat
} }
return formatter.Format(source) return formatter.Format(source)
} }
// formatWrite writes formatted checkpoints using the Context // FormatWrite writes formatted checkpoints using the Context
func formatWrite(fmtCtx formatter.Context, checkpoints []checkpoint.Summary) error { func FormatWrite(ctx formatter.Context, checkpoints []checkpoint.Summary) error {
cpContext := &checkpointContext{ render := func(format func(subContext formatter.SubContext) error) error {
HeaderContext: formatter.HeaderContext{
Header: formatter.SubHeaderContext{
"Name": checkpointNameHeader,
},
},
}
return fmtCtx.Write(cpContext, func(format func(subContext formatter.SubContext) error) error {
for _, cp := range checkpoints { for _, cp := range checkpoints {
if err := format(&checkpointContext{c: cp}); err != nil { if err := format(&checkpointContext{c: cp}); err != nil {
return err return err
} }
} }
return nil return nil
}) }
return ctx.Write(newCheckpointContext(), render)
} }
type checkpointContext struct { type checkpointContext struct {
@@ -42,6 +36,14 @@ type checkpointContext struct {
c checkpoint.Summary c checkpoint.Summary
} }
func newCheckpointContext() *checkpointContext {
cpCtx := checkpointContext{}
cpCtx.Header = formatter.SubHeaderContext{
"Name": checkpointNameHeader,
}
return &cpCtx
}
func (c *checkpointContext) MarshalJSON() ([]byte, error) { func (c *checkpointContext) MarshalJSON() ([]byte, error) {
return formatter.MarshalJSON(c) return formatter.MarshalJSON(c)
} }
+5 -5
View File
@@ -5,7 +5,7 @@ import (
"testing" "testing"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
"github.com/moby/moby/api/types/checkpoint" "github.com/docker/docker/api/types/checkpoint"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
) )
@@ -15,7 +15,7 @@ func TestCheckpointContextFormatWrite(t *testing.T) {
expected string expected string
}{ }{
{ {
formatter.Context{Format: newFormat(defaultCheckpointFormat)}, formatter.Context{Format: NewFormat(defaultCheckpointFormat)},
`CHECKPOINT NAME `CHECKPOINT NAME
checkpoint-1 checkpoint-1
checkpoint-2 checkpoint-2
@@ -23,14 +23,14 @@ checkpoint-3
`, `,
}, },
{ {
formatter.Context{Format: newFormat("{{.Name}}")}, formatter.Context{Format: NewFormat("{{.Name}}")},
`checkpoint-1 `checkpoint-1
checkpoint-2 checkpoint-2
checkpoint-3 checkpoint-3
`, `,
}, },
{ {
formatter.Context{Format: newFormat("{{.Name}}:")}, formatter.Context{Format: NewFormat("{{.Name}}:")},
`checkpoint-1: `checkpoint-1:
checkpoint-2: checkpoint-2:
checkpoint-3: checkpoint-3:
@@ -41,7 +41,7 @@ checkpoint-3:
for _, testcase := range cases { for _, testcase := range cases {
out := bytes.NewBufferString("") out := bytes.NewBufferString("")
testcase.context.Output = out testcase.context.Output = out
err := formatWrite(testcase.context, []checkpoint.Summary{ err := FormatWrite(testcase.context, []checkpoint.Summary{
{Name: "checkpoint-1"}, {Name: "checkpoint-1"},
{Name: "checkpoint-2"}, {Name: "checkpoint-2"},
{Name: "checkpoint-3"}, {Name: "checkpoint-3"},
+10 -11
View File
@@ -7,7 +7,7 @@ import (
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
"github.com/moby/moby/client" "github.com/docker/docker/api/types/checkpoint"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -15,7 +15,7 @@ type listOptions struct {
checkpointDir string checkpointDir string
} }
func newListCommand(dockerCLI command.Cli) *cobra.Command { func newListCommand(dockerCli command.Cli) *cobra.Command {
var opts listOptions var opts listOptions
cmd := &cobra.Command{ cmd := &cobra.Command{
@@ -24,20 +24,19 @@ func newListCommand(dockerCLI command.Cli) *cobra.Command {
Short: "List checkpoints for a container", Short: "List checkpoints for a container",
Args: cli.ExactArgs(1), Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runList(cmd.Context(), dockerCLI, args[0], opts) return runList(cmd.Context(), dockerCli, args[0], opts)
}, },
ValidArgsFunction: completion.ContainerNames(dockerCLI, false), ValidArgsFunction: completion.ContainerNames(dockerCli, false),
DisableFlagsInUseLine: true,
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.StringVar(&opts.checkpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory") flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory")
return cmd return cmd
} }
func runList(ctx context.Context, dockerCLI command.Cli, container string, opts listOptions) error { func runList(ctx context.Context, dockerCli command.Cli, container string, opts listOptions) error {
checkpoints, err := dockerCLI.Client().CheckpointList(ctx, container, client.CheckpointListOptions{ checkpoints, err := dockerCli.Client().CheckpointList(ctx, container, checkpoint.ListOptions{
CheckpointDir: opts.checkpointDir, CheckpointDir: opts.checkpointDir,
}) })
if err != nil { if err != nil {
@@ -45,8 +44,8 @@ func runList(ctx context.Context, dockerCLI command.Cli, container string, opts
} }
cpCtx := formatter.Context{ cpCtx := formatter.Context{
Output: dockerCLI.Out(), Output: dockerCli.Out(),
Format: newFormat(formatter.TableFormatKey), Format: NewFormat(formatter.TableFormatKey),
} }
return formatWrite(cpCtx, checkpoints.Items) return FormatWrite(cpCtx, checkpoints)
} }
+11 -15
View File
@@ -1,13 +1,12 @@
package checkpoint package checkpoint
import ( import (
"errors"
"io" "io"
"testing" "testing"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/moby/moby/api/types/checkpoint" "github.com/docker/docker/api/types/checkpoint"
"github.com/moby/moby/client" "github.com/pkg/errors"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp" is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/golden" "gotest.tools/v3/golden"
@@ -16,21 +15,21 @@ import (
func TestCheckpointListErrors(t *testing.T) { func TestCheckpointListErrors(t *testing.T) {
testCases := []struct { testCases := []struct {
args []string args []string
checkpointListFunc func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error) checkpointListFunc func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error)
expectedError string expectedError string
}{ }{
{ {
args: []string{}, args: []string{},
expectedError: "requires 1 argument", expectedError: "requires exactly 1 argument",
}, },
{ {
args: []string{"too", "many", "arguments"}, args: []string{"too", "many", "arguments"},
expectedError: "requires 1 argument", expectedError: "requires exactly 1 argument",
}, },
{ {
args: []string{"foo"}, args: []string{"foo"},
checkpointListFunc: func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error) { checkpointListFunc: func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) {
return client.CheckpointListResult{}, errors.New("error getting checkpoints for container foo") return []checkpoint.Summary{}, errors.Errorf("error getting checkpoints for container foo")
}, },
expectedError: "error getting checkpoints for container foo", expectedError: "error getting checkpoints for container foo",
}, },
@@ -43,7 +42,6 @@ func TestCheckpointListErrors(t *testing.T) {
cmd := newListCommand(cli) cmd := newListCommand(cli)
cmd.SetArgs(tc.args) cmd.SetArgs(tc.args)
cmd.SetOut(io.Discard) cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError) assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
} }
} }
@@ -51,19 +49,17 @@ func TestCheckpointListErrors(t *testing.T) {
func TestCheckpointListWithOptions(t *testing.T) { func TestCheckpointListWithOptions(t *testing.T) {
var containerID, checkpointDir string var containerID, checkpointDir string
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
checkpointListFunc: func(container string, options client.CheckpointListOptions) (client.CheckpointListResult, error) { checkpointListFunc: func(container string, options checkpoint.ListOptions) ([]checkpoint.Summary, error) {
containerID = container containerID = container
checkpointDir = options.CheckpointDir checkpointDir = options.CheckpointDir
return client.CheckpointListResult{ return []checkpoint.Summary{
Items: []checkpoint.Summary{ {Name: "checkpoint-foo"},
{Name: "checkpoint-foo"},
},
}, nil }, nil
}, },
}) })
cmd := newListCommand(cli) cmd := newListCommand(cli)
cmd.SetArgs([]string{"container-foo"}) cmd.SetArgs([]string{"container-foo"})
assert.Check(t, cmd.Flags().Set("checkpoint-dir", "/dir/foo")) cmd.Flags().Set("checkpoint-dir", "/dir/foo")
assert.NilError(t, cmd.Execute()) assert.NilError(t, cmd.Execute())
assert.Check(t, is.Equal("container-foo", containerID)) assert.Check(t, is.Equal("container-foo", containerID))
assert.Check(t, is.Equal("/dir/foo", checkpointDir)) assert.Check(t, is.Equal("/dir/foo", checkpointDir))
+13 -10
View File
@@ -1,9 +1,11 @@
package checkpoint package checkpoint
import ( import (
"context"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/moby/moby/client" "github.com/docker/docker/api/types/checkpoint"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -11,7 +13,7 @@ type removeOptions struct {
checkpointDir string checkpointDir string
} }
func newRemoveCommand(dockerCLI command.Cli) *cobra.Command { func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
var opts removeOptions var opts removeOptions
cmd := &cobra.Command{ cmd := &cobra.Command{
@@ -20,18 +22,19 @@ func newRemoveCommand(dockerCLI command.Cli) *cobra.Command {
Short: "Remove a checkpoint", Short: "Remove a checkpoint",
Args: cli.ExactArgs(2), Args: cli.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
containerID, checkpointID := args[0], args[1] return runRemove(cmd.Context(), dockerCli, args[0], args[1], opts)
_, err := dockerCLI.Client().CheckpointRemove(cmd.Context(), containerID, client.CheckpointRemoveOptions{
CheckpointID: checkpointID,
CheckpointDir: opts.checkpointDir,
})
return err
}, },
DisableFlagsInUseLine: true,
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.StringVar(&opts.checkpointDir, "checkpoint-dir", "", "Use a custom checkpoint storage directory") flags.StringVarP(&opts.checkpointDir, "checkpoint-dir", "", "", "Use a custom checkpoint storage directory")
return cmd return cmd
} }
func runRemove(ctx context.Context, dockerCli command.Cli, container string, checkpointID string, opts removeOptions) error {
return dockerCli.Client().CheckpointDelete(ctx, container, checkpoint.DeleteOptions{
CheckpointID: checkpointID,
CheckpointDir: opts.checkpointDir,
})
}
+10 -11
View File
@@ -1,12 +1,12 @@
package checkpoint package checkpoint
import ( import (
"errors"
"io" "io"
"testing" "testing"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/moby/moby/client" "github.com/docker/docker/api/types/checkpoint"
"github.com/pkg/errors"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp" is "gotest.tools/v3/assert/cmp"
) )
@@ -14,21 +14,21 @@ import (
func TestCheckpointRemoveErrors(t *testing.T) { func TestCheckpointRemoveErrors(t *testing.T) {
testCases := []struct { testCases := []struct {
args []string args []string
checkpointDeleteFunc func(container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error) checkpointDeleteFunc func(container string, options checkpoint.DeleteOptions) error
expectedError string expectedError string
}{ }{
{ {
args: []string{"too-few-arguments"}, args: []string{"too-few-arguments"},
expectedError: "requires 2 arguments", expectedError: "requires exactly 2 arguments",
}, },
{ {
args: []string{"too", "many", "arguments"}, args: []string{"too", "many", "arguments"},
expectedError: "requires 2 arguments", expectedError: "requires exactly 2 arguments",
}, },
{ {
args: []string{"foo", "bar"}, args: []string{"foo", "bar"},
checkpointDeleteFunc: func(container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error) { checkpointDeleteFunc: func(container string, options checkpoint.DeleteOptions) error {
return client.CheckpointRemoveResult{}, errors.New("error deleting checkpoint") return errors.Errorf("error deleting checkpoint")
}, },
expectedError: "error deleting checkpoint", expectedError: "error deleting checkpoint",
}, },
@@ -41,7 +41,6 @@ func TestCheckpointRemoveErrors(t *testing.T) {
cmd := newRemoveCommand(cli) cmd := newRemoveCommand(cli)
cmd.SetArgs(tc.args) cmd.SetArgs(tc.args)
cmd.SetOut(io.Discard) cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError) assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
} }
} }
@@ -49,16 +48,16 @@ func TestCheckpointRemoveErrors(t *testing.T) {
func TestCheckpointRemoveWithOptions(t *testing.T) { func TestCheckpointRemoveWithOptions(t *testing.T) {
var containerID, checkpointID, checkpointDir string var containerID, checkpointID, checkpointDir string
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
checkpointDeleteFunc: func(container string, options client.CheckpointRemoveOptions) (client.CheckpointRemoveResult, error) { checkpointDeleteFunc: func(container string, options checkpoint.DeleteOptions) error {
containerID = container containerID = container
checkpointID = options.CheckpointID checkpointID = options.CheckpointID
checkpointDir = options.CheckpointDir checkpointDir = options.CheckpointDir
return client.CheckpointRemoveResult{}, nil return nil
}, },
}) })
cmd := newRemoveCommand(cli) cmd := newRemoveCommand(cli)
cmd.SetArgs([]string{"container-foo", "checkpoint-bar"}) cmd.SetArgs([]string{"container-foo", "checkpoint-bar"})
assert.Check(t, cmd.Flags().Set("checkpoint-dir", "/dir/foo")) cmd.Flags().Set("checkpoint-dir", "/dir/foo")
assert.NilError(t, cmd.Execute()) assert.NilError(t, cmd.Execute())
assert.Check(t, is.Equal("container-foo", containerID)) assert.Check(t, is.Equal("container-foo", containerID))
assert.Check(t, is.Equal("checkpoint-bar", checkpointID)) assert.Check(t, is.Equal("checkpoint-bar", checkpointID))
+114 -210
View File
@@ -1,14 +1,14 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.26 //go:build go1.19
package command package command
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
"path/filepath"
"runtime" "runtime"
"strconv" "strconv"
"sync" "sync"
@@ -21,12 +21,21 @@ import (
"github.com/docker/cli/cli/context/store" "github.com/docker/cli/cli/context/store"
"github.com/docker/cli/cli/debug" "github.com/docker/cli/cli/debug"
cliflags "github.com/docker/cli/cli/flags" cliflags "github.com/docker/cli/cli/flags"
manifeststore "github.com/docker/cli/cli/manifest/store"
registryclient "github.com/docker/cli/cli/registry/client"
"github.com/docker/cli/cli/streams" "github.com/docker/cli/cli/streams"
"github.com/docker/cli/cli/trust"
"github.com/docker/cli/cli/version" "github.com/docker/cli/cli/version"
dopts "github.com/docker/cli/opts" dopts "github.com/docker/cli/opts"
"github.com/moby/moby/api/types/build" "github.com/docker/docker/api"
"github.com/moby/moby/client" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
"github.com/docker/go-connections/tlsconfig"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
notaryclient "github.com/theupdateframework/notary/client"
) )
const defaultInitTimeout = 2 * time.Second const defaultInitTimeout = 2 * time.Second
@@ -35,7 +44,7 @@ const defaultInitTimeout = 2 * time.Second
type Streams interface { type Streams interface {
In() *streams.In In() *streams.In
Out() *streams.Out Out() *streams.Out
Err() *streams.Out Err() io.Writer
} }
// Cli represents the docker command line client. // Cli represents the docker command line client.
@@ -43,45 +52,49 @@ type Cli interface {
Client() client.APIClient Client() client.APIClient
Streams Streams
SetIn(in *streams.In) SetIn(in *streams.In)
config.Provider Apply(ops ...CLIOption) error
ConfigFile() *configfile.ConfigFile
ServerInfo() ServerInfo ServerInfo() ServerInfo
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
DefaultVersion() string
CurrentVersion() string CurrentVersion() string
ManifestStore() manifeststore.Store
RegistryClient(bool) registryclient.RegistryClient
ContentTrustEnabled() bool
BuildKitEnabled() (bool, error) BuildKitEnabled() (bool, error)
ContextStore() store.Store ContextStore() store.Store
CurrentContext() string CurrentContext() string
DockerEndpoint() docker.Endpoint DockerEndpoint() docker.Endpoint
TelemetryClient
} }
// DockerCli is an instance the docker command line client. // DockerCli is an instance the docker command line client.
// Instances of the client should be created using the [NewDockerCli] // Instances of the client can be returned from NewDockerCli.
// constructor to make sure they are properly initialized with defaults
// set.
type DockerCli struct { type DockerCli struct {
configFile *configfile.ConfigFile configFile *configfile.ConfigFile
options *cliflags.ClientOptions options *cliflags.ClientOptions
clientOpts []client.Opt
in *streams.In in *streams.In
out *streams.Out out *streams.Out
err *streams.Out err io.Writer
client client.APIClient client client.APIClient
serverInfo ServerInfo serverInfo ServerInfo
contentTrust bool
contextStore store.Store contextStore store.Store
currentContext string currentContext string
contextResolver func(*DockerCli) (string, error)
init sync.Once init sync.Once
initErr error initErr error
dockerEndpoint docker.Endpoint dockerEndpoint docker.Endpoint
contextStoreConfig *store.Config contextStoreConfig store.Config
initTimeout time.Duration initTimeout time.Duration
res telemetryResource
// baseCtx is the base context used for internal operations. In the future // baseCtx is the base context used for internal operations. In the future
// this may be replaced by explicitly passing a context to functions that // this may be replaced by explicitly passing a context to functions that
// need it. // need it.
baseCtx context.Context baseCtx context.Context
}
enableGlobalMeter, enableGlobalTracer bool // DefaultVersion returns api.defaultVersion.
func (cli *DockerCli) DefaultVersion() string {
return api.DefaultVersion
} }
// CurrentVersion returns the API version currently negotiated, or the default // CurrentVersion returns the API version currently negotiated, or the default
@@ -89,7 +102,7 @@ type DockerCli struct {
func (cli *DockerCli) CurrentVersion() string { func (cli *DockerCli) CurrentVersion() string {
_ = cli.initialize() _ = cli.initialize()
if cli.client == nil { if cli.client == nil {
return client.MaxAPIVersion return api.DefaultVersion
} }
return cli.client.ClientVersion() return cli.client.ClientVersion()
} }
@@ -97,7 +110,7 @@ func (cli *DockerCli) CurrentVersion() string {
// Client returns the APIClient // Client returns the APIClient
func (cli *DockerCli) Client() client.APIClient { func (cli *DockerCli) Client() client.APIClient {
if err := cli.initialize(); err != nil { if err := cli.initialize(); err != nil {
_, _ = fmt.Fprintln(cli.Err(), "Failed to initialize:", err) _, _ = fmt.Fprintf(cli.Err(), "Failed to initialize: %s\n", err)
os.Exit(1) os.Exit(1)
} }
return cli.client return cli.client
@@ -109,7 +122,7 @@ func (cli *DockerCli) Out() *streams.Out {
} }
// Err returns the writer used for stderr // Err returns the writer used for stderr
func (cli *DockerCli) Err() *streams.Out { func (cli *DockerCli) Err() io.Writer {
return cli.err return cli.err
} }
@@ -148,13 +161,19 @@ func (cli *DockerCli) ServerInfo() ServerInfo {
return cli.serverInfo return cli.serverInfo
} }
// ContentTrustEnabled returns whether content trust has been enabled by an
// environment variable.
func (cli *DockerCli) ContentTrustEnabled() bool {
return cli.contentTrust
}
// BuildKitEnabled returns buildkit is enabled or not. // BuildKitEnabled returns buildkit is enabled or not.
func (cli *DockerCli) BuildKitEnabled() (bool, error) { func (cli *DockerCli) BuildKitEnabled() (bool, error) {
// use DOCKER_BUILDKIT env var value if set and not empty // use DOCKER_BUILDKIT env var value if set and not empty
if v := os.Getenv("DOCKER_BUILDKIT"); v != "" { if v := os.Getenv("DOCKER_BUILDKIT"); v != "" {
enabled, err := strconv.ParseBool(v) enabled, err := strconv.ParseBool(v)
if err != nil { if err != nil {
return false, fmt.Errorf("DOCKER_BUILDKIT environment variable expects boolean value: %w", err) return false, errors.Wrap(err, "DOCKER_BUILDKIT environment variable expects boolean value")
} }
return enabled, nil return enabled, nil
} }
@@ -163,54 +182,37 @@ func (cli *DockerCli) BuildKitEnabled() (bool, error) {
if _, ok := aliasMap["builder"]; ok { if _, ok := aliasMap["builder"]; ok {
return true, nil return true, nil
} }
// otherwise, assume BuildKit is enabled but
si := cli.ServerInfo() // not if wcow reported from server side
if si.BuildkitVersion == build.BuilderBuildKit { return cli.ServerInfo().OSType != "windows", nil
// The daemon advertised BuildKit as the preferred builder; this may
// be either a Linux daemon or a Windows daemon with experimental
// BuildKit support enabled.
return true, nil
}
// otherwise, assume BuildKit is enabled for Linux, but disabled for
// Windows / WCOW, which does not yet support BuildKit by default.
return si.OSType != "windows", nil
} }
// HooksEnabled returns whether plugin hooks are enabled. // ManifestStore returns a store for local manifests
func (cli *DockerCli) HooksEnabled() bool { func (cli *DockerCli) ManifestStore() manifeststore.Store {
// use DOCKER_CLI_HOOKS env var value if set and not empty // TODO: support override default location from config file
if v := os.Getenv("DOCKER_CLI_HOOKS"); v != "" { return manifeststore.NewStore(filepath.Join(config.Dir(), "manifests"))
enabled, err := strconv.ParseBool(v) }
if err != nil {
return false // RegistryClient returns a client for communicating with a Docker distribution
} // registry
return enabled func (cli *DockerCli) RegistryClient(allowInsecure bool) registryclient.RegistryClient {
resolver := func(ctx context.Context, index *registry.IndexInfo) registry.AuthConfig {
return ResolveAuthConfig(cli.ConfigFile(), index)
} }
// legacy support DOCKER_CLI_HINTS env var return registryclient.NewRegistryClient(resolver, UserAgent(), allowInsecure)
if v := os.Getenv("DOCKER_CLI_HINTS"); v != "" { }
enabled, err := strconv.ParseBool(v)
if err != nil { // WithInitializeClient is passed to DockerCli.Initialize by callers who wish to set a particular API Client for use by the CLI.
return false func WithInitializeClient(makeClient func(dockerCli *DockerCli) (client.APIClient, error)) CLIOption {
} return func(dockerCli *DockerCli) error {
return enabled var err error
dockerCli.client, err = makeClient(dockerCli)
return err
} }
featuresMap := cli.ConfigFile().Features
if v, ok := featuresMap["hooks"]; ok {
enabled, err := strconv.ParseBool(v)
if err != nil {
return false
}
return enabled
}
// default to false
return false
} }
// Initialize the dockerCli runs initialization that must happen after command // Initialize the dockerCli runs initialization that must happen after command
// line flags are parsed. // line flags are parsed.
//
//nolint:gocyclo
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption) error { func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption) error {
for _, o := range ops { for _, o := range ops {
if err := o(cli); err != nil { if err := o(cli); err != nil {
@@ -227,93 +229,42 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption)
debug.Enable() debug.Enable()
} }
if opts.Context != "" && len(opts.Hosts) > 0 { if opts.Context != "" && len(opts.Hosts) > 0 {
return errors.New("conflicting options: cannot specify both --host and --context") return errors.New("conflicting options: either specify --host or --context, not both")
}
if cli.contextStoreConfig == nil {
// This path can be hit when calling Initialize on a DockerCli that's
// not constructed through [NewDockerCli]. Using the default context
// store without a config set will result in Endpoints from contexts
// not being type-mapped correctly, and used as a generic "map[string]any",
// instead of a [docker.EndpointMeta].
//
// When looking up the API endpoint (using [EndpointFromContext]), no
// endpoint will be found, and a default, empty endpoint will be used
// instead which in its turn, causes newAPIClientFromEndpoint to
// be initialized with the default config instead of settings for
// the current context (which may mean; connecting with the wrong
// endpoint and/or TLS Config to be missing).
//
// [EndpointFromContext]: https://github.com/docker/cli/blob/33494921b80fd0b5a06acc3a34fa288de4bb2e6b/cli/context/docker/load.go#L139-L149
if err := WithDefaultContextStoreConfig()(cli); err != nil {
return err
}
} }
cli.options = opts cli.options = opts
cli.configFile = config.LoadDefaultConfigFile(cli.err) cli.configFile = config.LoadDefaultConfigFile(cli.err)
cli.currentContext = resolveContextName(cli.options, cli.configFile) cli.currentContext = resolveContextName(cli.options, cli.configFile)
cli.contextStore = &ContextStoreWithDefault{ cli.contextStore = &ContextStoreWithDefault{
Store: store.New(config.ContextStoreDir(), *cli.contextStoreConfig), Store: store.New(config.ContextStoreDir(), cli.contextStoreConfig),
Resolver: func() (*DefaultContext, error) { Resolver: func() (*DefaultContext, error) {
return resolveDefaultContext(cli.options, *cli.contextStoreConfig) return ResolveDefaultContext(cli.options, cli.contextStoreConfig)
}, },
} }
if cli.contextResolver != nil {
contextName, err := cli.contextResolver(cli)
if err != nil {
return err
}
if contextName != "" {
cli.currentContext = contextName
}
}
// TODO(krissetto): pass ctx to the funcs instead of using this
if cli.enableGlobalMeter {
cli.createGlobalMeterProvider(cli.baseCtx)
}
if cli.enableGlobalTracer {
cli.createGlobalTracerProvider(cli.baseCtx)
}
filterResourceAttributesEnvvar()
// early return if GODEBUG is already set or the docker context is
// the default context, i.e. is a virtual context where we won't override
// any GODEBUG values.
if v := os.Getenv("GODEBUG"); cli.currentContext == DefaultContextName || v != "" {
return nil
}
meta, err := cli.contextStore.GetMetadata(cli.currentContext)
if err == nil {
setGoDebug(meta)
}
return nil return nil
} }
// NewAPIClientFromFlags creates a new APIClient from command line flags // NewAPIClientFromFlags creates a new APIClient from command line flags
func NewAPIClientFromFlags(opts *cliflags.ClientOptions, configFile *configfile.ConfigFile) (client.APIClient, error) { func NewAPIClientFromFlags(opts *cliflags.ClientOptions, configFile *configfile.ConfigFile) (client.APIClient, error) {
if opts.Context != "" && len(opts.Hosts) > 0 { if opts.Context != "" && len(opts.Hosts) > 0 {
return nil, errors.New("conflicting options: cannot specify both --host and --context") return nil, errors.New("conflicting options: either specify --host or --context, not both")
} }
storeConfig := DefaultContextStoreConfig() storeConfig := DefaultContextStoreConfig()
contextStore := &ContextStoreWithDefault{ contextStore := &ContextStoreWithDefault{
Store: store.New(config.ContextStoreDir(), storeConfig), Store: store.New(config.ContextStoreDir(), storeConfig),
Resolver: func() (*DefaultContext, error) { Resolver: func() (*DefaultContext, error) {
return resolveDefaultContext(opts, storeConfig) return ResolveDefaultContext(opts, storeConfig)
}, },
} }
endpoint, err := resolveDockerEndpoint(contextStore, resolveContextName(opts, configFile)) endpoint, err := resolveDockerEndpoint(contextStore, resolveContextName(opts, configFile))
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to resolve docker endpoint: %w", err) return nil, errors.Wrap(err, "unable to resolve docker endpoint")
} }
return newAPIClientFromEndpoint(endpoint, configFile, client.WithUserAgent(UserAgent())) return newAPIClientFromEndpoint(endpoint, configFile)
} }
func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile, extraOpts ...client.Opt) (client.APIClient, error) { func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile) (client.APIClient, error) {
opts, err := ep.ClientOpts() opts, err := ep.ClientOpts()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -321,24 +272,13 @@ func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigF
if len(configFile.HTTPHeaders) > 0 { if len(configFile.HTTPHeaders) > 0 {
opts = append(opts, client.WithHTTPHeaders(configFile.HTTPHeaders)) opts = append(opts, client.WithHTTPHeaders(configFile.HTTPHeaders))
} }
withCustomHeaders, err := withCustomHeadersFromEnv() opts = append(opts, client.WithUserAgent(UserAgent()))
if err != nil { return client.NewClientWithOpts(opts...)
return nil, err
}
if withCustomHeaders != nil {
opts = append(opts, withCustomHeaders)
}
opts = append(opts, extraOpts...)
apiClient, err := client.New(opts...)
if err != nil {
return nil, err
}
return apiClient, nil
} }
func resolveDockerEndpoint(s store.Reader, contextName string) (docker.Endpoint, error) { func resolveDockerEndpoint(s store.Reader, contextName string) (docker.Endpoint, error) {
if s == nil { if s == nil {
return docker.Endpoint{}, errors.New("no context store initialized") return docker.Endpoint{}, fmt.Errorf("no context store initialized")
} }
ctxMeta, err := s.GetMetadata(contextName) ctxMeta, err := s.GetMetadata(contextName)
if err != nil { if err != nil {
@@ -353,10 +293,7 @@ func resolveDockerEndpoint(s store.Reader, contextName string) (docker.Endpoint,
// Resolve the Docker endpoint for the default context (based on config, env vars and CLI flags) // Resolve the Docker endpoint for the default context (based on config, env vars and CLI flags)
func resolveDefaultDockerEndpoint(opts *cliflags.ClientOptions) (docker.Endpoint, error) { func resolveDefaultDockerEndpoint(opts *cliflags.ClientOptions) (docker.Endpoint, error) {
// defaultToTLS determines whether we should use a TLS host as default host, err := getServerHost(opts.Hosts, opts.TLSOptions)
// if nothing was configured by the user.
defaultToTLS := opts.TLSOptions != nil
host, err := getServerHost(opts.Hosts, defaultToTLS)
if err != nil { if err != nil {
return docker.Endpoint{}, err return docker.Endpoint{}, err
} }
@@ -394,21 +331,29 @@ func (cli *DockerCli) initializeFromClient() {
ctx, cancel := context.WithTimeout(cli.baseCtx, cli.getInitTimeout()) ctx, cancel := context.WithTimeout(cli.baseCtx, cli.getInitTimeout())
defer cancel() defer cancel()
ping, err := cli.client.Ping(ctx, client.PingOptions{ ping, err := cli.client.Ping(ctx)
NegotiateAPIVersion: true,
ForceNegotiate: true,
})
if err != nil { if err != nil {
// Default to true if we fail to connect to daemon // Default to true if we fail to connect to daemon
cli.serverInfo = ServerInfo{HasExperimental: true} cli.serverInfo = ServerInfo{HasExperimental: true}
if ping.APIVersion != "" {
cli.client.NegotiateAPIVersionPing(ping)
}
return return
} }
cli.serverInfo = ServerInfo{ cli.serverInfo = ServerInfo{
HasExperimental: ping.Experimental, HasExperimental: ping.Experimental,
OSType: ping.OSType, OSType: ping.OSType,
BuildkitVersion: ping.BuilderVersion, BuildkitVersion: ping.BuilderVersion,
SwarmStatus: ping.SwarmStatus, SwarmStatus: ping.SwarmStatus,
} }
cli.client.NegotiateAPIVersionPing(ping)
}
// NotaryClient provides a Notary Repository to interact with signed metadata for an image
func (cli *DockerCli) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error) {
return trust.GetNotaryRepository(cli.In(), cli.Out(), UserAgent(), imgRefAndAuth.RepoInfo(), imgRefAndAuth.AuthConfig(), actions...)
} }
// ContextStore returns the ContextStore // ContextStore returns the ContextStore
@@ -478,7 +423,7 @@ func (cli *DockerCli) DockerEndpoint() docker.Endpoint {
if err := cli.initialize(); err != nil { if err := cli.initialize(); err != nil {
// Note that we're not terminating here, as this function may be used // Note that we're not terminating here, as this function may be used
// in cases where we're able to continue. // in cases where we're able to continue.
_, _ = fmt.Fprintln(cli.Err(), cli.initErr) _, _ = fmt.Fprintf(cli.Err(), "%v\n", cli.initErr)
} }
return cli.dockerEndpoint return cli.dockerEndpoint
} }
@@ -491,66 +436,15 @@ func (cli *DockerCli) getDockerEndPoint() (ep docker.Endpoint, err error) {
return resolveDockerEndpoint(cli.contextStore, cn) return resolveDockerEndpoint(cli.contextStore, cn)
} }
// setGoDebug is an escape hatch that sets the GODEBUG environment
// variable value using docker context metadata.
//
// {
// "Name": "my-context",
// "Metadata": { "GODEBUG": "x509negativeserial=1" }
// }
//
// WARNING: Setting x509negativeserial=1 allows Go's x509 library to accept
// X.509 certificates with negative serial numbers.
// This behavior is deprecated and non-compliant with current security
// standards (RFC 5280). Accepting negative serial numbers can introduce
// serious security vulnerabilities, including the risk of certificate
// collision or bypass attacks.
// This option should only be used for legacy compatibility and never in
// production environments.
// Use at your own risk.
func setGoDebug(meta store.Metadata) {
fieldName := "GODEBUG"
godebugEnv := os.Getenv(fieldName)
// early return if GODEBUG is already set. We don't want to override what
// the user already sets.
if godebugEnv != "" {
return
}
var cfg any
var ok bool
switch m := meta.Metadata.(type) {
case DockerContext:
cfg, ok = m.AdditionalFields[fieldName]
if !ok {
return
}
case map[string]any:
cfg, ok = m[fieldName]
if !ok {
return
}
default:
return
}
v, ok := cfg.(string)
if !ok {
return
}
// set the GODEBUG environment variable with whatever was in the context
_ = os.Setenv(fieldName, v)
}
func (cli *DockerCli) initialize() error { func (cli *DockerCli) initialize() error {
cli.init.Do(func() { cli.init.Do(func() {
cli.dockerEndpoint, cli.initErr = cli.getDockerEndPoint() cli.dockerEndpoint, cli.initErr = cli.getDockerEndPoint()
if cli.initErr != nil { if cli.initErr != nil {
cli.initErr = fmt.Errorf("unable to resolve docker endpoint: %w", cli.initErr) cli.initErr = errors.Wrap(cli.initErr, "unable to resolve docker endpoint")
return return
} }
if cli.client == nil { if cli.client == nil {
if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile, cli.clientOpts...); cli.initErr != nil { if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile); cli.initErr != nil {
return return
} }
} }
@@ -562,12 +456,22 @@ func (cli *DockerCli) initialize() error {
return cli.initErr return cli.initErr
} }
// Apply all the operation on the cli
func (cli *DockerCli) Apply(ops ...CLIOption) error {
for _, op := range ops {
if err := op(cli); err != nil {
return err
}
}
return nil
}
// ServerInfo stores details about the supported features and platform of the // ServerInfo stores details about the supported features and platform of the
// server // server
type ServerInfo struct { type ServerInfo struct {
HasExperimental bool HasExperimental bool
OSType string OSType string
BuildkitVersion build.BuilderVersion BuildkitVersion types.BuilderVersion
// SwarmStatus provides information about the current swarm status of the // SwarmStatus provides information about the current swarm status of the
// engine, obtained from the "Swarm" header in the API response. // engine, obtained from the "Swarm" header in the API response.
@@ -576,42 +480,42 @@ type ServerInfo struct {
// in the ping response, or if an error occurred, in which case the client // in the ping response, or if an error occurred, in which case the client
// should use other ways to get the current swarm status, such as the /swarm // should use other ways to get the current swarm status, such as the /swarm
// endpoint. // endpoint.
SwarmStatus *client.SwarmStatus SwarmStatus *swarm.Status
} }
// NewDockerCli returns a DockerCli instance with all operators applied on it. // NewDockerCli returns a DockerCli instance with all operators applied on it.
// It applies by default the standard streams, and the content trust from // It applies by default the standard streams, and the content trust from
// environment. // environment.
func NewDockerCli(ops ...CLIOption) (*DockerCli, error) { func NewDockerCli(ops ...CLIOption) (*DockerCli, error) {
defaultOps := make([]CLIOption, 0, 3+len(ops)) defaultOps := []CLIOption{
defaultOps = append(defaultOps, WithContentTrustFromEnv(),
WithDefaultContextStoreConfig(), WithDefaultContextStoreConfig(),
WithStandardStreams(), WithStandardStreams(),
WithUserAgent(UserAgent()), }
)
ops = append(defaultOps, ops...) ops = append(defaultOps, ops...)
cli := &DockerCli{baseCtx: context.Background()} cli := &DockerCli{baseCtx: context.Background()}
for _, op := range ops { if err := cli.Apply(ops...); err != nil {
if err := op(cli); err != nil { return nil, err
return nil, err
}
} }
return cli, nil return cli, nil
} }
func getServerHost(hosts []string, defaultToTLS bool) (string, error) { func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (string, error) {
var host string
switch len(hosts) { switch len(hosts) {
case 0: case 0:
return dopts.ParseHost(defaultToTLS, os.Getenv(client.EnvOverrideHost)) host = os.Getenv(client.EnvOverrideHost)
case 1: case 1:
return dopts.ParseHost(defaultToTLS, hosts[0]) host = hosts[0]
default: default:
return "", errors.New("specify only one -H") return "", errors.New("Please specify only one -H")
} }
return dopts.ParseHost(tlsOptions != nil, host)
} }
// UserAgent returns the default user agent string used for making API requests. // UserAgent returns the user agent string used for making API requests
func UserAgent() string { func UserAgent() string {
return "Docker-Client/" + version.Version + " (" + runtime.GOOS + ")" return "Docker-Client/" + version.Version + " (" + runtime.GOOS + ")"
} }
+40 -173
View File
@@ -2,24 +2,28 @@ package command
import ( import (
"context" "context"
"encoding/csv"
"errors"
"fmt"
"io" "io"
"net/http"
"os" "os"
"strings" "strconv"
"github.com/docker/cli/cli/streams" "github.com/docker/cli/cli/streams"
"github.com/moby/moby/client" "github.com/docker/docker/client"
"github.com/moby/term" "github.com/moby/term"
) )
// CLIOption is a functional argument to apply options to a [DockerCli]. These // CLIOption applies a modification on a DockerCli.
// options can be passed to [NewDockerCli] to initialize a new CLI, or
// applied with [DockerCli.Initialize] or [DockerCli.Apply].
type CLIOption func(cli *DockerCli) error type CLIOption func(cli *DockerCli) error
// DockerCliOption applies a modification on a DockerCli.
//
// Deprecated: use [CLIOption] instead.
type DockerCliOption = CLIOption
// InitializeOpt is the type of the functional options passed to DockerCli.Initialize
//
// Deprecated: use [CLIOption] instead.
type InitializeOpt = CLIOption
// WithStandardStreams sets a cli in, out and err streams with the standard streams. // WithStandardStreams sets a cli in, out and err streams with the standard streams.
func WithStandardStreams() CLIOption { func WithStandardStreams() CLIOption {
return func(cli *DockerCli) error { return func(cli *DockerCli) error {
@@ -27,7 +31,7 @@ func WithStandardStreams() CLIOption {
stdin, stdout, stderr := term.StdStreams() stdin, stdout, stderr := term.StdStreams()
cli.in = streams.NewIn(stdin) cli.in = streams.NewIn(stdin)
cli.out = streams.NewOut(stdout) cli.out = streams.NewOut(stdout)
cli.err = streams.NewOut(stderr) cli.err = stderr
return nil return nil
} }
} }
@@ -44,9 +48,8 @@ func WithBaseContext(ctx context.Context) CLIOption {
// WithCombinedStreams uses the same stream for the output and error streams. // WithCombinedStreams uses the same stream for the output and error streams.
func WithCombinedStreams(combined io.Writer) CLIOption { func WithCombinedStreams(combined io.Writer) CLIOption {
return func(cli *DockerCli) error { return func(cli *DockerCli) error {
s := streams.NewOut(combined) cli.out = streams.NewOut(combined)
cli.out = s cli.err = combined
cli.err = s
return nil return nil
} }
} }
@@ -70,7 +73,29 @@ func WithOutputStream(out io.Writer) CLIOption {
// WithErrorStream sets a cli error stream. // WithErrorStream sets a cli error stream.
func WithErrorStream(err io.Writer) CLIOption { func WithErrorStream(err io.Writer) CLIOption {
return func(cli *DockerCli) error { return func(cli *DockerCli) error {
cli.err = streams.NewOut(err) cli.err = err
return nil
}
}
// WithContentTrustFromEnv enables content trust on a cli from environment variable DOCKER_CONTENT_TRUST value.
func WithContentTrustFromEnv() CLIOption {
return func(cli *DockerCli) error {
cli.contentTrust = false
if e := os.Getenv("DOCKER_CONTENT_TRUST"); e != "" {
if t, err := strconv.ParseBool(e); t || err != nil {
// treat any other value as true
cli.contentTrust = true
}
}
return nil
}
}
// WithContentTrust enables content trust on a cli.
func WithContentTrust(enabled bool) CLIOption {
return func(cli *DockerCli) error {
cli.contentTrust = enabled
return nil return nil
} }
} }
@@ -78,21 +103,7 @@ func WithErrorStream(err io.Writer) CLIOption {
// WithDefaultContextStoreConfig configures the cli to use the default context store configuration. // WithDefaultContextStoreConfig configures the cli to use the default context store configuration.
func WithDefaultContextStoreConfig() CLIOption { func WithDefaultContextStoreConfig() CLIOption {
return func(cli *DockerCli) error { return func(cli *DockerCli) error {
cfg := DefaultContextStoreConfig() cli.contextStoreConfig = DefaultContextStoreConfig()
cli.contextStoreConfig = &cfg
return nil
}
}
// WithContextResolver overrides context selection during [DockerCli.Initialize].
// The resolver runs after configuration and the context store are loaded, but
// before telemetry and endpoint initialization.
// An empty result preserves normal context selection; an error aborts
// initialization.
// The resolver must not initialize the API client.
func WithContextResolver(resolve func(*DockerCli) (string, error)) CLIOption {
return func(cli *DockerCli) error {
cli.contextResolver = resolve
return nil return nil
} }
} }
@@ -104,147 +115,3 @@ func WithAPIClient(c client.APIClient) CLIOption {
return nil return nil
} }
} }
// WithInitializeClient is passed to [DockerCli.Initialize] to initialize
// an API Client for use by the CLI.
func WithInitializeClient(makeClient func(*DockerCli) (client.APIClient, error)) CLIOption {
return func(cli *DockerCli) error {
c, err := makeClient(cli)
if err != nil {
return err
}
return WithAPIClient(c)(cli)
}
}
// WithAPIClientOptions configures additional [client.Opt] to use when
// initializing the API client. These options have no effect if a custom
// client is set (through [WithAPIClient] or [WithInitializeClient]).
func WithAPIClientOptions(c ...client.Opt) CLIOption {
return func(cli *DockerCli) error {
cli.clientOpts = append(cli.clientOpts, c...)
return nil
}
}
// envOverrideHTTPHeaders is the name of the environment-variable that can be
// used to set custom HTTP headers to be sent by the client. This environment
// variable is the equivalent to the HttpHeaders field in the configuration
// file.
//
// WARNING: If both config and environment-variable are set, the environment
// variable currently overrides all headers set in the configuration file.
// This behavior may change in a future update, as we are considering the
// environment variable to be appending to existing headers (and to only
// override headers with the same name).
//
// While this env-var allows for custom headers to be set, it does not allow
// for built-in headers (such as "User-Agent", if set) to be overridden.
// Also see [client.WithHTTPHeaders] and [client.WithUserAgent].
//
// This environment variable can be used in situations where headers must be
// set for a specific invocation of the CLI, but should not be set by default,
// and therefore cannot be set in the config-file.
//
// envOverrideHTTPHeaders accepts a comma-separated (CSV) list of key=value pairs,
// where key must be a non-empty, valid MIME header format. Whitespaces surrounding
// the key are trimmed, and the key is normalised. Whitespaces in values are
// preserved, but "key=value" pairs with an empty value (e.g. "key=") are ignored.
// Tuples without a "=" produce an error.
//
// It follows CSV rules for escaping, allowing "key=value" pairs to be quoted
// if they must contain commas, which allows for multiple values for a single
// header to be set. If a key is repeated in the list, later values override
// prior values.
//
// For example, the following value:
//
// one=one-value,"two=two,value","three= a value with whitespace ",four=,five=five=one,five=five-two
//
// Produces four headers (four is omitted as it has an empty value set):
//
// - one (value is "one-value")
// - two (value is "two,value")
// - three (value is " a value with whitespace ")
// - five (value is "five-two", the later value has overridden the prior value)
const envOverrideHTTPHeaders = "DOCKER_CUSTOM_HEADERS"
// withCustomHeadersFromEnv overriding custom HTTP headers to be sent by the
// client through the [envOverrideHTTPHeaders] environment-variable. This
// environment variable is the equivalent to the HttpHeaders field in the
// configuration file.
//
// WARNING: If both config and environment-variable are set, the environment-
// variable currently overrides all headers set in the configuration file.
// This behavior may change in a future update, as we are considering the
// environment-variable to be appending to existing headers (and to only
// override headers with the same name).
//
// TODO(thaJeztah): this is a client Option, and should be moved to the client. It is non-exported for that reason.
func withCustomHeadersFromEnv() (client.Opt, error) {
value := os.Getenv(envOverrideHTTPHeaders)
if value == "" {
return nil, nil
}
csvReader := csv.NewReader(strings.NewReader(value))
fields, err := csvReader.Read()
if err != nil {
return nil, invalidParameter(fmt.Errorf(
"failed to parse custom headers from %s environment variable: value must be formatted as comma-separated key=value pairs",
envOverrideHTTPHeaders,
))
}
if len(fields) == 0 {
return nil, nil
}
env := map[string]string{}
for _, kv := range fields {
k, v, hasValue := strings.Cut(kv, "=")
// Only strip whitespace in keys; preserve whitespace in values.
k = strings.TrimSpace(k)
if k == "" {
return nil, invalidParameter(fmt.Errorf(
`failed to set custom headers from %s environment variable: value contains a key=value pair with an empty key: '%s'`,
envOverrideHTTPHeaders, kv,
))
}
// We don't currently allow empty key=value pairs, and produce an error.
// This is something we could allow in future (e.g. to read value
// from an environment variable with the same name). In the meantime,
// produce an error to prevent users from depending on this.
if !hasValue {
return nil, invalidParameter(fmt.Errorf(
`failed to set custom headers from %s environment variable: missing "=" in key=value pair: '%s'`,
envOverrideHTTPHeaders, kv,
))
}
env[http.CanonicalHeaderKey(k)] = v
}
if len(env) == 0 {
// We should probably not hit this case, as we don't skip values
// (only return errors), but we don't want to discard existing
// headers with an empty set.
return nil, nil
}
// TODO(thaJeztah): add a client.WithExtraHTTPHeaders() function to allow these headers to be _added_ to existing ones, instead of _replacing_
// see https://github.com/docker/cli/pull/5098#issuecomment-2147403871 (when updating, also update the WARNING in the function and env-var GoDoc)
return client.WithHTTPHeaders(env), nil
}
// WithUserAgent configures the User-Agent string for cli HTTP requests.
func WithUserAgent(userAgent string) CLIOption {
return func(cli *DockerCli) error {
if userAgent == "" {
return errors.New("user agent cannot be blank")
}
cli.clientOpts = append(cli.clientOpts, client.WithUserAgent(userAgent))
return nil
}
}
+28
View File
@@ -0,0 +1,28 @@
package command
import (
"os"
"testing"
"gotest.tools/v3/assert"
)
func contentTrustEnabled(t *testing.T) bool {
t.Helper()
var cli DockerCli
assert.NilError(t, WithContentTrustFromEnv()(&cli))
return cli.contentTrust
}
// NB: Do not t.Parallel() this test -- it messes with the process environment.
func TestWithContentTrustFromEnv(t *testing.T) {
const envvar = "DOCKER_CONTENT_TRUST"
t.Setenv(envvar, "true")
assert.Check(t, contentTrustEnabled(t))
t.Setenv(envvar, "false")
assert.Check(t, !contentTrustEnabled(t))
t.Setenv(envvar, "invalid")
assert.Check(t, contentTrustEnabled(t))
os.Unsetenv(envvar)
assert.Check(t, !contentTrustEnabled(t))
}
+113 -307
View File
@@ -3,7 +3,6 @@ package command
import ( import (
"bytes" "bytes"
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"net" "net"
@@ -18,10 +17,13 @@ import (
"github.com/docker/cli/cli/config" "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/context/store"
"github.com/docker/cli/cli/flags" "github.com/docker/cli/cli/flags"
"github.com/moby/moby/client" "github.com/docker/docker/api"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/pkg/errors"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
"gotest.tools/v3/fs"
) )
func TestNewAPIClientFromFlags(t *testing.T) { func TestNewAPIClientFromFlags(t *testing.T) {
@@ -33,7 +35,7 @@ func TestNewAPIClientFromFlags(t *testing.T) {
apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{}) apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{})
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, apiClient.DaemonHost(), host) assert.Equal(t, apiClient.DaemonHost(), host)
assert.Equal(t, apiClient.ClientVersion(), client.MaxAPIVersion) assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
} }
func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) { func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) {
@@ -46,7 +48,7 @@ func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) {
apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{}) apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{})
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, apiClient.DaemonHost(), slug+host) assert.Equal(t, apiClient.DaemonHost(), slug+host)
assert.Equal(t, apiClient.ClientVersion(), client.MaxAPIVersion) assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
} }
func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) { func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) {
@@ -70,7 +72,7 @@ func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) {
apiClient, err := NewAPIClientFromFlags(opts, configFile) apiClient, err := NewAPIClientFromFlags(opts, configFile)
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, apiClient.DaemonHost(), host) assert.Equal(t, apiClient.DaemonHost(), host)
assert.Equal(t, apiClient.ClientVersion(), client.MaxAPIVersion) assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion)
// verify User-Agent is not appended to the configfile. see https://github.com/docker/cli/pull/2756 // verify User-Agent is not appended to the configfile. see https://github.com/docker/cli/pull/2756
assert.DeepEqual(t, configFile.HTTPHeaders, map[string]string{"My-Header": "Custom-Value"}) assert.DeepEqual(t, configFile.HTTPHeaders, map[string]string{"My-Header": "Custom-Value"})
@@ -79,49 +81,13 @@ func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) {
"My-Header": "Custom-Value", "My-Header": "Custom-Value",
"User-Agent": UserAgent(), "User-Agent": UserAgent(),
} }
_, err = apiClient.Ping(context.TODO(), client.PingOptions{}) _, err = apiClient.Ping(context.Background())
assert.NilError(t, err)
assert.DeepEqual(t, received, expectedHeaders)
}
func TestNewAPIClientFromFlagsWithCustomHeadersFromEnv(t *testing.T) {
var received http.Header
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
received = r.Header.Clone()
_, _ = w.Write([]byte("OK"))
}))
defer ts.Close()
host := strings.Replace(ts.URL, "http://", "tcp://", 1)
opts := &flags.ClientOptions{Hosts: []string{host}}
configFile := &configfile.ConfigFile{
HTTPHeaders: map[string]string{
"My-Header": "Custom-Value from config-file",
},
}
// envOverrideHTTPHeaders should override the HTTPHeaders from the config-file,
// so "My-Header" should not be present.
t.Setenv(envOverrideHTTPHeaders, `one=one-value,"two=two,value",three=,four=four-value,four=four-value-override`)
apiClient, err := NewAPIClientFromFlags(opts, configFile)
assert.NilError(t, err)
assert.Equal(t, apiClient.DaemonHost(), host)
assert.Equal(t, apiClient.ClientVersion(), client.MaxAPIVersion)
expectedHeaders := http.Header{
"One": []string{"one-value"},
"Two": []string{"two,value"},
"Three": []string{""},
"Four": []string{"four-value-override"},
"User-Agent": []string{UserAgent()},
}
_, err = apiClient.Ping(context.TODO(), client.PingOptions{})
assert.NilError(t, err) assert.NilError(t, err)
assert.DeepEqual(t, received, expectedHeaders) assert.DeepEqual(t, received, expectedHeaders)
} }
func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) { func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
const customVersion = "v3.3" customVersion := "v3.3.3"
const expectedVersion = "3.3"
t.Setenv("DOCKER_API_VERSION", customVersion) t.Setenv("DOCKER_API_VERSION", customVersion)
t.Setenv("DOCKER_HOST", ":2375") t.Setenv("DOCKER_HOST", ":2375")
@@ -129,87 +95,75 @@ func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) {
configFile := &configfile.ConfigFile{} configFile := &configfile.ConfigFile{}
apiclient, err := NewAPIClientFromFlags(opts, configFile) apiclient, err := NewAPIClientFromFlags(opts, configFile)
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, apiclient.ClientVersion(), expectedVersion) assert.Equal(t, apiclient.ClientVersion(), customVersion)
}
func TestNewAPIClientFromFlagsWithInvalidAPIVersionFromEnv(t *testing.T) {
t.Setenv("DOCKER_API_VERSION", "1")
t.Setenv("DOCKER_HOST", ":2375")
apiClient, err := NewAPIClientFromFlags(&flags.ClientOptions{}, &configfile.ConfigFile{})
assert.ErrorContains(t, err, "invalid API version")
assert.Check(t, apiClient == nil)
} }
type fakeClient struct { type fakeClient struct {
client.Client client.Client
pingFunc func() (client.PingResult, error) pingFunc func() (types.Ping, error)
version string version string
negotiated bool negotiated bool
} }
func (c *fakeClient) Ping(_ context.Context, options client.PingOptions) (client.PingResult, error) { func (c *fakeClient) Ping(_ context.Context) (types.Ping, error) {
res, err := c.pingFunc() return c.pingFunc()
if options.NegotiateAPIVersion {
if res.APIVersion != "" {
if c.negotiated || options.ForceNegotiate {
c.negotiated = true
}
}
}
return res, err
} }
func (c *fakeClient) ClientVersion() string { func (c *fakeClient) ClientVersion() string {
return c.version return c.version
} }
func (c *fakeClient) NegotiateAPIVersionPing(types.Ping) {
c.negotiated = true
}
func TestInitializeFromClient(t *testing.T) { func TestInitializeFromClient(t *testing.T) {
const defaultVersion = "v1.55" const defaultVersion = "v1.55"
testcases := []struct { testcases := []struct {
doc string doc string
pingFunc func() (client.PingResult, error) pingFunc func() (types.Ping, error)
expectedServer ServerInfo expectedServer ServerInfo
negotiated bool negotiated bool
}{ }{
{ {
doc: "successful ping", doc: "successful ping",
pingFunc: func() (client.PingResult, error) { pingFunc: func() (types.Ping, error) {
return client.PingResult{Experimental: true, OSType: "linux", APIVersion: "v1.44"}, nil return types.Ping{Experimental: true, OSType: "linux", APIVersion: "v1.30"}, nil
}, },
expectedServer: ServerInfo{HasExperimental: true, OSType: "linux"}, expectedServer: ServerInfo{HasExperimental: true, OSType: "linux"},
negotiated: true, negotiated: true,
}, },
{ {
doc: "failed ping, no API version", doc: "failed ping, no API version",
pingFunc: func() (client.PingResult, error) { pingFunc: func() (types.Ping, error) {
return client.PingResult{}, errors.New("failed") return types.Ping{}, errors.New("failed")
}, },
expectedServer: ServerInfo{HasExperimental: true}, expectedServer: ServerInfo{HasExperimental: true},
}, },
{ {
doc: "failed ping, with API version", doc: "failed ping, with API version",
pingFunc: func() (client.PingResult, error) { pingFunc: func() (types.Ping, error) {
return client.PingResult{APIVersion: "v1.44"}, errors.New("failed") return types.Ping{APIVersion: "v1.33"}, errors.New("failed")
}, },
expectedServer: ServerInfo{HasExperimental: true}, expectedServer: ServerInfo{HasExperimental: true},
negotiated: true, negotiated: true,
}, },
} }
for _, tc := range testcases { for _, testcase := range testcases {
t.Run(tc.doc, func(t *testing.T) { testcase := testcase
apiClient := &fakeClient{ t.Run(testcase.doc, func(t *testing.T) {
pingFunc: tc.pingFunc, apiclient := &fakeClient{
pingFunc: testcase.pingFunc,
version: defaultVersion, version: defaultVersion,
} }
cli := &DockerCli{client: apiClient} cli := &DockerCli{client: apiclient}
err := cli.Initialize(flags.NewClientOptions()) err := cli.Initialize(flags.NewClientOptions())
assert.NilError(t, err) assert.NilError(t, err)
assert.DeepEqual(t, cli.ServerInfo(), tc.expectedServer) assert.DeepEqual(t, cli.ServerInfo(), testcase.expectedServer)
assert.Equal(t, apiClient.negotiated, tc.negotiated) assert.Equal(t, apiclient.negotiated, testcase.negotiated)
}) })
} }
} }
@@ -217,100 +171,129 @@ func TestInitializeFromClient(t *testing.T) {
// Makes sure we don't hang forever on the initial connection. // Makes sure we don't hang forever on the initial connection.
// https://github.com/docker/cli/issues/3652 // https://github.com/docker/cli/issues/3652
func TestInitializeFromClientHangs(t *testing.T) { func TestInitializeFromClientHangs(t *testing.T) {
const ( dir := t.TempDir()
// Sized against measured scheduler stalls: socket := filepath.Join(dir, "my.sock")
// under CPU pressure this test sees 40-90ms stalls;
// this should give about 5x headroom.
// See https://github.com/docker/cli/issues/6003.
clientInitTimeout = 500 * time.Millisecond
// This is only a backstop against a genuine hang.
// It should never be reached on a healthy run.
// So, it should be fine to have a lenient timeout here.
waitTimeout = 10 * time.Second
)
tmpDir := t.TempDir()
socket := filepath.Join(tmpDir, "my.sock")
l, err := net.Listen("unix", socket) l, err := net.Listen("unix", socket)
assert.NilError(t, err) assert.NilError(t, err)
// Buffered, so the handler can record receiveReqCh := make(chan bool)
// that it was reached without a reader timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second)
// having to be ready at that instant. defer cancel()
receivedReqCh := make(chan struct{}, 1)
releaseHandlerCh := make(chan struct{})
// Simulate a server that hangs on connections. // Simulate a server that hangs on connections.
ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
select { select {
case receivedReqCh <- struct{}{}: case <-timeoutCtx.Done():
default: case receiveReqCh <- true: // Blocks until someone receives on the channel.
} }
<-releaseHandlerCh _, _ = w.Write([]byte("OK"))
})) }))
ts.Listener = l ts.Listener = l
ts.Start() ts.Start()
t.Cleanup(func() { defer ts.Close()
close(releaseHandlerCh)
ts.Close()
})
opts := &flags.ClientOptions{Hosts: []string{"unix://" + socket}} opts := &flags.ClientOptions{Hosts: []string{fmt.Sprintf("unix://%s", socket)}}
configFile := &configfile.ConfigFile{} configFile := &configfile.ConfigFile{}
apiClient, err := NewAPIClientFromFlags(opts, configFile) apiClient, err := NewAPIClientFromFlags(opts, configFile)
assert.NilError(t, err) assert.NilError(t, err)
initErrCh := make(chan error, 1) initializedCh := make(chan bool)
go func() { go func() {
cli := &DockerCli{client: apiClient, initTimeout: clientInitTimeout} cli := &DockerCli{client: apiClient, initTimeout: time.Millisecond}
err := cli.Initialize(flags.NewClientOptions()) err := cli.Initialize(flags.NewClientOptions())
assert.Check(t, err)
cli.CurrentVersion() cli.CurrentVersion()
initErrCh <- err close(initializedCh)
}() }()
select { select {
case err := <-initErrCh: case <-timeoutCtx.Done():
assert.Check(t, err)
case <-time.After(waitTimeout):
t.Fatal("timeout waiting for initialization to complete") t.Fatal("timeout waiting for initialization to complete")
case <-initializedCh:
} }
select { select {
case <-receivedReqCh: case <-timeoutCtx.Done():
case <-time.After(waitTimeout):
t.Fatal("server never received an init request") t.Fatal("server never received an init request")
case <-receiveReqCh:
}
}
// The CLI no longer disables/hides experimental CLI features, however, we need
// to verify that existing configuration files do not break
func TestExperimentalCLI(t *testing.T) {
defaultVersion := "v1.55"
testcases := []struct {
doc string
configfile string
}{
{
doc: "default",
configfile: `{}`,
},
{
doc: "experimental",
configfile: `{
"experimental": "enabled"
}`,
},
}
for _, testcase := range testcases {
testcase := testcase
t.Run(testcase.doc, func(t *testing.T) {
dir := fs.NewDir(t, testcase.doc, fs.WithFile("config.json", testcase.configfile))
defer dir.Remove()
apiclient := &fakeClient{
version: defaultVersion,
pingFunc: func() (types.Ping, error) {
return types.Ping{Experimental: true, OSType: "linux", APIVersion: defaultVersion}, nil
},
}
cli := &DockerCli{client: apiclient, err: os.Stderr}
config.SetDir(dir.Path())
err := cli.Initialize(flags.NewClientOptions())
assert.NilError(t, err)
})
} }
} }
func TestNewDockerCliAndOperators(t *testing.T) { func TestNewDockerCliAndOperators(t *testing.T) {
outbuf := bytes.NewBuffer(nil) // Test default operations and also overriding default ones
errbuf := bytes.NewBuffer(nil)
cli, err := NewDockerCli( cli, err := NewDockerCli(
WithInputStream(io.NopCloser(strings.NewReader("some input"))), WithContentTrust(true),
WithOutputStream(outbuf),
WithErrorStream(errbuf),
) )
assert.NilError(t, err) assert.NilError(t, err)
// Check streams are initialized // Check streams are initialized
assert.Check(t, cli.In() != nil) assert.Check(t, cli.In() != nil)
assert.Check(t, cli.Out() != nil) assert.Check(t, cli.Out() != nil)
assert.Check(t, cli.Err() != nil) assert.Check(t, cli.Err() != nil)
assert.Equal(t, cli.ContentTrustEnabled(), true)
// Apply can modify a dockerCli after construction
inbuf := bytes.NewBuffer([]byte("input"))
outbuf := bytes.NewBuffer(nil)
errbuf := bytes.NewBuffer(nil)
err = cli.Apply(
WithInputStream(io.NopCloser(inbuf)),
WithOutputStream(outbuf),
WithErrorStream(errbuf),
)
assert.NilError(t, err)
// Check input stream
inputStream, err := io.ReadAll(cli.In()) inputStream, err := io.ReadAll(cli.In())
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, string(inputStream), "some input") assert.Equal(t, string(inputStream), "input")
// Check output stream // Check output stream
_, err = fmt.Fprint(cli.Out(), "output") fmt.Fprintf(cli.Out(), "output")
assert.NilError(t, err)
outputStream, err := io.ReadAll(outbuf) outputStream, err := io.ReadAll(outbuf)
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, string(outputStream), "output") assert.Equal(t, string(outputStream), "output")
// Check error stream // Check error stream
_, err = fmt.Fprint(cli.Err(), "error") fmt.Fprintf(cli.Err(), "error")
assert.NilError(t, err)
errStream, err := io.ReadAll(errbuf) errStream, err := io.ReadAll(errbuf)
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, string(errStream), "error") assert.Equal(t, string(errStream), "error")
@@ -319,185 +302,8 @@ func TestNewDockerCliAndOperators(t *testing.T) {
func TestInitializeShouldAlwaysCreateTheContextStore(t *testing.T) { func TestInitializeShouldAlwaysCreateTheContextStore(t *testing.T) {
cli, err := NewDockerCli() cli, err := NewDockerCli()
assert.NilError(t, err) assert.NilError(t, err)
apiClient, err := client.New() assert.NilError(t, cli.Initialize(flags.NewClientOptions(), WithInitializeClient(func(cli *DockerCli) (client.APIClient, error) {
assert.NilError(t, err) return client.NewClientWithOpts()
assert.NilError(t, cli.Initialize(flags.NewClientOptions(), WithAPIClient(apiClient))) })))
assert.Check(t, cli.ContextStore() != nil) assert.Check(t, cli.ContextStore() != nil)
} }
func TestInitializeContextResolver(t *testing.T) {
resolverErr := errors.New("resolver failed")
for _, tc := range []struct {
name string
context string
err error
}{
{name: "resolved context", context: "resolved"},
{name: "normal selection"},
{name: "resolver failure", err: resolverErr},
} {
t.Run(tc.name, func(t *testing.T) {
originalConfigDir := config.Dir()
t.Cleanup(func() { config.SetDir(originalConfigDir) })
config.SetDir(t.TempDir())
configDir := t.TempDir()
cfg := configfile.New(filepath.Join(configDir, config.ConfigFileName))
cfg.Features = map[string]string{"cloud": "foobar"}
assert.NilError(t, cfg.Save())
cli, err := NewDockerCli()
assert.NilError(t, err)
var loadedConfig *configfile.ConfigFile
calls := 0
err = cli.Initialize(&flags.ClientOptions{ConfigDir: configDir, Context: "original"},
WithContextResolver(func(cli *DockerCli) (string, error) {
calls++
assert.Equal(t, config.Dir(), configDir)
loadedConfig = cli.ConfigFile()
assert.Equal(t, loadedConfig.Features["cloud"], "foobar")
assert.Assert(t, cli.ContextStore() != nil)
assert.Assert(t, cli.client == nil)
return tc.context, tc.err
}),
)
assert.Equal(t, calls, 1)
assert.Assert(t, cli.ConfigFile() == loadedConfig)
assert.Assert(t, cli.client == nil)
if tc.err != nil {
assert.ErrorIs(t, err, tc.err)
return
}
assert.NilError(t, err)
wantContext := tc.context
if wantContext == "" {
wantContext = "original"
}
assert.Equal(t, cli.CurrentContext(), wantContext)
})
}
}
func TestHooksEnabled(t *testing.T) {
t.Run("disabled by default", func(t *testing.T) {
// Make sure we don't depend on any existing ~/.docker/config.json
config.SetDir(t.TempDir())
cli, err := NewDockerCli()
assert.NilError(t, err)
assert.Check(t, !cli.HooksEnabled())
})
t.Run("enabled in configFile", func(t *testing.T) {
configFile := `{
"features": {
"hooks": "true"
}}`
config.SetDir(t.TempDir())
err := os.WriteFile(filepath.Join(config.Dir(), "config.json"), []byte(configFile), 0o600)
assert.NilError(t, err)
cli, err := NewDockerCli()
assert.NilError(t, err)
assert.Check(t, cli.HooksEnabled())
})
t.Run("env var overrides configFile", func(t *testing.T) {
configFile := `{
"features": {
"hooks": "true"
}}`
t.Setenv("DOCKER_CLI_HOOKS", "false")
config.SetDir(t.TempDir())
err := os.WriteFile(filepath.Join(config.Dir(), "config.json"), []byte(configFile), 0o600)
assert.NilError(t, err)
cli, err := NewDockerCli()
assert.NilError(t, err)
assert.Check(t, !cli.HooksEnabled())
})
t.Run("legacy env var overrides configFile", func(t *testing.T) {
configFile := `{
"features": {
"hooks": "true"
}}`
t.Setenv("DOCKER_CLI_HINTS", "false")
config.SetDir(t.TempDir())
err := os.WriteFile(filepath.Join(config.Dir(), "config.json"), []byte(configFile), 0o600)
assert.NilError(t, err)
cli, err := NewDockerCli()
assert.NilError(t, err)
assert.Check(t, !cli.HooksEnabled())
})
}
func TestSetGoDebug(t *testing.T) {
t.Run("GODEBUG already set", func(t *testing.T) {
t.Setenv("GODEBUG", "val1,val2")
meta := store.Metadata{}
setGoDebug(meta)
assert.Equal(t, "val1,val2", os.Getenv("GODEBUG"))
})
t.Run("GODEBUG in context metadata can set env", func(t *testing.T) {
t.Setenv("GODEBUG", "")
meta := store.Metadata{
Metadata: DockerContext{
AdditionalFields: map[string]any{
"GODEBUG": "val1,val2=1",
},
},
}
setGoDebug(meta)
assert.Equal(t, "val1,val2=1", os.Getenv("GODEBUG"))
})
}
func TestNewDockerCliWithCustomUserAgent(t *testing.T) {
var received string
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
received = r.UserAgent()
w.WriteHeader(http.StatusOK)
}))
defer ts.Close()
host := strings.Replace(ts.URL, "http://", "tcp://", 1)
opts := &flags.ClientOptions{Hosts: []string{host}}
cli, err := NewDockerCli(
WithUserAgent("fake-agent/0.0.1"),
)
assert.NilError(t, err)
cli.currentContext = DefaultContextName
cli.options = opts
cli.configFile = &configfile.ConfigFile{}
_, err = cli.Client().Ping(context.TODO(), client.PingOptions{})
assert.NilError(t, err)
assert.DeepEqual(t, received, "fake-agent/0.0.1")
}
func TestNewDockerCliWithAPIClientOptions(t *testing.T) {
var received string
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
received = r.UserAgent()
w.WriteHeader(http.StatusOK)
}))
defer ts.Close()
host := strings.Replace(ts.URL, "http://", "tcp://", 1)
opts := &flags.ClientOptions{Hosts: []string{host}}
cli, err := NewDockerCli(
WithAPIClientOptions(client.WithUserAgent("fake-agent/0.0.1")),
)
assert.NilError(t, err)
cli.currentContext = DefaultContextName
cli.options = opts
cli.configFile = &configfile.ConfigFile{}
_, err = cli.Client().Ping(t.Context(), client.PingOptions{})
assert.NilError(t, err)
assert.DeepEqual(t, received, "fake-agent/0.0.1")
}
+100 -21
View File
@@ -1,30 +1,109 @@
package commands package commands
import ( import (
"os"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
_ "github.com/docker/cli/cli/command/builder" "github.com/docker/cli/cli/command/builder"
_ "github.com/docker/cli/cli/command/checkpoint" "github.com/docker/cli/cli/command/checkpoint"
_ "github.com/docker/cli/cli/command/config" "github.com/docker/cli/cli/command/config"
_ "github.com/docker/cli/cli/command/container" "github.com/docker/cli/cli/command/container"
_ "github.com/docker/cli/cli/command/context" "github.com/docker/cli/cli/command/context"
_ "github.com/docker/cli/cli/command/image" "github.com/docker/cli/cli/command/image"
_ "github.com/docker/cli/cli/command/manifest" "github.com/docker/cli/cli/command/manifest"
_ "github.com/docker/cli/cli/command/network" "github.com/docker/cli/cli/command/network"
_ "github.com/docker/cli/cli/command/node" "github.com/docker/cli/cli/command/node"
_ "github.com/docker/cli/cli/command/plugin" "github.com/docker/cli/cli/command/plugin"
_ "github.com/docker/cli/cli/command/registry" "github.com/docker/cli/cli/command/registry"
_ "github.com/docker/cli/cli/command/secret" "github.com/docker/cli/cli/command/secret"
_ "github.com/docker/cli/cli/command/service" "github.com/docker/cli/cli/command/service"
_ "github.com/docker/cli/cli/command/stack" "github.com/docker/cli/cli/command/stack"
_ "github.com/docker/cli/cli/command/swarm" "github.com/docker/cli/cli/command/swarm"
_ "github.com/docker/cli/cli/command/system" "github.com/docker/cli/cli/command/system"
_ "github.com/docker/cli/cli/command/volume" "github.com/docker/cli/cli/command/trust"
"github.com/docker/cli/internal/commands" "github.com/docker/cli/cli/command/volume"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func AddCommands(cmd *cobra.Command, dockerCLI command.Cli) { // AddCommands adds all the commands from cli/command to the root command
for _, c := range commands.Commands() { func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
cmd.AddCommand(c(dockerCLI)) cmd.AddCommand(
// commonly used shorthands
container.NewRunCommand(dockerCli),
container.NewExecCommand(dockerCli),
container.NewPsCommand(dockerCli),
image.NewBuildCommand(dockerCli),
image.NewPullCommand(dockerCli),
image.NewPushCommand(dockerCli),
image.NewImagesCommand(dockerCli),
registry.NewLoginCommand(dockerCli),
registry.NewLogoutCommand(dockerCli),
registry.NewSearchCommand(dockerCli),
system.NewVersionCommand(dockerCli),
system.NewInfoCommand(dockerCli),
// management commands
builder.NewBuilderCommand(dockerCli),
checkpoint.NewCheckpointCommand(dockerCli),
container.NewContainerCommand(dockerCli),
context.NewContextCommand(dockerCli),
image.NewImageCommand(dockerCli),
manifest.NewManifestCommand(dockerCli),
network.NewNetworkCommand(dockerCli),
plugin.NewPluginCommand(dockerCli),
system.NewSystemCommand(dockerCli),
trust.NewTrustCommand(dockerCli),
volume.NewVolumeCommand(dockerCli),
// orchestration (swarm) commands
config.NewConfigCommand(dockerCli),
node.NewNodeCommand(dockerCli),
secret.NewSecretCommand(dockerCli),
service.NewServiceCommand(dockerCli),
stack.NewStackCommand(dockerCli),
swarm.NewSwarmCommand(dockerCli),
// legacy commands may be hidden
hide(container.NewAttachCommand(dockerCli)),
hide(container.NewCommitCommand(dockerCli)),
hide(container.NewCopyCommand(dockerCli)),
hide(container.NewCreateCommand(dockerCli)),
hide(container.NewDiffCommand(dockerCli)),
hide(container.NewExportCommand(dockerCli)),
hide(container.NewKillCommand(dockerCli)),
hide(container.NewLogsCommand(dockerCli)),
hide(container.NewPauseCommand(dockerCli)),
hide(container.NewPortCommand(dockerCli)),
hide(container.NewRenameCommand(dockerCli)),
hide(container.NewRestartCommand(dockerCli)),
hide(container.NewRmCommand(dockerCli)),
hide(container.NewStartCommand(dockerCli)),
hide(container.NewStatsCommand(dockerCli)),
hide(container.NewStopCommand(dockerCli)),
hide(container.NewTopCommand(dockerCli)),
hide(container.NewUnpauseCommand(dockerCli)),
hide(container.NewUpdateCommand(dockerCli)),
hide(container.NewWaitCommand(dockerCli)),
hide(image.NewHistoryCommand(dockerCli)),
hide(image.NewImportCommand(dockerCli)),
hide(image.NewLoadCommand(dockerCli)),
hide(image.NewRemoveCommand(dockerCli)),
hide(image.NewSaveCommand(dockerCli)),
hide(image.NewTagCommand(dockerCli)),
hide(system.NewEventsCommand(dockerCli)),
hide(system.NewInspectCommand(dockerCli)),
)
}
func hide(cmd *cobra.Command) *cobra.Command {
// If the environment variable with name "DOCKER_HIDE_LEGACY_COMMANDS" is not empty,
// these legacy commands (such as `docker ps`, `docker exec`, etc)
// will not be shown in output console.
if os.Getenv("DOCKER_HIDE_LEGACY_COMMANDS") == "" {
return cmd
} }
cmdCopy := *cmd
cmdCopy.Hidden = true
cmdCopy.Aliases = []string{}
return &cmdCopy
} }
+34 -216
View File
@@ -2,81 +2,39 @@ package completion
import ( import (
"os" "os"
"strings"
"github.com/distribution/reference" "github.com/docker/cli/cli/command"
"github.com/moby/moby/api/types/container" "github.com/docker/cli/cli/command/formatter"
"github.com/moby/moby/client" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/volume"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
// APIClientProvider provides a method to get a [client.APIClient], initializing // ValidArgsFn a function to be used by cobra command as `ValidArgsFunction` to offer command line completion
// it if needed. type ValidArgsFn func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective)
//
// It's a smaller interface than [command.Cli], and used in situations where an
// APIClient is needed, but we want to postpone initializing the client until
// it's used.
type APIClientProvider interface {
Client() client.APIClient
}
// ImageNames offers completion for images present within the local store // ImageNames offers completion for images present within the local store
func ImageNames(dockerCLI APIClientProvider, limit int) cobra.CompletionFunc { func ImageNames(dockerCli command.Cli) ValidArgsFn {
return Unique(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if limit > 0 && len(args) >= limit { list, err := dockerCli.Client().ImageList(cmd.Context(), types.ImageListOptions{})
return nil, cobra.ShellCompDirectiveNoFileComp
}
res, err := dockerCLI.Client().ImageList(cmd.Context(), client.ImageListOptions{})
if err != nil { if err != nil {
return nil, cobra.ShellCompDirectiveError return nil, cobra.ShellCompDirectiveError
} }
var names []string var names []string
for _, img := range res.Items { for _, image := range list {
names = append(names, img.RepoTags...) names = append(names, image.RepoTags...)
} }
return names, cobra.ShellCompDirectiveNoFileComp return names, cobra.ShellCompDirectiveNoFileComp
}) }
}
// ImageNamesWithBase offers completion for images present within the local store,
// including both full image names with tags and base image names (repository names only)
// when multiple tags exist for the same base name
func ImageNamesWithBase(dockerCLI APIClientProvider, limit int) cobra.CompletionFunc {
return Unique(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if limit > 0 && len(args) >= limit {
return nil, cobra.ShellCompDirectiveNoFileComp
}
res, err := dockerCLI.Client().ImageList(cmd.Context(), client.ImageListOptions{})
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
var names []string
baseNameCounts := make(map[string]int)
for _, img := range res.Items {
names = append(names, img.RepoTags...)
for _, tag := range img.RepoTags {
ref, err := reference.ParseNormalizedNamed(tag)
if err != nil {
continue
}
baseNameCounts[reference.FamiliarName(ref)]++
}
}
for baseName, count := range baseNameCounts {
if count > 1 {
names = append(names, baseName)
}
}
return names, cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
})
} }
// ContainerNames offers completion for container names and IDs // ContainerNames offers completion for container names and IDs
// By default, only names are returned. // By default, only names are returned.
// Set DOCKER_COMPLETION_SHOW_CONTAINER_IDS=yes to also complete IDs. // Set DOCKER_COMPLETION_SHOW_CONTAINER_IDS=yes to also complete IDs.
func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(container.Summary) bool) cobra.CompletionFunc { func ContainerNames(dockerCli command.Cli, all bool, filters ...func(types.Container) bool) ValidArgsFn {
return Unique(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
res, err := dockerCLI.Client().ContainerList(cmd.Context(), client.ContainerListOptions{ list, err := dockerCli.Client().ContainerList(cmd.Context(), container.ListOptions{
All: all, All: all,
}) })
if err != nil { if err != nil {
@@ -86,10 +44,10 @@ func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(conta
showContainerIDs := os.Getenv("DOCKER_COMPLETION_SHOW_CONTAINER_IDS") == "yes" showContainerIDs := os.Getenv("DOCKER_COMPLETION_SHOW_CONTAINER_IDS") == "yes"
var names []string var names []string
for _, ctr := range res.Items { for _, container := range list {
skip := false skip := false
for _, fn := range filters { for _, fn := range filters {
if fn != nil && !fn(ctr) { if !fn(container) {
skip = true skip = true
break break
} }
@@ -98,185 +56,45 @@ func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(conta
continue continue
} }
if showContainerIDs { if showContainerIDs {
names = append(names, ctr.ID) names = append(names, container.ID)
}
for _, n := range ctr.Names {
// Skip legacy link names: "/linked-container/link-name"
if len(n) <= 1 || strings.IndexByte(n[1:], '/') != -1 {
continue
}
names = append(names, strings.TrimPrefix(n, "/"))
} }
names = append(names, formatter.StripNamePrefix(container.Names)...)
} }
return names, cobra.ShellCompDirectiveNoFileComp return names, cobra.ShellCompDirectiveNoFileComp
}) }
} }
// VolumeNames offers completion for volumes // VolumeNames offers completion for volumes
func VolumeNames(dockerCLI APIClientProvider) cobra.CompletionFunc { func VolumeNames(dockerCli command.Cli) ValidArgsFn {
return Unique(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
res, err := dockerCLI.Client().VolumeList(cmd.Context(), client.VolumeListOptions{}) list, err := dockerCli.Client().VolumeList(cmd.Context(), volume.ListOptions{})
if err != nil { if err != nil {
return nil, cobra.ShellCompDirectiveError return nil, cobra.ShellCompDirectiveError
} }
var names []string var names []string
for _, vol := range res.Items { for _, vol := range list.Volumes {
names = append(names, vol.Name) names = append(names, vol.Name)
} }
return names, cobra.ShellCompDirectiveNoFileComp return names, cobra.ShellCompDirectiveNoFileComp
}) }
} }
// NetworkNames offers completion for networks // NetworkNames offers completion for networks
func NetworkNames(dockerCLI APIClientProvider) cobra.CompletionFunc { func NetworkNames(dockerCli command.Cli) ValidArgsFn {
return Unique(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
res, err := dockerCLI.Client().NetworkList(cmd.Context(), client.NetworkListOptions{}) list, err := dockerCli.Client().NetworkList(cmd.Context(), types.NetworkListOptions{})
if err != nil { if err != nil {
return nil, cobra.ShellCompDirectiveError return nil, cobra.ShellCompDirectiveError
} }
var names []string var names []string
for _, nw := range res.Items { for _, network := range list {
names = append(names, nw.Name) names = append(names, network.Name)
} }
return names, cobra.ShellCompDirectiveNoFileComp return names, cobra.ShellCompDirectiveNoFileComp
})
}
// EnvVarNames offers completion for environment-variable names. This
// completion can be used for "--env" and "--build-arg" flags, which
// allow obtaining the value of the given environment-variable if present
// in the local environment, so we only should complete the names of the
// environment variables, and not their value. This also prevents the
// completion script from printing values of environment variables
// containing sensitive values.
//
// For example;
//
// export MY_VAR=hello
// docker run --rm --env MY_VAR alpine printenv MY_VAR
// hello
func EnvVarNames() cobra.CompletionFunc {
return Unique(func(_ *cobra.Command, _ []string, _ string) (names []string, _ cobra.ShellCompDirective) {
envs := os.Environ()
names = make([]string, 0, len(envs))
for _, env := range envs {
name, _, _ := strings.Cut(env, "=")
names = append(names, name)
}
return names, cobra.ShellCompDirectiveNoFileComp
})
}
// FromList offers completion for the given list of options.
func FromList(options ...string) cobra.CompletionFunc {
return Unique(cobra.FixedCompletions(options, cobra.ShellCompDirectiveNoFileComp))
}
// WithPrefix prefixes every element in the slice with the given prefix.
// It is a helper for building "--filter" completions, where each candidate
// value is offered as "key=value".
func WithPrefix(prefix string, values []string) []string {
result := make([]string, len(values))
for i, v := range values {
result[i] = prefix + v
}
return result
}
// WithSuffix appends the given suffix to every element in the slice. It is a
// helper for building "--filter" completions, where filter keys are offered
// with a trailing "=" (combined with [cobra.ShellCompDirectiveNoSpace]).
func WithSuffix(suffix string, values []string) []string {
result := make([]string, len(values))
for i, v := range values {
result[i] = v + suffix
}
return result
}
// FileNames is a convenience function to use [cobra.ShellCompDirectiveDefault],
// which indicates to let the shell perform its default behavior after
// completions have been provided.
func FileNames() cobra.CompletionFunc {
return func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveDefault
} }
} }
var commonPlatforms = []string{ // NoComplete is used for commands where there's no relevant completion
"linux/386", func NoComplete(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
"linux/amd64", return nil, cobra.ShellCompDirectiveNoFileComp
"linux/arm",
"linux/arm/v5",
"linux/arm/v6",
"linux/arm/v7",
"linux/arm64",
"linux/arm64/v8",
// IBM power and z platforms
"linux/ppc64le",
"linux/s390x",
// Not yet supported
"linux/riscv64",
"windows/amd64",
"wasip1/wasm",
}
// Platforms offers completion for platform-strings. It provides a non-exhaustive
// list of platforms to be used for completion. Platform-strings are based on
// [runtime.GOOS] and [runtime.GOARCH], but with (optional) variants added. A
// list of recognised os/arch combinations from the Go runtime can be obtained
// through "go tool dist list".
//
// Some noteworthy exclusions from this list:
//
// - arm64 images ("windows/arm64", "windows/arm64/v8") do not yet exist for windows.
// - we don't (yet) include `os-variant` for completion (as can be used for Windows images)
// - we don't (yet) include platforms for which we don't build binaries, such as
// BSD platforms (freebsd, netbsd, openbsd), android, macOS (darwin).
// - we currently exclude architectures that may have unofficial builds,
// but don't have wide adoption (and no support), such as loong64, mipsXXX,
// ppc64 (non-le) to prevent confusion.
func Platforms() cobra.CompletionFunc {
return func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return commonPlatforms, cobra.ShellCompDirectiveNoFileComp
}
}
// Unique wraps a completion func and removes completion results that are
// already consumed (i.e., appear in "args").
//
// For example:
//
// # initial completion: args is empty, so all results are shown
// command <tab>
// one two three
//
// # "one" is already used so omitted
// command one <tab>
// two three
func Unique(fn cobra.CompletionFunc) cobra.CompletionFunc {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
all, dir := fn(cmd, args, toComplete)
if len(all) == 0 || len(args) == 0 {
return all, dir
}
alreadyCompleted := make(map[string]struct{}, len(args))
for _, a := range args {
alreadyCompleted[a] = struct{}{}
}
out := make([]string, 0, len(all))
for _, c := range all {
if _, ok := alreadyCompleted[c]; !ok {
out = append(out, c)
}
}
return out, dir
}
} }
-386
View File
@@ -1,386 +0,0 @@
package completion
import (
"context"
"errors"
"testing"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/image"
"github.com/moby/moby/api/types/network"
"github.com/moby/moby/api/types/volume"
"github.com/moby/moby/client"
"github.com/spf13/cobra"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/env"
)
type fakeCLI struct {
*fakeClient
}
// Client implements [APIClientProvider].
func (c fakeCLI) Client() client.APIClient {
return c.fakeClient
}
type fakeClient struct {
client.Client
containerListFunc func(context.Context, client.ContainerListOptions) (client.ContainerListResult, error)
imageListFunc func(context.Context, client.ImageListOptions) (client.ImageListResult, error)
networkListFunc func(context.Context, client.NetworkListOptions) (client.NetworkListResult, error)
volumeListFunc func(context.Context, client.VolumeListOptions) (client.VolumeListResult, error)
}
func (c *fakeClient) ContainerList(ctx context.Context, options client.ContainerListOptions) (client.ContainerListResult, error) {
if c.containerListFunc != nil {
return c.containerListFunc(ctx, options)
}
return client.ContainerListResult{}, nil
}
func (c *fakeClient) ImageList(ctx context.Context, options client.ImageListOptions) (client.ImageListResult, error) {
if c.imageListFunc != nil {
return c.imageListFunc(ctx, options)
}
return client.ImageListResult{}, nil
}
func (c *fakeClient) NetworkList(ctx context.Context, options client.NetworkListOptions) (client.NetworkListResult, error) {
if c.networkListFunc != nil {
return c.networkListFunc(ctx, options)
}
return client.NetworkListResult{}, nil
}
func (c *fakeClient) VolumeList(ctx context.Context, options client.VolumeListOptions) (client.VolumeListResult, error) {
if c.volumeListFunc != nil {
return c.volumeListFunc(ctx, options)
}
return client.VolumeListResult{}, nil
}
func TestCompleteContainerNames(t *testing.T) {
tests := []struct {
doc string
showAll, showIDs bool
filters []func(container.Summary) bool
containers []container.Summary
expOut []string
expOpts client.ContainerListOptions
expDirective cobra.ShellCompDirective
}{
{
doc: "no results",
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "all containers",
showAll: true,
containers: []container.Summary{
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b"}},
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}},
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
},
expOut: []string{"container-c", "container-b", "container-a"},
expOpts: client.ContainerListOptions{All: true},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "all containers with ids",
showAll: true,
showIDs: true,
containers: []container.Summary{
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b"}},
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}},
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
},
expOut: []string{"id-c", "container-c", "id-b", "container-b", "id-a", "container-a"},
expOpts: client.ContainerListOptions{All: true},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "only running containers",
showAll: false,
containers: []container.Summary{
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b"}},
},
expOut: []string{"container-c"},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "with filter",
showAll: true,
filters: []func(container.Summary) bool{
func(ctr container.Summary) bool { return ctr.State == container.StateCreated },
},
containers: []container.Summary{
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c"}},
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}},
{ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}},
},
expOut: []string{"container-b"},
expOpts: client.ContainerListOptions{All: true},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "multiple filters",
showAll: true,
filters: []func(container.Summary) bool{
func(ctr container.Summary) bool { return ctr.ID == "id-a" },
func(ctr container.Summary) bool { return ctr.State == container.StateCreated },
},
containers: []container.Summary{
{ID: "id-c", State: container.StateRunning, Names: []string{"/container-c"}},
{ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}},
{ID: "id-a", State: container.StateCreated, Names: []string{"/container-a"}},
},
expOut: []string{"container-a"},
expOpts: client.ContainerListOptions{All: true},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "with error",
expDirective: cobra.ShellCompDirectiveError,
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
if tc.showIDs {
t.Setenv("DOCKER_COMPLETION_SHOW_CONTAINER_IDS", "yes")
}
comp := ContainerNames(fakeCLI{&fakeClient{
containerListFunc: func(_ context.Context, opts client.ContainerListOptions) (client.ContainerListResult, error) {
assert.Check(t, is.DeepEqual(opts, tc.expOpts))
if tc.expDirective == cobra.ShellCompDirectiveError {
return client.ContainerListResult{}, errors.New("some error occurred")
}
return client.ContainerListResult{Items: tc.containers}, nil
},
}}, tc.showAll, tc.filters...)
containers, directives := comp(&cobra.Command{}, nil, "")
assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective))
assert.Check(t, is.DeepEqual(containers, tc.expOut))
})
}
}
func TestCompleteEnvVarNames(t *testing.T) {
env.PatchAll(t, map[string]string{
"ENV_A": "hello-a",
"ENV_B": "hello-b",
})
values, directives := EnvVarNames()(nil, nil, "")
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
expected := []string{"ENV_A", "ENV_B"}
assert.Check(t, is.DeepEqual(values, expected, cmpopts.SortSlices(func(a, b string) bool {
return a < b
})))
}
func TestCompleteFileNames(t *testing.T) {
values, directives := FileNames()(nil, nil, "")
assert.Check(t, is.Equal(directives, cobra.ShellCompDirectiveDefault))
assert.Check(t, is.Len(values, 0))
}
func TestCompleteFromList(t *testing.T) {
expected := []string{"one", "two", "three"}
values, directives := FromList(expected...)(nil, nil, "")
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
assert.Check(t, is.DeepEqual(values, expected))
}
func TestWithPrefix(t *testing.T) {
assert.Check(t, is.DeepEqual(WithPrefix("node=", []string{"n1", "n2"}), []string{"node=n1", "node=n2"}))
assert.Check(t, is.DeepEqual(WithPrefix("node=", []string{}), []string{}))
assert.Check(t, is.DeepEqual(WithPrefix("", []string{"n1"}), []string{"n1"}))
}
func TestWithSuffix(t *testing.T) {
assert.Check(t, is.DeepEqual(WithSuffix("=", []string{"id", "name"}), []string{"id=", "name="}))
assert.Check(t, is.DeepEqual(WithSuffix("=", []string{}), []string{}))
assert.Check(t, is.DeepEqual(WithSuffix("", []string{"id"}), []string{"id"}))
}
func TestCompleteImageNames(t *testing.T) {
tests := []struct {
doc string
images []image.Summary
expOut []string
expDirective cobra.ShellCompDirective
}{
{
doc: "no results",
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "with results",
images: []image.Summary{
{RepoTags: []string{"image-c:latest", "image-c:other"}},
{RepoTags: []string{"image-b:latest", "image-b:other"}},
{RepoTags: []string{"image-a:latest", "image-a:other"}},
},
expOut: []string{"image-c:latest", "image-c:other", "image-b:latest", "image-b:other", "image-a:latest", "image-a:other"},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "with error",
expDirective: cobra.ShellCompDirectiveError,
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
comp := ImageNames(fakeCLI{&fakeClient{
imageListFunc: func(context.Context, client.ImageListOptions) (client.ImageListResult, error) {
if tc.expDirective == cobra.ShellCompDirectiveError {
return client.ImageListResult{}, errors.New("some error occurred")
}
return client.ImageListResult{Items: tc.images}, nil
},
}}, -1)
volumes, directives := comp(&cobra.Command{}, nil, "")
assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective))
assert.Check(t, is.DeepEqual(volumes, tc.expOut))
})
}
}
func TestCompleteNetworkNames(t *testing.T) {
tests := []struct {
doc string
networks []network.Summary
expOut []string
expDirective cobra.ShellCompDirective
}{
{
doc: "no results",
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "with results",
networks: []network.Summary{
{
Network: network.Network{
ID: "nw-c",
Name: "network-c",
},
},
{
Network: network.Network{
ID: "nw-b",
Name: "network-b",
},
},
{
Network: network.Network{
ID: "nw-a",
Name: "network-a",
},
},
},
expOut: []string{"network-c", "network-b", "network-a"},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "with error",
expDirective: cobra.ShellCompDirectiveError,
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
comp := NetworkNames(fakeCLI{&fakeClient{
networkListFunc: func(context.Context, client.NetworkListOptions) (client.NetworkListResult, error) {
if tc.expDirective == cobra.ShellCompDirectiveError {
return client.NetworkListResult{}, errors.New("some error occurred")
}
return client.NetworkListResult{Items: tc.networks}, nil
},
}})
volumes, directives := comp(&cobra.Command{}, nil, "")
assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective))
assert.Check(t, is.DeepEqual(volumes, tc.expOut))
})
}
}
func TestCompletePlatforms(t *testing.T) {
values, directives := Platforms()(nil, nil, "")
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
assert.Check(t, is.DeepEqual(values, commonPlatforms))
}
func TestCompleteVolumeNames(t *testing.T) {
tests := []struct {
doc string
volumes []volume.Volume
expOut []string
expDirective cobra.ShellCompDirective
}{
{
doc: "no results",
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "with results",
volumes: []volume.Volume{
{Name: "volume-c"},
{Name: "volume-b"},
{Name: "volume-a"},
},
expOut: []string{"volume-c", "volume-b", "volume-a"},
expDirective: cobra.ShellCompDirectiveNoFileComp,
},
{
doc: "with error",
expDirective: cobra.ShellCompDirectiveError,
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
comp := VolumeNames(fakeCLI{&fakeClient{
volumeListFunc: func(context.Context, client.VolumeListOptions) (client.VolumeListResult, error) {
if tc.expDirective == cobra.ShellCompDirectiveError {
return client.VolumeListResult{}, errors.New("some error occurred")
}
return client.VolumeListResult{Items: tc.volumes}, nil
},
}})
volumes, directives := comp(&cobra.Command{}, nil, "")
assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective))
assert.Check(t, is.DeepEqual(volumes, tc.expOut))
})
}
}
func TestUnique(t *testing.T) {
base := []string{"alpha", "beta", "gamma"}
comp := Unique(func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
return base, cobra.ShellCompDirectiveNoFileComp
})
values, directives := comp(&cobra.Command{}, []string{"beta"}, "")
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp))
assert.Check(t, is.DeepEqual(values, []string{"alpha", "gamma"}))
assert.Check(t, is.DeepEqual(base, []string{"alpha", "beta", "gamma"}))
values, directives = comp(&cobra.Command{}, []string{"gamma"}, "")
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp))
assert.Check(t, is.DeepEqual(values, []string{"alpha", "beta"}))
assert.Check(t, is.DeepEqual(base, []string{"alpha", "beta", "gamma"}))
}
+18 -16
View File
@@ -3,41 +3,43 @@ package config
import ( import (
"context" "context"
"github.com/moby/moby/client" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
) )
type fakeClient struct { type fakeClient struct {
client.Client client.Client
configCreateFunc func(context.Context, client.ConfigCreateOptions) (client.ConfigCreateResult, error) configCreateFunc func(context.Context, swarm.ConfigSpec) (types.ConfigCreateResponse, error)
configInspectFunc func(context.Context, string, client.ConfigInspectOptions) (client.ConfigInspectResult, error) configInspectFunc func(context.Context, string) (swarm.Config, []byte, error)
configListFunc func(context.Context, client.ConfigListOptions) (client.ConfigListResult, error) configListFunc func(context.Context, types.ConfigListOptions) ([]swarm.Config, error)
configRemoveFunc func(context.Context, string, client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) configRemoveFunc func(string) error
} }
func (c *fakeClient) ConfigCreate(ctx context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) { func (c *fakeClient) ConfigCreate(ctx context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
if c.configCreateFunc != nil { if c.configCreateFunc != nil {
return c.configCreateFunc(ctx, options) return c.configCreateFunc(ctx, spec)
} }
return client.ConfigCreateResult{}, nil return types.ConfigCreateResponse{}, nil
} }
func (c *fakeClient) ConfigInspect(ctx context.Context, id string, options client.ConfigInspectOptions) (client.ConfigInspectResult, error) { func (c *fakeClient) ConfigInspectWithRaw(ctx context.Context, id string) (swarm.Config, []byte, error) {
if c.configInspectFunc != nil { if c.configInspectFunc != nil {
return c.configInspectFunc(ctx, id, options) return c.configInspectFunc(ctx, id)
} }
return client.ConfigInspectResult{}, nil return swarm.Config{}, nil, nil
} }
func (c *fakeClient) ConfigList(ctx context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) { func (c *fakeClient) ConfigList(ctx context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
if c.configListFunc != nil { if c.configListFunc != nil {
return c.configListFunc(ctx, options) return c.configListFunc(ctx, options)
} }
return client.ConfigListResult{}, nil return []swarm.Config{}, nil
} }
func (c *fakeClient) ConfigRemove(ctx context.Context, name string, options client.ConfigRemoveOptions) (client.ConfigRemoveResult, error) { func (c *fakeClient) ConfigRemove(_ context.Context, name string) error {
if c.configRemoveFunc != nil { if c.configRemoveFunc != nil {
return c.configRemoveFunc(ctx, name, options) return c.configRemoveFunc(name)
} }
return client.ConfigRemoveResult{}, nil return nil
} }
+11 -17
View File
@@ -4,46 +4,40 @@ import (
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/internal/commands" "github.com/docker/docker/api/types"
"github.com/moby/moby/client"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func init() { // NewConfigCommand returns a cobra command for `config` subcommands
commands.Register(newConfigCommand) func NewConfigCommand(dockerCli command.Cli) *cobra.Command {
}
// newConfigCommand returns a cobra command for `config` subcommands
func newConfigCommand(dockerCLI command.Cli) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "config", Use: "config",
Short: "Manage Swarm configs", Short: "Manage Swarm configs",
Args: cli.NoArgs, Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCLI.Err()), RunE: command.ShowHelp(dockerCli.Err()),
Annotations: map[string]string{ Annotations: map[string]string{
"version": "1.30", "version": "1.30",
"swarm": "manager", "swarm": "manager",
}, },
DisableFlagsInUseLine: true,
} }
cmd.AddCommand( cmd.AddCommand(
newConfigListCommand(dockerCLI), newConfigListCommand(dockerCli),
newConfigCreateCommand(dockerCLI), newConfigCreateCommand(dockerCli),
newConfigInspectCommand(dockerCLI), newConfigInspectCommand(dockerCli),
newConfigRemoveCommand(dockerCLI), newConfigRemoveCommand(dockerCli),
) )
return cmd return cmd
} }
// completeNames offers completion for swarm configs // completeNames offers completion for swarm configs
func completeNames(dockerCLI completion.APIClientProvider) cobra.CompletionFunc { func completeNames(dockerCli command.Cli) completion.ValidArgsFn {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
res, err := dockerCLI.Client().ConfigList(cmd.Context(), client.ConfigListOptions{}) list, err := dockerCli.Client().ConfigList(cmd.Context(), types.ConfigListOptions{})
if err != nil { if err != nil {
return nil, cobra.ShellCompDirectiveError return nil, cobra.ShellCompDirectiveError
} }
var names []string var names []string
for _, config := range res.Items { for _, config := range list {
names = append(names, config.ID) names = append(names, config.ID)
} }
return names, cobra.ShellCompDirectiveNoFileComp return names, cobra.ShellCompDirectiveNoFileComp
+40 -97
View File
@@ -2,30 +2,30 @@ package config
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/completion"
"github.com/docker/cli/opts" "github.com/docker/cli/opts"
"github.com/moby/moby/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"github.com/moby/moby/client"
"github.com/moby/sys/sequential" "github.com/moby/sys/sequential"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
// createOptions specifies some options that are used when creating a config. // CreateOptions specifies some options that are used when creating a config.
type createOptions struct { type CreateOptions struct {
name string Name string
templateDriver string TemplateDriver string
file string File string
labels opts.ListOpts Labels opts.ListOpts
} }
func newConfigCreateCommand(dockerCLI command.Cli) *cobra.Command { func newConfigCreateCommand(dockerCli command.Cli) *cobra.Command {
createOpts := createOptions{ createOpts := CreateOptions{
labels: opts.NewListOpts(opts.ValidateLabel), Labels: opts.NewListOpts(opts.ValidateLabel),
} }
cmd := &cobra.Command{ cmd := &cobra.Command{
@@ -33,113 +33,56 @@ func newConfigCreateCommand(dockerCLI command.Cli) *cobra.Command {
Short: "Create a config from a file or STDIN", Short: "Create a config from a file or STDIN",
Args: cli.ExactArgs(2), Args: cli.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
createOpts.name = args[0] createOpts.Name = args[0]
createOpts.file = args[1] createOpts.File = args[1]
return runCreate(cmd.Context(), dockerCLI, createOpts) return RunConfigCreate(cmd.Context(), dockerCli, createOpts)
}, },
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: completion.NoComplete,
switch len(args) {
case 0:
// No completion for the first argument, which is the name for
// the new config, but if a non-empty name is given, we return
// it as completion to allow "tab"-ing to the next completion.
return []string{toComplete}, cobra.ShellCompDirectiveNoFileComp
case 1:
// Second argument is either "-" or a file to load.
//
// TODO(thaJeztah): provide completion for "-".
return nil, cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveDefault
default:
// Command only accepts two arguments.
return nil, cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
}
},
DisableFlagsInUseLine: true,
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.VarP(&createOpts.labels, "label", "l", "Config labels") flags.VarP(&createOpts.Labels, "label", "l", "Config labels")
flags.StringVar(&createOpts.templateDriver, "template-driver", "", "Template driver") flags.StringVar(&createOpts.TemplateDriver, "template-driver", "", "Template driver")
_ = flags.SetAnnotation("template-driver", "version", []string{"1.37"}) flags.SetAnnotation("template-driver", "version", []string{"1.37"})
return cmd return cmd
} }
// runCreate creates a config with the given options. // RunConfigCreate creates a config with the given options.
func runCreate(ctx context.Context, dockerCLI command.Cli, options createOptions) error { func RunConfigCreate(ctx context.Context, dockerCli command.Cli, options CreateOptions) error {
apiClient := dockerCLI.Client() client := dockerCli.Client()
configData, err := readConfigData(dockerCLI.In(), options.file) var in io.Reader = dockerCli.In()
if options.File != "-" {
file, err := sequential.Open(options.File)
if err != nil {
return err
}
in = file
defer file.Close()
}
configData, err := io.ReadAll(in)
if err != nil { if err != nil {
return fmt.Errorf("error reading content from %q: %v", options.file, err) return errors.Errorf("Error reading content from %q: %v", options.File, err)
} }
spec := swarm.ConfigSpec{ spec := swarm.ConfigSpec{
Annotations: swarm.Annotations{ Annotations: swarm.Annotations{
Name: options.name, Name: options.Name,
Labels: opts.ConvertKVStringsToMap(options.labels.GetSlice()), Labels: opts.ConvertKVStringsToMap(options.Labels.GetAll()),
}, },
Data: configData, Data: configData,
} }
if options.templateDriver != "" { if options.TemplateDriver != "" {
spec.Templating = &swarm.Driver{ spec.Templating = &swarm.Driver{
Name: options.templateDriver, Name: options.TemplateDriver,
} }
} }
r, err := apiClient.ConfigCreate(ctx, client.ConfigCreateOptions{ r, err := client.ConfigCreate(ctx, spec)
Spec: spec,
})
if err != nil { if err != nil {
return err return err
} }
_, _ = fmt.Fprintln(dockerCLI.Out(), r.ID) fmt.Fprintln(dockerCli.Out(), r.ID)
return nil return nil
} }
// maxConfigSize is the maximum byte length of the [swarm.ConfigSpec.Data] field,
// as defined by [MaxConfigSize] in SwarmKit.
//
// [MaxConfigSize]: https://pkg.go.dev/github.com/moby/swarmkit/v2@v2.0.0-20250103191802-8c1959736554/manager/controlapi#MaxConfigSize
const maxConfigSize = 1000 * 1024 // 1000KB
// readConfigData reads the config from either stdin or the given fileName.
//
// It reads up to twice the maximum size of the config ([maxConfigSize]),
// just in case swarm's limit changes; this is only a safeguard to prevent
// reading arbitrary files into memory.
func readConfigData(in io.Reader, fileName string) ([]byte, error) {
switch fileName {
case "-":
data, err := io.ReadAll(io.LimitReader(in, 2*maxConfigSize))
if err != nil {
return nil, fmt.Errorf("error reading from STDIN: %w", err)
}
if len(data) == 0 {
return nil, errors.New("error reading from STDIN: data is empty")
}
return data, nil
case "":
return nil, errors.New("config file is required")
default:
// Open file with [FILE_FLAG_SEQUENTIAL_SCAN] on Windows, which
// prevents Windows from aggressively caching it. We expect this
// file to be only read once. Given that this is expected to be
// a small file, this may not be a significant optimization, so
// we could choose to omit this, and use a regular [os.Open].
//
// [FILE_FLAG_SEQUENTIAL_SCAN]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea#FILE_FLAG_SEQUENTIAL_SCAN
f, err := sequential.Open(fileName)
if err != nil {
return nil, fmt.Errorf("error reading from %s: %w", fileName, err)
}
defer f.Close()
data, err := io.ReadAll(io.LimitReader(f, 2*maxConfigSize))
if err != nil {
return nil, fmt.Errorf("error reading from %s: %w", fileName, err)
}
if len(data) == 0 {
return nil, fmt.Errorf("error reading from %s: data is empty", fileName)
}
return data, nil
}
}
+37 -41
View File
@@ -2,8 +2,6 @@ package config
import ( import (
"context" "context"
"errors"
"fmt"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
@@ -12,8 +10,9 @@ import (
"testing" "testing"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/moby/moby/api/types/swarm" "github.com/docker/docker/api/types"
"github.com/moby/moby/client" "github.com/docker/docker/api/types/swarm"
"github.com/pkg/errors"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp" is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/golden" "gotest.tools/v3/golden"
@@ -24,53 +23,50 @@ const configDataFile = "config-create-with-name.golden"
func TestConfigCreateErrors(t *testing.T) { func TestConfigCreateErrors(t *testing.T) {
testCases := []struct { testCases := []struct {
args []string args []string
configCreateFunc func(context.Context, client.ConfigCreateOptions) (client.ConfigCreateResult, error) configCreateFunc func(context.Context, swarm.ConfigSpec) (types.ConfigCreateResponse, error)
expectedError string expectedError string
}{ }{
{ {
args: []string{"too_few"}, args: []string{"too_few"},
expectedError: "requires 2 arguments", expectedError: "requires exactly 2 arguments",
}, },
{ {
args: []string{"too", "many", "arguments"}, args: []string{"too", "many", "arguments"},
expectedError: "requires 2 arguments", expectedError: "requires exactly 2 arguments",
}, },
{ {
args: []string{"name", filepath.Join("testdata", configDataFile)}, args: []string{"name", filepath.Join("testdata", configDataFile)},
configCreateFunc: func(_ context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) { configCreateFunc: func(_ context.Context, configSpec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
return client.ConfigCreateResult{}, errors.New("error creating config") return types.ConfigCreateResponse{}, errors.Errorf("error creating config")
}, },
expectedError: "error creating config", expectedError: "error creating config",
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.expectedError, func(t *testing.T) { cmd := newConfigCreateCommand(
cmd := newConfigCreateCommand( test.NewFakeCli(&fakeClient{
test.NewFakeCli(&fakeClient{ configCreateFunc: tc.configCreateFunc,
configCreateFunc: tc.configCreateFunc, }),
}), )
) cmd.SetArgs(tc.args)
cmd.SetArgs(tc.args) cmd.SetOut(io.Discard)
cmd.SetOut(io.Discard) assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
cmd.SetErr(io.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
})
} }
} }
func TestConfigCreateWithName(t *testing.T) { func TestConfigCreateWithName(t *testing.T) {
const name = "config-with-name" name := "foo"
var actual []byte var actual []byte
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
configCreateFunc: func(_ context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) { configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
if options.Spec.Name != name { if spec.Name != name {
return client.ConfigCreateResult{}, fmt.Errorf("expected name %q, got %q", name, options.Spec.Name) return types.ConfigCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name)
} }
actual = options.Spec.Data actual = spec.Data
return client.ConfigCreateResult{ return types.ConfigCreateResponse{
ID: "ID-" + options.Spec.Name, ID: "ID-" + spec.Name,
}, nil }, nil
}, },
}) })
@@ -87,7 +83,7 @@ func TestConfigCreateWithLabels(t *testing.T) {
"lbl1": "Label-foo", "lbl1": "Label-foo",
"lbl2": "Label-bar", "lbl2": "Label-bar",
} }
const name = "config-with-labels" name := "foo"
data, err := os.ReadFile(filepath.Join("testdata", configDataFile)) data, err := os.ReadFile(filepath.Join("testdata", configDataFile))
assert.NilError(t, err) assert.NilError(t, err)
@@ -101,13 +97,13 @@ func TestConfigCreateWithLabels(t *testing.T) {
} }
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
configCreateFunc: func(_ context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) { configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
if !reflect.DeepEqual(options.Spec, expected) { if !reflect.DeepEqual(spec, expected) {
return client.ConfigCreateResult{}, fmt.Errorf("expected %+v, got %+v", expected, options.Spec) return types.ConfigCreateResponse{}, errors.Errorf("expected %+v, got %+v", expected, spec)
} }
return client.ConfigCreateResult{ return types.ConfigCreateResponse{
ID: "ID-" + options.Spec.Name, ID: "ID-" + spec.Name,
}, nil }, nil
}, },
}) })
@@ -124,20 +120,20 @@ func TestConfigCreateWithTemplatingDriver(t *testing.T) {
expectedDriver := &swarm.Driver{ expectedDriver := &swarm.Driver{
Name: "template-driver", Name: "template-driver",
} }
const name = "config-with-template-driver" name := "foo"
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
configCreateFunc: func(_ context.Context, options client.ConfigCreateOptions) (client.ConfigCreateResult, error) { configCreateFunc: func(_ context.Context, spec swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
if options.Spec.Name != name { if spec.Name != name {
return client.ConfigCreateResult{}, fmt.Errorf("expected name %q, got %q", name, options.Spec.Name) return types.ConfigCreateResponse{}, errors.Errorf("expected name %q, got %q", name, spec.Name)
} }
if options.Spec.Templating.Name != expectedDriver.Name { if spec.Templating.Name != expectedDriver.Name {
return client.ConfigCreateResult{}, fmt.Errorf("expected driver %v, got %v", expectedDriver, options.Spec.Labels) return types.ConfigCreateResponse{}, errors.Errorf("expected driver %v, got %v", expectedDriver, spec.Labels)
} }
return client.ConfigCreateResult{ return types.ConfigCreateResponse{
ID: "ID-" + options.Spec.Name, ID: "ID-" + spec.Name,
}, nil }, nil
}, },
}) })
+33 -34
View File
@@ -1,19 +1,15 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.26
package config package config
import ( import (
"fmt" "fmt"
"slices"
"strings" "strings"
"time" "time"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
"github.com/docker/cli/cli/command/inspect" "github.com/docker/cli/cli/command/inspect"
"github.com/docker/go-units" "github.com/docker/docker/api/types/swarm"
"github.com/moby/moby/api/types/swarm" units "github.com/docker/go-units"
"github.com/moby/moby/client"
) )
const ( const (
@@ -34,8 +30,8 @@ Data:
{{.Data}}` {{.Data}}`
) )
// newFormat returns a Format for rendering using a configContext. // NewFormat returns a Format for rendering using a config Context
func newFormat(source string, quiet bool) formatter.Format { func NewFormat(source string, quiet bool) formatter.Format {
switch source { switch source {
case formatter.PrettyFormatKey: case formatter.PrettyFormatKey:
return configInspectPrettyTemplate return configInspectPrettyTemplate
@@ -48,28 +44,31 @@ func newFormat(source string, quiet bool) formatter.Format {
return formatter.Format(source) return formatter.Format(source)
} }
// formatWrite writes the context // FormatWrite writes the context
func formatWrite(fmtCtx formatter.Context, configs client.ConfigListResult) error { func FormatWrite(ctx formatter.Context, configs []swarm.Config) error {
cCtx := &configContext{ render := func(format func(subContext formatter.SubContext) error) error {
HeaderContext: formatter.HeaderContext{ for _, config := range configs {
Header: formatter.SubHeaderContext{
"ID": configIDHeader,
"Name": formatter.NameHeader,
"CreatedAt": configCreatedHeader,
"UpdatedAt": configUpdatedHeader,
"Labels": formatter.LabelsHeader,
},
},
}
return fmtCtx.Write(cCtx, func(format func(subContext formatter.SubContext) error) error {
for _, config := range configs.Items {
configCtx := &configContext{c: config} configCtx := &configContext{c: config}
if err := format(configCtx); err != nil { if err := format(configCtx); err != nil {
return err return err
} }
} }
return nil return nil
}) }
return ctx.Write(newConfigContext(), render)
}
func newConfigContext() *configContext {
cCtx := &configContext{}
cCtx.Header = formatter.SubHeaderContext{
"ID": configIDHeader,
"Name": formatter.NameHeader,
"CreatedAt": configCreatedHeader,
"UpdatedAt": configUpdatedHeader,
"Labels": formatter.LabelsHeader,
}
return cCtx
} }
type configContext struct { type configContext struct {
@@ -106,7 +105,6 @@ func (c *configContext) Labels() string {
for k, v := range mapLabels { for k, v := range mapLabels {
joinLabels = append(joinLabels, k+"="+v) joinLabels = append(joinLabels, k+"="+v)
} }
slices.Sort(joinLabels)
return strings.Join(joinLabels, ",") return strings.Join(joinLabels, ",")
} }
@@ -117,12 +115,12 @@ func (c *configContext) Label(name string) string {
return c.c.Spec.Annotations.Labels[name] return c.c.Spec.Annotations.Labels[name]
} }
// inspectFormatWrite renders the context for a list of configs // InspectFormatWrite renders the context for a list of configs
func inspectFormatWrite(fmtCtx formatter.Context, refs []string, getRef inspect.GetRefFunc) error { func InspectFormatWrite(ctx formatter.Context, refs []string, getRef inspect.GetRefFunc) error {
if fmtCtx.Format != configInspectPrettyTemplate { if ctx.Format != configInspectPrettyTemplate {
return inspect.Inspect(fmtCtx.Output, refs, string(fmtCtx.Format), getRef) return inspect.Inspect(ctx.Output, refs, string(ctx.Format), getRef)
} }
return fmtCtx.Write(&configInspectContext{}, func(format func(subContext formatter.SubContext) error) error { render := func(format func(subContext formatter.SubContext) error) error {
for _, ref := range refs { for _, ref := range refs {
configI, _, err := getRef(ref) configI, _, err := getRef(ref)
if err != nil { if err != nil {
@@ -137,7 +135,8 @@ func inspectFormatWrite(fmtCtx formatter.Context, refs []string, getRef inspect.
} }
} }
return nil return nil
}) }
return ctx.Write(&configInspectContext{}, render)
} }
type configInspectContext struct { type configInspectContext struct {
@@ -158,11 +157,11 @@ func (ctx *configInspectContext) Labels() map[string]string {
} }
func (ctx *configInspectContext) CreatedAt() string { func (ctx *configInspectContext) CreatedAt() string {
return formatter.PrettyPrint(ctx.Config.CreatedAt) return command.PrettyPrint(ctx.Config.CreatedAt)
} }
func (ctx *configInspectContext) UpdatedAt() string { func (ctx *configInspectContext) UpdatedAt() string {
return formatter.PrettyPrint(ctx.Config.UpdatedAt) return command.PrettyPrint(ctx.Config.UpdatedAt)
} }
func (ctx *configInspectContext) Data() string { func (ctx *configInspectContext) Data() string {
+16 -18
View File
@@ -6,8 +6,7 @@ import (
"time" "time"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
"github.com/moby/moby/api/types/swarm" "github.com/docker/docker/api/types/swarm"
"github.com/moby/moby/client"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
) )
@@ -28,46 +27,45 @@ func TestConfigContextFormatWrite(t *testing.T) {
}, },
// Table format // Table format
{ {
formatter.Context{Format: newFormat("table", false)}, formatter.Context{Format: NewFormat("table", false)},
`ID NAME CREATED UPDATED `ID NAME CREATED UPDATED
1 passwords Less than a second ago Less than a second ago 1 passwords Less than a second ago Less than a second ago
2 id_rsa Less than a second ago Less than a second ago 2 id_rsa Less than a second ago Less than a second ago
`, `,
}, },
{ {
formatter.Context{Format: newFormat("table {{.Name}}", true)}, formatter.Context{Format: NewFormat("table {{.Name}}", true)},
`NAME `NAME
passwords passwords
id_rsa id_rsa
`, `,
}, },
{ {
formatter.Context{Format: newFormat("{{.ID}}-{{.Name}}", false)}, formatter.Context{Format: NewFormat("{{.ID}}-{{.Name}}", false)},
`1-passwords `1-passwords
2-id_rsa 2-id_rsa
`, `,
}, },
} }
res := client.ConfigListResult{ configs := []swarm.Config{
Items: []swarm.Config{ {
{ ID: "1",
ID: "1", Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()},
Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()}, Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "passwords"}},
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "passwords"}}, },
}, {
{ ID: "2",
ID: "2", Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()},
Meta: swarm.Meta{CreatedAt: time.Now(), UpdatedAt: time.Now()}, Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "id_rsa"}},
Spec: swarm.ConfigSpec{Annotations: swarm.Annotations{Name: "id_rsa"}},
},
}, },
} }
for _, tc := range cases { for _, tc := range cases {
tc := tc
t.Run(string(tc.context.Format), func(t *testing.T) { t.Run(string(tc.context.Format), func(t *testing.T) {
var out bytes.Buffer var out bytes.Buffer
tc.context.Output = &out tc.context.Output = &out
if err := formatWrite(tc.context, res); err != nil { if err := FormatWrite(tc.context, configs); err != nil {
assert.ErrorContains(t, err, tc.expected) assert.ErrorContains(t, err, tc.expected)
} else { } else {
assert.Equal(t, out.String(), tc.expected) assert.Equal(t, out.String(), tc.expected)
+26 -26
View File
@@ -1,5 +1,5 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.26 //go:build go1.19
package config package config
@@ -12,61 +12,61 @@ import (
"github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/formatter" "github.com/docker/cli/cli/command/formatter"
flagsHelper "github.com/docker/cli/cli/flags" flagsHelper "github.com/docker/cli/cli/flags"
"github.com/moby/moby/client"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
// inspectOptions contains options for the docker config inspect command. // InspectOptions contains options for the docker config inspect command.
type inspectOptions struct { type InspectOptions struct {
names []string Names []string
format string Format string
pretty bool Pretty bool
} }
func newConfigInspectCommand(dockerCLI command.Cli) *cobra.Command { func newConfigInspectCommand(dockerCli command.Cli) *cobra.Command {
opts := inspectOptions{} opts := InspectOptions{}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "inspect [OPTIONS] CONFIG [CONFIG...]", Use: "inspect [OPTIONS] CONFIG [CONFIG...]",
Short: "Display detailed information on one or more configs", Short: "Display detailed information on one or more configs",
Args: cli.RequiresMinArgs(1), Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
opts.names = args opts.Names = args
return runInspect(cmd.Context(), dockerCLI, opts) return RunConfigInspect(cmd.Context(), dockerCli, opts)
},
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return completeNames(dockerCli)(cmd, args, toComplete)
}, },
ValidArgsFunction: completeNames(dockerCLI),
DisableFlagsInUseLine: true,
} }
cmd.Flags().StringVarP(&opts.format, "format", "f", "", flagsHelper.InspectFormatHelp) cmd.Flags().StringVarP(&opts.Format, "format", "f", "", flagsHelper.InspectFormatHelp)
cmd.Flags().BoolVar(&opts.pretty, "pretty", false, "Print the information in a human friendly format") cmd.Flags().BoolVar(&opts.Pretty, "pretty", false, "Print the information in a human friendly format")
return cmd return cmd
} }
// runInspect inspects the given Swarm config. // RunConfigInspect inspects the given Swarm config.
func runInspect(ctx context.Context, dockerCLI command.Cli, opts inspectOptions) error { func RunConfigInspect(ctx context.Context, dockerCli command.Cli, opts InspectOptions) error {
apiClient := dockerCLI.Client() client := dockerCli.Client()
if opts.pretty { if opts.Pretty {
opts.format = "pretty" opts.Format = "pretty"
} }
getRef := func(id string) (any, []byte, error) { getRef := func(id string) (any, []byte, error) {
res, err := apiClient.ConfigInspect(ctx, id, client.ConfigInspectOptions{}) return client.ConfigInspectWithRaw(ctx, id)
return res.Config, res.Raw, err
} }
f := opts.Format
// check if the user is trying to apply a template to the pretty format, which // check if the user is trying to apply a template to the pretty format, which
// is not supported // is not supported
if strings.HasPrefix(opts.format, "pretty") && opts.format != "pretty" { if strings.HasPrefix(f, "pretty") && f != "pretty" {
return errors.New("cannot supply extra formatting options to the pretty template") return errors.New("cannot supply extra formatting options to the pretty template")
} }
configCtx := formatter.Context{ configCtx := formatter.Context{
Output: dockerCLI.Out(), Output: dockerCli.Out(),
Format: newFormat(opts.format, false), Format: NewFormat(f, false),
} }
if err := inspectFormatWrite(configCtx, opts.names, getRef); err != nil { if err := InspectFormatWrite(configCtx, opts.Names, getRef); err != nil {
return cli.StatusError{StatusCode: 1, Status: err.Error()} return cli.StatusError{StatusCode: 1, Status: err.Error()}
} }
return nil return nil
+33 -50
View File
@@ -2,7 +2,6 @@ package config
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"testing" "testing"
@@ -10,7 +9,8 @@ import (
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/builders" "github.com/docker/cli/internal/test/builders"
"github.com/moby/moby/client" "github.com/docker/docker/api/types/swarm"
"github.com/pkg/errors"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
"gotest.tools/v3/golden" "gotest.tools/v3/golden"
) )
@@ -19,7 +19,7 @@ func TestConfigInspectErrors(t *testing.T) {
testCases := []struct { testCases := []struct {
args []string args []string
flags map[string]string flags map[string]string
configInspectFunc func(_ context.Context, configID string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) configInspectFunc func(_ context.Context, configID string) (swarm.Config, []byte, error)
expectedError string expectedError string
}{ }{
{ {
@@ -27,8 +27,8 @@ func TestConfigInspectErrors(t *testing.T) {
}, },
{ {
args: []string{"foo"}, args: []string{"foo"},
configInspectFunc: func(context.Context, string, client.ConfigInspectOptions) (client.ConfigInspectResult, error) { configInspectFunc: func(_ context.Context, configID string) (swarm.Config, []byte, error) {
return client.ConfigInspectResult{}, errors.New("error while inspecting the config") return swarm.Config{}, nil, errors.Errorf("error while inspecting the config")
}, },
expectedError: "error while inspecting the config", expectedError: "error while inspecting the config",
}, },
@@ -41,13 +41,11 @@ func TestConfigInspectErrors(t *testing.T) {
}, },
{ {
args: []string{"foo", "bar"}, args: []string{"foo", "bar"},
configInspectFunc: func(_ context.Context, configID string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) { configInspectFunc: func(_ context.Context, configID string) (swarm.Config, []byte, error) {
if configID == "foo" { if configID == "foo" {
return client.ConfigInspectResult{ return *builders.Config(builders.ConfigName("foo")), nil, nil
Config: *builders.Config(builders.ConfigName("foo")),
}, nil
} }
return client.ConfigInspectResult{}, errors.New("error while inspecting the config") return swarm.Config{}, nil, errors.Errorf("error while inspecting the config")
}, },
expectedError: "error while inspecting the config", expectedError: "error while inspecting the config",
}, },
@@ -63,7 +61,6 @@ func TestConfigInspectErrors(t *testing.T) {
assert.Check(t, cmd.Flags().Set(key, value)) assert.Check(t, cmd.Flags().Set(key, value))
} }
cmd.SetOut(io.Discard) cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError) assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
} }
} }
@@ -72,34 +69,25 @@ func TestConfigInspectWithoutFormat(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
args []string args []string
configInspectFunc func(_ context.Context, configID string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) configInspectFunc func(_ context.Context, configID string) (swarm.Config, []byte, error)
}{ }{
{ {
name: "single-config", name: "single-config",
args: []string{"foo"}, args: []string{"foo"},
configInspectFunc: func(_ context.Context, name string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) { configInspectFunc: func(_ context.Context, name string) (swarm.Config, []byte, error) {
if name != "foo" { if name != "foo" {
return client.ConfigInspectResult{}, fmt.Errorf("invalid name, expected %s, got %s", "foo", name) return swarm.Config{}, nil, errors.Errorf("Invalid name, expected %s, got %s", "foo", name)
} }
return client.ConfigInspectResult{ return *builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")), nil, nil
Config: *builders.Config(
builders.ConfigID("ID-foo"),
builders.ConfigName("foo"),
),
}, nil
}, },
}, },
{ {
name: "multiple-configs-with-labels", name: "multiple-configs-with-labels",
args: []string{"foo", "bar"}, args: []string{"foo", "bar"},
configInspectFunc: func(_ context.Context, name string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) { configInspectFunc: func(_ context.Context, name string) (swarm.Config, []byte, error) {
return client.ConfigInspectResult{ return *builders.Config(builders.ConfigID("ID-"+name), builders.ConfigName(name), builders.ConfigLabels(map[string]string{
Config: *builders.Config( "label1": "label-foo",
builders.ConfigID("ID-"+name), })), nil, nil
builders.ConfigName(name),
builders.ConfigLabels(map[string]string{"label1": "label-foo"}),
),
}, nil
}, },
}, },
} }
@@ -113,19 +101,16 @@ func TestConfigInspectWithoutFormat(t *testing.T) {
} }
func TestConfigInspectWithFormat(t *testing.T) { func TestConfigInspectWithFormat(t *testing.T) {
configInspectFunc := func(_ context.Context, name string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) { configInspectFunc := func(_ context.Context, name string) (swarm.Config, []byte, error) {
return client.ConfigInspectResult{ return *builders.Config(builders.ConfigName("foo"), builders.ConfigLabels(map[string]string{
Config: *builders.Config( "label1": "label-foo",
builders.ConfigName("foo"), })), nil, nil
builders.ConfigLabels(map[string]string{"label1": "label-foo"}),
),
}, nil
} }
testCases := []struct { testCases := []struct {
name string name string
format string format string
args []string args []string
configInspectFunc func(_ context.Context, name string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) configInspectFunc func(_ context.Context, name string) (swarm.Config, []byte, error)
}{ }{
{ {
name: "simple-template", name: "simple-template",
@@ -155,23 +140,21 @@ func TestConfigInspectWithFormat(t *testing.T) {
func TestConfigInspectPretty(t *testing.T) { func TestConfigInspectPretty(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
configInspectFunc func(context.Context, string, client.ConfigInspectOptions) (client.ConfigInspectResult, error) configInspectFunc func(context.Context, string) (swarm.Config, []byte, error)
}{ }{
{ {
name: "simple", name: "simple",
configInspectFunc: func(_ context.Context, id string, _ client.ConfigInspectOptions) (client.ConfigInspectResult, error) { configInspectFunc: func(_ context.Context, id string) (swarm.Config, []byte, error) {
return client.ConfigInspectResult{ return *builders.Config(
Config: *builders.Config( builders.ConfigLabels(map[string]string{
builders.ConfigLabels(map[string]string{ "lbl1": "value1",
"lbl1": "value1", }),
}), builders.ConfigID("configID"),
builders.ConfigID("configID"), builders.ConfigName("configName"),
builders.ConfigName("configName"), builders.ConfigCreatedAt(time.Time{}),
builders.ConfigCreatedAt(time.Time{}), builders.ConfigUpdatedAt(time.Time{}),
builders.ConfigUpdatedAt(time.Time{}), builders.ConfigData([]byte("payload here")),
builders.ConfigData([]byte("payload here")), ), []byte{}, nil
),
}, nil
}, },
}, },
} }
+27 -31
View File
@@ -1,32 +1,29 @@
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
//go:build go1.26
package config package config
import ( import (
"context" "context"
"slices" "sort"
"github.com/docker/cli/cli" "github.com/docker/cli/cli"
"github.com/docker/cli/cli/command" "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/formatter"
flagsHelper "github.com/docker/cli/cli/flags" flagsHelper "github.com/docker/cli/cli/flags"
"github.com/docker/cli/opts" "github.com/docker/cli/opts"
"github.com/docker/docker/api/types"
"github.com/fvbommel/sortorder" "github.com/fvbommel/sortorder"
"github.com/moby/moby/api/types/swarm"
"github.com/moby/moby/client"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
// listOptions contains options for the docker config ls command. // ListOptions contains options for the docker config ls command.
type listOptions struct { type ListOptions struct {
quiet bool Quiet bool
format string Format string
filter opts.FilterOpt Filter opts.FilterOpt
} }
func newConfigListCommand(dockerCLI command.Cli) *cobra.Command { func newConfigListCommand(dockerCli command.Cli) *cobra.Command {
listOpts := listOptions{filter: opts.NewFilterOpt()} listOpts := ListOptions{Filter: opts.NewFilterOpt()}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "ls [OPTIONS]", Use: "ls [OPTIONS]",
@@ -34,45 +31,44 @@ func newConfigListCommand(dockerCLI command.Cli) *cobra.Command {
Short: "List configs", Short: "List configs",
Args: cli.NoArgs, Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runList(cmd.Context(), dockerCLI, listOpts) return RunConfigList(cmd.Context(), dockerCli, listOpts)
}, },
ValidArgsFunction: cobra.NoFileCompletions, ValidArgsFunction: completion.NoComplete,
DisableFlagsInUseLine: true,
} }
flags := cmd.Flags() flags := cmd.Flags()
flags.BoolVarP(&listOpts.quiet, "quiet", "q", false, "Only display IDs") flags.BoolVarP(&listOpts.Quiet, "quiet", "q", false, "Only display IDs")
flags.StringVar(&listOpts.format, "format", "", flagsHelper.FormatHelp) flags.StringVarP(&listOpts.Format, "format", "", "", flagsHelper.FormatHelp)
flags.VarP(&listOpts.filter, "filter", "f", "Filter output based on conditions provided") flags.VarP(&listOpts.Filter, "filter", "f", "Filter output based on conditions provided")
return cmd return cmd
} }
// runList lists Swarm configs. // RunConfigList lists Swarm configs.
func runList(ctx context.Context, dockerCLI command.Cli, options listOptions) error { func RunConfigList(ctx context.Context, dockerCli command.Cli, options ListOptions) error {
apiClient := dockerCLI.Client() client := dockerCli.Client()
res, err := apiClient.ConfigList(ctx, client.ConfigListOptions{Filters: options.filter.Value()}) configs, err := client.ConfigList(ctx, types.ConfigListOptions{Filters: options.Filter.Value()})
if err != nil { if err != nil {
return err return err
} }
format := options.format format := options.Format
if len(format) == 0 { if len(format) == 0 {
if len(dockerCLI.ConfigFile().ConfigFormat) > 0 && !options.quiet { if len(dockerCli.ConfigFile().ConfigFormat) > 0 && !options.Quiet {
format = dockerCLI.ConfigFile().ConfigFormat format = dockerCli.ConfigFile().ConfigFormat
} else { } else {
format = formatter.TableFormatKey format = formatter.TableFormatKey
} }
} }
slices.SortFunc(res.Items, func(a, b swarm.Config) int { sort.Slice(configs, func(i, j int) bool {
return sortorder.NaturalCompare(a.Spec.Name, b.Spec.Name) return sortorder.NaturalLess(configs[i].Spec.Name, configs[j].Spec.Name)
}) })
configCtx := formatter.Context{ configCtx := formatter.Context{
Output: dockerCLI.Out(), Output: dockerCli.Out(),
Format: newFormat(format, options.quiet), Format: NewFormat(format, options.Quiet),
} }
return formatWrite(configCtx, res) return FormatWrite(configCtx, configs)
} }
+61 -71
View File
@@ -2,7 +2,6 @@ package config
import ( import (
"context" "context"
"errors"
"io" "io"
"testing" "testing"
"time" "time"
@@ -10,16 +9,18 @@ import (
"github.com/docker/cli/cli/config/configfile" "github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/internal/test" "github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/builders" "github.com/docker/cli/internal/test/builders"
"github.com/moby/moby/api/types/swarm" "github.com/docker/docker/api/types"
"github.com/moby/moby/client" "github.com/docker/docker/api/types/swarm"
"github.com/pkg/errors"
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/golden" "gotest.tools/v3/golden"
) )
func TestConfigListErrors(t *testing.T) { func TestConfigListErrors(t *testing.T) {
testCases := []struct { testCases := []struct {
args []string args []string
configListFunc func(context.Context, client.ConfigListOptions) (client.ConfigListResult, error) configListFunc func(context.Context, types.ConfigListOptions) ([]swarm.Config, error)
expectedError string expectedError string
}{ }{
{ {
@@ -27,8 +28,8 @@ func TestConfigListErrors(t *testing.T) {
expectedError: "accepts no argument", expectedError: "accepts no argument",
}, },
{ {
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) { configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
return client.ConfigListResult{}, errors.New("error listing configs") return []swarm.Config{}, errors.Errorf("error listing configs")
}, },
expectedError: "error listing configs", expectedError: "error listing configs",
}, },
@@ -41,35 +42,32 @@ func TestConfigListErrors(t *testing.T) {
) )
cmd.SetArgs(tc.args) cmd.SetArgs(tc.args)
cmd.SetOut(io.Discard) cmd.SetOut(io.Discard)
cmd.SetErr(io.Discard)
assert.ErrorContains(t, cmd.Execute(), tc.expectedError) assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
} }
} }
func TestConfigList(t *testing.T) { func TestConfigList(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) { configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
return client.ConfigListResult{ return []swarm.Config{
Items: []swarm.Config{ *builders.Config(builders.ConfigID("ID-1-foo"),
*builders.Config(builders.ConfigID("ID-1-foo"), builders.ConfigName("1-foo"),
builders.ConfigName("1-foo"), builders.ConfigVersion(swarm.Version{Index: 10}),
builders.ConfigVersion(swarm.Version{Index: 10}), builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)), builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)), ),
), *builders.Config(builders.ConfigID("ID-10-foo"),
*builders.Config(builders.ConfigID("ID-10-foo"), builders.ConfigName("10-foo"),
builders.ConfigName("10-foo"), builders.ConfigVersion(swarm.Version{Index: 11}),
builders.ConfigVersion(swarm.Version{Index: 11}), builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)), builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)), ),
), *builders.Config(builders.ConfigID("ID-2-foo"),
*builders.Config(builders.ConfigID("ID-2-foo"), builders.ConfigName("2-foo"),
builders.ConfigName("2-foo"), builders.ConfigVersion(swarm.Version{Index: 11}),
builders.ConfigVersion(swarm.Version{Index: 11}), builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)), builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)), ),
),
},
}, nil }, nil
}, },
}) })
@@ -80,14 +78,12 @@ func TestConfigList(t *testing.T) {
func TestConfigListWithQuietOption(t *testing.T) { func TestConfigListWithQuietOption(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) { configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
return client.ConfigListResult{ return []swarm.Config{
Items: []swarm.Config{ *builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")), *builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{ "label": "label-bar",
"label": "label-bar", })),
})),
},
}, nil }, nil
}, },
}) })
@@ -99,14 +95,12 @@ func TestConfigListWithQuietOption(t *testing.T) {
func TestConfigListWithConfigFormat(t *testing.T) { func TestConfigListWithConfigFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) { configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
return client.ConfigListResult{ return []swarm.Config{
Items: []swarm.Config{ *builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")), *builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{ "label": "label-bar",
"label": "label-bar", })),
})),
},
}, nil }, nil
}, },
}) })
@@ -120,14 +114,12 @@ func TestConfigListWithConfigFormat(t *testing.T) {
func TestConfigListWithFormat(t *testing.T) { func TestConfigListWithFormat(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) { configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
return client.ConfigListResult{ return []swarm.Config{
Items: []swarm.Config{ *builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")),
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo")), *builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"), builders.ConfigLabels(map[string]string{ "label": "label-bar",
"label": "label-bar", })),
})),
},
}, nil }, nil
}, },
}) })
@@ -139,24 +131,22 @@ func TestConfigListWithFormat(t *testing.T) {
func TestConfigListWithFilter(t *testing.T) { func TestConfigListWithFilter(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{ cli := test.NewFakeCli(&fakeClient{
configListFunc: func(_ context.Context, options client.ConfigListOptions) (client.ConfigListResult, error) { configListFunc: func(_ context.Context, options types.ConfigListOptions) ([]swarm.Config, error) {
assert.Check(t, options.Filters["name"]["foo"]) assert.Check(t, is.Equal("foo", options.Filters.Get("name")[0]))
assert.Check(t, options.Filters["label"]["lbl1=Label-bar"]) assert.Check(t, is.Equal("lbl1=Label-bar", options.Filters.Get("label")[0]))
return client.ConfigListResult{ return []swarm.Config{
Items: []swarm.Config{ *builders.Config(builders.ConfigID("ID-foo"),
*builders.Config(builders.ConfigID("ID-foo"), builders.ConfigName("foo"),
builders.ConfigName("foo"), builders.ConfigVersion(swarm.Version{Index: 10}),
builders.ConfigVersion(swarm.Version{Index: 10}), builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)), builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)), ),
), *builders.Config(builders.ConfigID("ID-bar"),
*builders.Config(builders.ConfigID("ID-bar"), builders.ConfigName("bar"),
builders.ConfigName("bar"), builders.ConfigVersion(swarm.Version{Index: 11}),
builders.ConfigVersion(swarm.Version{Index: 11}), builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)),
builders.ConfigCreatedAt(time.Now().Add(-2*time.Hour)), builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)),
builders.ConfigUpdatedAt(time.Now().Add(-1*time.Hour)), ),
),
},
}, nil }, nil
}, },
}) })

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